@hashsome/ui 0.5.0 → 0.6.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/debug.d.ts +27 -0
- package/dist/debug.js +95 -0
- package/dist/entities/artwork-ring.d.ts +4 -2
- package/dist/entities/artwork-ring.js +78 -61
- package/dist/entities/media-browser.d.ts +3 -1
- package/dist/entities/media-browser.js +41 -15
- package/dist/entities/media-player-bar.js +1 -1
- package/dist/entities/media-player-column.d.ts +5 -2
- package/dist/entities/media-player-column.js +33 -5
- package/dist/entities/media-player-full.d.ts +12 -2
- package/dist/entities/media-player-full.js +45 -3
- package/dist/entities/now-playing.d.ts +3 -1
- package/dist/entities/now-playing.js +8 -3
- package/dist/entities/top-bar.js +6 -2
- package/dist/gallery/gallery.js +3 -1
- package/dist/gallery/props-data.js +102 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/layout/board.d.ts +26 -0
- package/dist/layout/board.js +85 -0
- package/dist/layout/debug-menu.d.ts +11 -0
- package/dist/layout/debug-menu.js +99 -0
- package/dist/layout/drawer-controls.js +18 -6
- package/dist/layout/grid-overlay.d.ts +6 -0
- package/dist/layout/grid-overlay.js +143 -0
- package/dist/layout/page.d.ts +7 -1
- package/dist/layout/page.js +43 -6
- package/dist/layout/room-header.js +1 -1
- package/dist/layout/tile.js +10 -2
- package/dist/layout/top-row.d.ts +9 -0
- package/dist/layout/top-row.js +10 -0
- package/dist/provider.d.ts +7 -3
- package/dist/provider.js +39 -21
- package/dist/theme/grid.d.ts +30 -0
- package/dist/theme/grid.js +38 -0
- package/package.json +2 -2
|
@@ -3,7 +3,7 @@ import type { EntityHandle } from '../entity-handle.ts';
|
|
|
3
3
|
/** The upright "now playing" panel the media column and page share: the artwork on a record, what is playing, how
|
|
4
4
|
* far along it is, the transport buttons and a volume bar that is always there. Not exported from
|
|
5
5
|
* the package; `MediaPlayerColumn` and `MediaPlayerFull` are its public forms. */
|
|
6
|
-
export declare function NowPlaying({ handle, fallback, name, extra, onOpenArtwork, size, shuffle: shufflePlacement, }: {
|
|
6
|
+
export declare function NowPlaying({ handle, fallback, name, extra, leading, onOpenArtwork, size, shuffle: shufflePlacement, }: {
|
|
7
7
|
handle: EntityHandle<'mediaPlayer'>;
|
|
8
8
|
/** What to call the player before it reports a name. */
|
|
9
9
|
fallback: string;
|
|
@@ -11,6 +11,8 @@ export declare function NowPlaying({ handle, fallback, name, extra, onOpenArtwor
|
|
|
11
11
|
name?: string;
|
|
12
12
|
/** One more button, to the right of the transport (the browse button). Shuffle takes the slot on the left, unless it sits by the title. */
|
|
13
13
|
extra?: ReactNode;
|
|
14
|
+
/** One more button, to the left of the transport, in the slot shuffle has when it sits by the title. Not shown while shuffle is in the row. */
|
|
15
|
+
leading?: ReactNode;
|
|
14
16
|
/** Makes the artwork a button that calls this (opens the library). */
|
|
15
17
|
onOpenArtwork?: () => void;
|
|
16
18
|
/** The artwork ring's diameter in px, for a roomier player. Default 168. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
|
|
2
2
|
/** @jsxImportSource @emotion/react */
|
|
3
|
+
import { useTheme } from '@emotion/react';
|
|
3
4
|
import { Flex, Grid, Typography } from 'e-prim';
|
|
4
5
|
import { Icon } from '../icon.js';
|
|
5
6
|
import { ValueBar } from '../layout/drawer-controls.js';
|
|
@@ -13,8 +14,10 @@ import { ArtworkRing } from './artwork-ring.js';
|
|
|
13
14
|
/** The upright "now playing" panel the media column and page share: the artwork on a record, what is playing, how
|
|
14
15
|
* far along it is, the transport buttons and a volume bar that is always there. Not exported from
|
|
15
16
|
* the package; `MediaPlayerColumn` and `MediaPlayerFull` are its public forms. */
|
|
16
|
-
export function NowPlaying({ handle, fallback, name, extra, onOpenArtwork, size, shuffle: shufflePlacement = 'transport', }) {
|
|
17
|
+
export function NowPlaying({ handle, fallback, name, extra, leading, onOpenArtwork, size, shuffle: shufflePlacement = 'transport', }) {
|
|
17
18
|
const player = handle.entity;
|
|
19
|
+
const { iconCircle, space } = useTheme().density;
|
|
20
|
+
const transportWidth = 5 * iconCircle + 4 * space;
|
|
18
21
|
const { status } = handle;
|
|
19
22
|
const ready = status === 'ready';
|
|
20
23
|
const caps = player?.capabilities;
|
|
@@ -25,10 +28,12 @@ export function NowPlaying({ handle, fallback, name, extra, onOpenArtwork, size,
|
|
|
25
28
|
const duration = ready ? player?.duration : undefined;
|
|
26
29
|
// The ring and the time under the title share one position, so dragging the ring moves both.
|
|
27
30
|
const seek = useSeekHold(ready ? position : undefined, caps?.seek ? (next) => void handle.command('seek', { position: next }) : undefined);
|
|
28
|
-
return (_jsxs(Flex, { direction: "column", gap: 4, "data-status": status, children: [_jsx(ArtworkRing, { artworkUrl: player?.media?.artworkUrl, seek: seek, duration: duration, seekable: caps?.seek === true, onOpen: onOpenArtwork, ...(size !== undefined ? { size } : {})
|
|
31
|
+
return (_jsxs(Flex, { direction: "column", gap: 4, "data-status": status, css: { minHeight: 0 }, children: [_jsx(ArtworkRing, { artworkUrl: player?.media?.artworkUrl, seek: seek, duration: duration, seekable: caps?.seek === true, onOpen: onOpenArtwork, ...(size !== undefined ? { size } : {}),
|
|
32
|
+
// No wider than the row of five buttons under it (268px: five circles and the gaps between them).
|
|
33
|
+
maxSize: transportWidth }), _jsxs(Flex, { direction: "column", align: "center", gap: 0.5, color: ready ? 'text' : 'textMuted', children: [_jsxs(Flex, { align: "center", justify: "center", gap: 2, maxWidth: "100%", children: [_jsx(Typography, { as: "h2", variant: "title", noWrap: true, textOverflow: "ellipsis", overflow: "hidden", m: 0, minWidth: 0, children: ready
|
|
29
34
|
? (player?.media?.title ?? 'Nothing playing')
|
|
30
35
|
: statusLabels[status] }), caps?.shuffle && shufflePlacement === 'title' ? (_jsx(IconButton, { icon: "lu:shuffle", label: "Shuffle", size: 14, active: player?.shuffle === true, pressed: player?.shuffle === true, disabled: !ready, onClick: () => void handle.command('setShuffle', { shuffle: player?.shuffle !== true }) })) : null] }), ready && player?.media?.artist ? (_jsx(Typography, { as: "span", variant: "body", color: "textMuted", noWrap: true, textOverflow: "ellipsis", overflow: "hidden", children: [player.media.artist, player.media.album].filter(Boolean).join(' · ') })) : (_jsx(Typography, { as: "span", variant: "body", color: "textMuted", noWrap: true, textOverflow: "ellipsis", overflow: "hidden", children: name ?? player?.name ?? fallback }))] }), seek.shown !== undefined && duration ? (_jsxs(Typography, { as: "span", variant: "secondary", color: "textMuted", align: "center", css: { fontVariantNumeric: 'tabular-nums' }, children: [formatDuration(seek.shown), " / ", formatDuration(duration)] })) : null, _jsxs(Grid, { gap: 3, css: ({ density }) => ({
|
|
31
36
|
gridTemplateColumns: `repeat(5, ${density.iconCircle}px)`,
|
|
32
37
|
justifyContent: 'center',
|
|
33
|
-
}), children: [caps?.shuffle && shufflePlacement === 'transport' ? (_jsx(IconButton, { icon: "lu:shuffle", label: "Shuffle", glyph: 18, active: player?.shuffle === true, pressed: player?.shuffle === true, disabled: !ready, onClick: () => void handle.command('setShuffle', { shuffle: player?.shuffle !== true }) })) : (_jsx("span", {})), _jsx(IconButton, { icon: "lu:skip-back", label: "Previous", glyph: 18, disabled: !ready || caps?.previous === false, feedback: feedbackFor('previous'), onClick: () => press('previous', () => handle.command('previous')) }), _jsx(IconButton, { icon: playing ? 'lu:pause' : 'lu:play', label: playing ? 'Pause' : 'Play', glyph: 18, disabled: !ready, primary: true, onClick: () => void handle.command('togglePlay') }), _jsx(IconButton, { icon: "lu:skip-forward", label: "Next", glyph: 18, disabled: !ready || caps?.next === false, feedback: feedbackFor('next'), onClick: () => press('next', () => handle.command('next')) }), extra ?? _jsx("span", {})] }), caps?.volume !== false || caps.mute ? (_jsxs(Flex, { align: "center", gap: 2, children: [_jsx(PlainButton, { "aria-label": volume.muted ? 'Unmute' : 'Mute', "aria-pressed": volume.muted, title: volume.muted ? 'Unmute' : 'Mute', disabled: !ready || caps?.mute === false, onClick: volume.toggleMute, center: true, color: volume.muted ? 'accent' : 'textMuted', css: { flex: 'none' }, children: _jsx(Icon, { name: volume.muted ? 'lu:volume-x' : 'lu:volume-2', size: 16 }) }), _jsx(ValueBar, { label: "Volume level", value: volume.shown, min: 0, max: 100, keyStep: 5, height: 10, onDrag: volume.drag, onCommit: volume.commit }), _jsxs(Typography, { as: "span", variant: "secondary", color: "textMuted", minWidth: 32, align: "right", css: { flex: 'none', fontVariantNumeric: 'tabular-nums' }, children: [volume.shown, "%"] })] })) : null] }));
|
|
38
|
+
}), children: [caps?.shuffle && shufflePlacement === 'transport' ? (_jsx(IconButton, { icon: "lu:shuffle", label: "Shuffle", glyph: 18, active: player?.shuffle === true, pressed: player?.shuffle === true, disabled: !ready, onClick: () => void handle.command('setShuffle', { shuffle: player?.shuffle !== true }) })) : shufflePlacement === 'title' ? ((leading ?? _jsx("span", {}))) : (_jsx("span", {})), _jsx(IconButton, { icon: "lu:skip-back", label: "Previous", glyph: 18, disabled: !ready || caps?.previous === false, feedback: feedbackFor('previous'), onClick: () => press('previous', () => handle.command('previous')) }), _jsx(IconButton, { icon: playing ? 'lu:pause' : 'lu:play', label: playing ? 'Pause' : 'Play', glyph: 18, disabled: !ready, primary: true, onClick: () => void handle.command('togglePlay') }), _jsx(IconButton, { icon: "lu:skip-forward", label: "Next", glyph: 18, disabled: !ready || caps?.next === false, feedback: feedbackFor('next'), onClick: () => press('next', () => handle.command('next')) }), extra ?? _jsx("span", {})] }), caps?.volume !== false || caps.mute ? (_jsxs(Flex, { align: "center", gap: 2, children: [_jsx(PlainButton, { "aria-label": volume.muted ? 'Unmute' : 'Mute', "aria-pressed": volume.muted, title: volume.muted ? 'Unmute' : 'Mute', disabled: !ready || caps?.mute === false, onClick: volume.toggleMute, center: true, color: volume.muted ? 'accent' : 'textMuted', css: { flex: 'none' }, children: _jsx(Icon, { name: volume.muted ? 'lu:volume-x' : 'lu:volume-2', size: 16 }) }), _jsx(ValueBar, { label: "Volume level", value: volume.shown, min: 0, max: 100, keyStep: 5, height: 10, onDrag: volume.drag, onCommit: volume.commit }), _jsxs(Typography, { as: "span", variant: "secondary", color: "textMuted", minWidth: 32, align: "right", css: { flex: 'none', fontVariantNumeric: 'tabular-nums' }, children: [volume.shown, "%"] })] })) : null] }));
|
|
34
39
|
}
|
package/dist/entities/top-bar.js
CHANGED
|
@@ -7,6 +7,7 @@ import { fallbackName } from '../entity-handle.js';
|
|
|
7
7
|
import { useConnectionStatus, useEntity, useEntityHandle, useIntegrationStatuses, } from '../hooks.js';
|
|
8
8
|
import { Icon } from '../icon.js';
|
|
9
9
|
import { statusLabels } from '../status.js';
|
|
10
|
+
import { TOP_ROW } from '../theme/grid.js';
|
|
10
11
|
import { DrawerTrigger } from '../layout/use-drawer.js';
|
|
11
12
|
import { useWeatherForecast } from '../use-weather-forecast.js';
|
|
12
13
|
import { SensorReadout } from './sensor-readout.js';
|
|
@@ -54,7 +55,10 @@ function useDismiss(open, close, ref) {
|
|
|
54
55
|
* shortcuts, and live status (weather, presence, a clock). Composes freely — every prop but
|
|
55
56
|
* `title` is optional. */
|
|
56
57
|
export function TopBar({ title, dashboards, scenes, weather, extra, presence, people, presenceColors, showClock = true, showDate = true, clockFormat = 'auto', showStatus = true, }) {
|
|
57
|
-
return (_jsxs(Flex, { align: "center", justify: "space-between", gap: 4,
|
|
58
|
+
return (_jsxs(Flex, { align: "center", justify: "space-between", gap: 4,
|
|
59
|
+
// A whole number of grid modules whatever is in it, so what is under it starts on the grid, and
|
|
60
|
+
// no padding of its own, so its content is as far from the page's edge as the rest.
|
|
61
|
+
css: { minHeight: TOP_ROW }, children: [_jsxs(Flex, { align: "center", gap: 3, children: [dashboards && dashboards.length > 1 ? (_jsx(DashboardSwitcher, { title: title, dashboards: dashboards })) : (_jsx(Typography, { as: "h1", variant: "title", children: title })), scenes?.length ? (_jsx(Flex, { align: "center", gap: 2.5, children: scenes.map((scene) => {
|
|
58
62
|
const { entity, icon, color } = typeof scene === 'string' ? { entity: scene } : scene;
|
|
59
63
|
return (_jsx(SceneDot, { entity: entity, ...(color ? { color } : {}), ...(icon ? { icon } : {}) }, entity));
|
|
60
64
|
}) })) : null] }), _jsxs(Flex, { align: "center", gap: 3, children: [showDate ? _jsx(DateChip, {}) : null, weather ? _jsx(WeatherChip, { entity: weather }) : null, extra, presence ? (_jsx(StatusChip, { children: _jsx(SensorReadout, { entity: presence, drawer: false }) })) : null, people?.length ? (_jsx(PresenceStack, { entities: people, ...(presenceColors ? { colors: presenceColors } : {}) })) : null, showClock ? _jsx(Clock, { format: clockFormat }) : null, showStatus ? _jsx(SystemStatus, {}) : null] })] }));
|
|
@@ -258,5 +262,5 @@ export function DateChip() {
|
|
|
258
262
|
export function Clock({ format = 'auto' }) {
|
|
259
263
|
const now = useNow(30_000);
|
|
260
264
|
const { time, dayPeriod } = formatClock(now, format);
|
|
261
|
-
return (_jsxs(Flex, { align: "flex-start", gap: 1, children: [_jsx(Typography, { as: "span", variant: "clock", children: time }), dayPeriod ? (_jsx(Typography, { as: "span", variant: "label", css: { textTransform: 'uppercase' }, children: dayPeriod })) : null] }));
|
|
265
|
+
return (_jsxs(Flex, { align: "flex-start", gap: 1, children: [_jsx(Typography, { as: "span", variant: "clock", css: { lineHeight: `${TOP_ROW}px` }, children: time }), dayPeriod ? (_jsx(Typography, { as: "span", variant: "label", css: { textTransform: 'uppercase' }, children: dayPeriod })) : null] }));
|
|
262
266
|
}
|
package/dist/gallery/gallery.js
CHANGED
|
@@ -22,10 +22,12 @@ import { SensorReadout } from '../entities/sensor-readout.js';
|
|
|
22
22
|
import { Clock, DateChip, PresenceStack, SystemStatus, TopBar, WeatherChip, } from '../entities/top-bar.js';
|
|
23
23
|
import { Icon } from '../icon.js';
|
|
24
24
|
import { EnergyChart, } from '../layout/energy-chart.js';
|
|
25
|
+
import { Board, Cell } from '../layout/board.js';
|
|
25
26
|
import { Grid } from '../layout/grid.js';
|
|
26
27
|
import { HistorySection } from '../layout/history-section.js';
|
|
27
28
|
import { RoomHeader } from '../layout/room-header.js';
|
|
28
29
|
import { Tile } from '../layout/tile.js';
|
|
30
|
+
import { TopRow } from '../layout/top-row.js';
|
|
29
31
|
import { HashsomeProvider } from '../provider.js';
|
|
30
32
|
import { createGalleryIntegration } from './fixtures.js';
|
|
31
33
|
import { COMPONENT_PROPS } from './props-data.js';
|
|
@@ -362,7 +364,7 @@ export function Gallery({ density = 'comfortable' }) {
|
|
|
362
364
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)',
|
|
363
365
|
transform: mode === 'dark' ? 'translate(28px, -50%)' : 'translateY(-50%)',
|
|
364
366
|
transition: 'transform 0.2s ease',
|
|
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: [
|
|
367
|
+
}) }), _jsx(Icon, { name: "lu:sun", size: 14 }), _jsx(Icon, { name: "lu:moon", size: 14 })] })] }), _jsx(ComponentDoc, { title: "Board & Cell", components: ['Board', 'Cell'], description: "A page for one known device, laid out on a grid. Each cell says only how big it is (`cols` wide, `rows` tall); the board places them in order, in the first spot they fit. Rows are as tall as their content, so a cell of `rows={2}` ends where the second row does, and `rows='fill'` runs from where the cell is placed down to the bottom of the page, whatever rows are beside it. Every card is a whole number of grid modules tall, the gap between columns and inside a cell is three modules and between rows six, so the result stays on the grid (turn on the debug menu's Show grid to see it).", children: _jsxs(Flex, { direction: "column", gap: 5, children: [_jsxs(Board, { children: [_jsxs(Cell, { cols: 8, children: [_jsx(RoomHeader, { title: "Kitchen", icon: "lu:utensils-crossed" }), _jsxs(Grid, { columns: 2, children: [_jsx(Tile, { label: "Lamp", icon: "lu:lightbulb" }), _jsx(Tile, { label: "Ceiling", icon: "lu:lightbulb" })] })] }), _jsx(Cell, { cols: 4, rows: 2, children: _jsx(MediaPlayerColumn, { entity: "ha:media_player.off" }) }), _jsxs(Cell, { cols: 8, children: [_jsx(RoomHeader, { title: "Stairs", icon: "lu:footprints" }), _jsxs(Grid, { columns: 2, children: [_jsx(Tile, { label: "Lamp", icon: "lu:lightbulb" }), _jsx(Tile, { label: "Spotlights", icon: "lu:lightbulb" })] })] })] }), _jsx(Flex, { direction: "column", height: 460, css: { outline: '1px dashed currentColor' }, children: _jsxs(Board, { children: [_jsx(Cell, { children: _jsxs(TopRow, { children: [_jsx(DateChip, {}), _jsx(Clock, { format: "24h" })] }) }), _jsxs(Cell, { cols: 8, children: [_jsx(RoomHeader, { title: "Kitchen", icon: "lu:utensils-crossed" }), _jsxs(Grid, { columns: 2, children: [_jsx(Tile, { label: "Lamp", icon: "lu:lightbulb" }), _jsx(Tile, { label: "Ceiling", icon: "lu:lightbulb" })] })] }), _jsx(Cell, { cols: 4, rows: "fill", children: _jsx(MediaPlayerColumn, { entity: "ha:media_player.off" }) })] }) })] }) }), _jsx(ComponentDoc, { title: "Top Row", components: ['TopRow'], description: "The row along the top of a page you build yourself instead of with `TopBar`: the date, the weather, the clock and the status in a line at the right, in the height the grid gives the top row (40px), with no padding of its own.", children: _jsxs(TopRow, { children: [_jsx(DateChip, {}), _jsx(Clock, { format: "24h" })] }) }), _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. Where the queue has no room beside the player (a narrow column, the side drawer), Library and Queue are tabs above the list; `tabs={false}` leaves only the library, `defaultTab` and `tab` / `onTabChange` choose which is open. 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; with `search='icon'` (the second demo) it is an icon at the end of the row of tabs, after a thin line, and pressing it turns that row into the field, so it takes no room of its own. The `theater` layout (third 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 (fourth 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(Flex, { direction: "column", maxHeight: 420, overflow: "auto", children: _jsx(MediaBrowser, { entity: "ha:media_player.living_room", search: "icon" }) }), _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: [
|
|
366
368
|
{ id: 'living-room', title: 'Living Room', icon: 'lu:sofa' },
|
|
367
369
|
{ id: 'bedroom', title: 'Bedroom', icon: 'lu:bed' },
|
|
368
370
|
{ id: 'bathroom', title: 'Bathroom', icon: 'tb:bath' },
|
|
@@ -18,8 +18,59 @@ export const COMPONENT_PROPS = {
|
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
20
|
},
|
|
21
|
+
Board: {
|
|
22
|
+
doc: 'A page laid out on a grid, for a dashboard made for one device. The page is `columns` equal columns (a gap between them) and rows as tall as what is in them, with twice that gap between rows. Nothing says where a cell goes: each `Cell` only says how big it is, and the board puts it in the first place it fits, in the order written. Every card is a whole number of grid modules tall and every gap is 3, so whatever the board makes stays on the grid. It takes the height that is left of the page. Opt in: a page of plain flex columns keeps working the same.',
|
|
23
|
+
props: [
|
|
24
|
+
{
|
|
25
|
+
doc: 'How many equal columns the page is divided into. Default 12.',
|
|
26
|
+
name: 'columns',
|
|
27
|
+
optional: true,
|
|
28
|
+
type: 'number',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
doc: '`Cell`s, each saying how big it is; the board places them.',
|
|
32
|
+
name: 'children',
|
|
33
|
+
optional: false,
|
|
34
|
+
type: 'ReactNode',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
Cell: {
|
|
39
|
+
doc: "Something on a `Board`, and how big it is: `cols` wide and `rows` tall. Where it goes is the board's business. A room is one cell (its header and its tiles), a player beside three of them is one that covers three rows, or fills.",
|
|
40
|
+
props: [
|
|
41
|
+
{
|
|
42
|
+
doc: 'How many columns wide. Default: all of them.',
|
|
43
|
+
name: 'cols',
|
|
44
|
+
optional: true,
|
|
45
|
+
type: 'number',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
doc: "How many rows of the board it covers, counting rows as tall as their content (a room is one): `3` ends where the third row does, and the cell is as tall as those rows, whatever is in it (a card in it that is too tall squeezes). `'fill'` starts where the cell is placed and goes down to the bottom of the page (or of the content, if that is longer), whatever rows are beside it; they keep their place. Default 1.",
|
|
49
|
+
name: 'rows',
|
|
50
|
+
optional: true,
|
|
51
|
+
type: "number | 'fill'",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
doc: "What is in it, one under the other with the gap between. In a cell of more than one row, or `'fill'`, the last card stretches to the cell's height.",
|
|
55
|
+
name: 'children',
|
|
56
|
+
optional: false,
|
|
57
|
+
type: 'ReactNode',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
TopRow: {
|
|
62
|
+
doc: 'The row along the top of a page you build yourself instead of with `TopBar`: its pieces (`DateChip`, `WeatherChip`, `Clock`, `SystemStatus`) in a line at the right, in the height the grid gives the top row, with no padding of its own.',
|
|
63
|
+
props: [
|
|
64
|
+
{
|
|
65
|
+
doc: 'What goes in the row: the date, the weather, the clock.',
|
|
66
|
+
name: 'children',
|
|
67
|
+
optional: false,
|
|
68
|
+
type: 'ReactNode',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
21
72
|
Page: {
|
|
22
|
-
doc: "The page every dashboard renders into: padded by the density's spacing, a column with gaps, exactly viewport height, and scrolling inside itself (the document never scrolls — the entity drawer is `position: fixed` and relies on that). It pads further for a `NavRail` or `NavDock` the dashboard includes. Render it once, in the app's root layout.",
|
|
73
|
+
doc: "The page every dashboard renders into: padded by the density's spacing, a column with gaps, exactly viewport height, and scrolling inside itself (the document never scrolls — the entity drawer is `position: fixed` and relies on that). It pads further for a `NavRail` or `NavDock` the dashboard includes. Render it once, in the app's root layout. The debug menu (`HASHSOME_DEBUG=1`) can draw the module grid over the page, spanning all of it (its padding is the grid's first three modules), with every card boxed green or red by whether it sits on the grid: for laying a dashboard out for one device. The height is rarely a whole number of grid modules; what is left over (under one module) is shared between the top and the bottom padding, so the content sits centered and the grid stays whole.",
|
|
23
74
|
props: [
|
|
24
75
|
{
|
|
25
76
|
doc: 'The dashboard.',
|
|
@@ -496,7 +547,7 @@ export const COMPONENT_PROPS = {
|
|
|
496
547
|
],
|
|
497
548
|
},
|
|
498
549
|
MediaPlayerColumn: {
|
|
499
|
-
doc: 'The player as an upright card for a narrow column beside a dashboard (a quarter to a third of the width): the artwork in a ring that shows (and seeks) the position, title, transport and a volume bar that is always visible. It is only as tall as it needs to be. Holding the card or pressing the artwork opens the drawer with the player and its library; the browse button opens that drawer expanded.',
|
|
550
|
+
doc: 'The player as an upright card for a narrow column beside a dashboard (a quarter to a third of the width): the artwork in a ring that shows (and seeks) the position, title, transport and a volume bar that is always visible. It is only as tall as it needs to be. Holding the card or pressing the artwork opens the drawer with the player and its library; the browse button opens that drawer expanded. With `overlays` there is no drawer: the library and the queue open full size, each on its own.',
|
|
500
551
|
props: [
|
|
501
552
|
{
|
|
502
553
|
doc: 'A media player, as a ref like `ma:kitchen` or `ha:media_player.kitchen`, or a handle (a custom source).',
|
|
@@ -516,6 +567,12 @@ export const COMPONENT_PROPS = {
|
|
|
516
567
|
optional: true,
|
|
517
568
|
type: 'ReactNode | false',
|
|
518
569
|
},
|
|
570
|
+
{
|
|
571
|
+
doc: "The library and the queue each open as a full-size overlay of their own, from a library button and a queue button beside the transport, instead of one half-size drawer with the whole player in it: for a small display, where the drawer's half state is no use. 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. Shuffle moves up beside the title. Default `false`.",
|
|
572
|
+
name: 'overlays',
|
|
573
|
+
optional: true,
|
|
574
|
+
type: 'boolean',
|
|
575
|
+
},
|
|
519
576
|
],
|
|
520
577
|
},
|
|
521
578
|
MediaPlayerFull: {
|
|
@@ -551,6 +608,30 @@ export const COMPONENT_PROPS = {
|
|
|
551
608
|
optional: true,
|
|
552
609
|
type: 'boolean',
|
|
553
610
|
},
|
|
611
|
+
{
|
|
612
|
+
doc: 'In the narrow layout, where the queue has no room beside the player, a Library and a Queue tab above the list switch between them (shown when there is a `browser`, a `queue` and a player with a queue). `false` leaves the library alone, as before. In the wide layout the queue is beside the player and there are no tabs. Default `true`.',
|
|
613
|
+
name: 'tabs',
|
|
614
|
+
optional: true,
|
|
615
|
+
type: 'boolean',
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
doc: "The tab that is open first, when the tabs are on. Default `'library'`.",
|
|
619
|
+
name: 'defaultTab',
|
|
620
|
+
optional: true,
|
|
621
|
+
type: 'ListTab',
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
doc: 'The open tab, to keep it yourself (to remember it between visits, or open the queue from elsewhere); the tabs ask for a change through `onTabChange`. Without it, the component keeps it, until it is gone.',
|
|
625
|
+
name: 'tab',
|
|
626
|
+
optional: true,
|
|
627
|
+
type: 'ListTab',
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
doc: 'Called with the tab someone chose.',
|
|
631
|
+
name: 'onTabChange',
|
|
632
|
+
optional: true,
|
|
633
|
+
type: '(tab: ListTab) => void',
|
|
634
|
+
},
|
|
554
635
|
],
|
|
555
636
|
},
|
|
556
637
|
MediaBrowser: {
|
|
@@ -574,6 +655,12 @@ export const COMPONENT_PROPS = {
|
|
|
574
655
|
optional: true,
|
|
575
656
|
type: "'list' | 'theater' | 'auto'",
|
|
576
657
|
},
|
|
658
|
+
{
|
|
659
|
+
doc: 'How the search is offered, for a library that can be searched. `bar` is a field above the list. `icon` is a search icon at the right end of the row of tabs (or of the heading), after a thin line, which becomes the field when pressed, in that same row, so search takes no room of its own; its close button puts the tabs back. Default `bar`.',
|
|
660
|
+
name: 'search',
|
|
661
|
+
optional: true,
|
|
662
|
+
type: "'bar' | 'icon'",
|
|
663
|
+
},
|
|
577
664
|
],
|
|
578
665
|
},
|
|
579
666
|
MediaQueue: {
|
|
@@ -839,7 +926,7 @@ export const COMPONENT_PROPS = {
|
|
|
839
926
|
type: 'Density',
|
|
840
927
|
},
|
|
841
928
|
{
|
|
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
|
|
929
|
+
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, which wins over this prop. Nothing is kept: it holds while the app is open, moving between its pages, and a reload without the parameter goes back to this prop.",
|
|
843
930
|
name: 'motion',
|
|
844
931
|
optional: true,
|
|
845
932
|
type: 'MotionPreference',
|
|
@@ -850,6 +937,18 @@ export const COMPONENT_PROPS = {
|
|
|
850
937
|
optional: true,
|
|
851
938
|
type: 'ThemeOverrides',
|
|
852
939
|
},
|
|
940
|
+
{
|
|
941
|
+
doc: 'Shows the debug menu: a floating button at the bottom left whose popover shows the module grid, makes the page fullscreen and changes the theme, on the device, kept there. Default: on when `HASHSOME_DEBUG=1` (or `true`) was in the environment of `hashsome dev` or `build`.',
|
|
942
|
+
name: 'debug',
|
|
943
|
+
optional: true,
|
|
944
|
+
type: 'boolean',
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
doc: "The daylight sensor (`on` while the sun is up) the debug menu's Sun theme follows, when `theme` is not a sun schedule already (`theme={{ sun: … }}` names one). Without either, the menu has no Sun choice: Hashsome does not know which entity is the sun.",
|
|
948
|
+
name: 'sun',
|
|
949
|
+
optional: true,
|
|
950
|
+
type: 'EntityRef',
|
|
951
|
+
},
|
|
853
952
|
{
|
|
854
953
|
doc: 'The app.',
|
|
855
954
|
name: 'children',
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export * from './status.ts';
|
|
|
8
8
|
export type { IconName } from './icon-data.ts';
|
|
9
9
|
export * from './icon.tsx';
|
|
10
10
|
export * from './layout/grid.tsx';
|
|
11
|
+
export * from './layout/board.tsx';
|
|
12
|
+
export * from './layout/top-row.tsx';
|
|
11
13
|
export * from './layout/page.tsx';
|
|
12
14
|
export * from './layout/animated-outlet.tsx';
|
|
13
15
|
export * from './layout/room-header.tsx';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export * from './hooks.js';
|
|
|
4
4
|
export * from './status.js';
|
|
5
5
|
export * from './icon.js';
|
|
6
6
|
export * from './layout/grid.js';
|
|
7
|
+
export * from './layout/board.js';
|
|
8
|
+
export * from './layout/top-row.js';
|
|
7
9
|
export * from './layout/page.js';
|
|
8
10
|
export * from './layout/animated-outlet.js';
|
|
9
11
|
export * from './layout/room-header.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface BoardProps {
|
|
3
|
+
/** How many equal columns the page is divided into. Default 12. */
|
|
4
|
+
columns?: number;
|
|
5
|
+
/** `Cell`s, each saying how big it is; the board places them. */
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/** A page laid out on a grid, for a dashboard made for one device. The page is `columns` equal columns
|
|
9
|
+
* (a gap between them) and rows as tall as what is in them, with twice that gap between rows. Nothing
|
|
10
|
+
* says where a cell goes: each `Cell` only says how big it is, and the board puts it in the first
|
|
11
|
+
* place it fits, in the order written. Every card is a whole number of grid modules tall and every gap
|
|
12
|
+
* is 3, so whatever the board makes stays on the grid. It takes the height that is left of the page.
|
|
13
|
+
* Opt in: a page of plain flex columns keeps working the same. */
|
|
14
|
+
export declare function Board({ columns, children }: BoardProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
15
|
+
export interface CellProps {
|
|
16
|
+
/** How many columns wide. Default: all of them. */
|
|
17
|
+
cols?: number;
|
|
18
|
+
/** How many rows of the board it covers, counting rows as tall as their content (a room is one): `3` ends where the third row does, and the cell is as tall as those rows, whatever is in it (a card in it that is too tall squeezes). `'fill'` starts where the cell is placed and goes down to the bottom of the page (or of the content, if that is longer), whatever rows are beside it; they keep their place. Default 1. */
|
|
19
|
+
rows?: number | 'fill';
|
|
20
|
+
/** What is in it, one under the other with the gap between. In a cell of more than one row, or `'fill'`, the last card stretches to the cell's height. */
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
/** Something on a `Board`, and how big it is: `cols` wide and `rows` tall. Where it goes is the board's
|
|
24
|
+
* business. A room is one cell (its header and its tiles), a player beside three of them is one that
|
|
25
|
+
* covers three rows, or fills. */
|
|
26
|
+
export declare function Cell({ cols, rows, children }: CellProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @emotion/react */
|
|
3
|
+
import { Box } from 'e-prim';
|
|
4
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
5
|
+
/** A page laid out on a grid, for a dashboard made for one device. The page is `columns` equal columns
|
|
6
|
+
* (a gap between them) and rows as tall as what is in them, with twice that gap between rows. Nothing
|
|
7
|
+
* says where a cell goes: each `Cell` only says how big it is, and the board puts it in the first
|
|
8
|
+
* place it fits, in the order written. Every card is a whole number of grid modules tall and every gap
|
|
9
|
+
* is 3, so whatever the board makes stays on the grid. It takes the height that is left of the page.
|
|
10
|
+
* Opt in: a page of plain flex columns keeps working the same. */
|
|
11
|
+
export function Board({ columns = 12, children }) {
|
|
12
|
+
return (_jsx(Box, { "data-board": true, position: "relative", css: ({ density }) => ({
|
|
13
|
+
display: 'grid',
|
|
14
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
15
|
+
gridAutoRows: 'max-content',
|
|
16
|
+
alignContent: 'start',
|
|
17
|
+
// Twice as far between rows as between columns and inside a cell: a row is a group (a room), and
|
|
18
|
+
// groups read as groups.
|
|
19
|
+
columnGap: density.space,
|
|
20
|
+
rowGap: density.space * 2,
|
|
21
|
+
// The height of its rows, or the page's if that is more: it grows into what is left, and never
|
|
22
|
+
// shrinks below its content (the page scrolls instead).
|
|
23
|
+
flex: '1 0 auto',
|
|
24
|
+
minWidth: 0,
|
|
25
|
+
}), children: children }));
|
|
26
|
+
}
|
|
27
|
+
/** Something on a `Board`, and how big it is: `cols` wide and `rows` tall. Where it goes is the board's
|
|
28
|
+
* business. A room is one cell (its header and its tiles), a player beside three of them is one that
|
|
29
|
+
* covers three rows, or fills. */
|
|
30
|
+
export function Cell({ cols, rows = 1, children }) {
|
|
31
|
+
const ref = useRef(null);
|
|
32
|
+
const fill = rows === 'fill';
|
|
33
|
+
const [height, setHeight] = useState(0);
|
|
34
|
+
// `fill`: as tall as from where the cell starts to the bottom of the board, or to the end of the
|
|
35
|
+
// others' content if that is further. The others' content, not its own: it is what is being set.
|
|
36
|
+
useLayoutEffect(() => {
|
|
37
|
+
const cell = ref.current;
|
|
38
|
+
const board = cell?.parentElement;
|
|
39
|
+
if (!fill || !cell || !board) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const measure = () => {
|
|
43
|
+
const others = [...board.children].filter((child) => child !== cell);
|
|
44
|
+
const bottom = Math.max(board.clientHeight, ...others.map((child) => child.offsetTop + child.offsetHeight));
|
|
45
|
+
const next = Math.max(0, Math.floor(bottom - cell.offsetTop));
|
|
46
|
+
setHeight((current) => (Math.abs(current - next) < 1 ? current : next));
|
|
47
|
+
};
|
|
48
|
+
measure();
|
|
49
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const observer = new ResizeObserver(measure);
|
|
53
|
+
observer.observe(board);
|
|
54
|
+
for (const child of board.children) {
|
|
55
|
+
observer.observe(child);
|
|
56
|
+
}
|
|
57
|
+
return () => observer.disconnect();
|
|
58
|
+
});
|
|
59
|
+
const span = typeof rows === 'number' ? rows : 1;
|
|
60
|
+
const stretches = fill || span > 1;
|
|
61
|
+
const column = (gap) => ({
|
|
62
|
+
display: 'flex',
|
|
63
|
+
flexDirection: 'column',
|
|
64
|
+
gap,
|
|
65
|
+
'& > [data-grid-card]:last-child': stretches ? { flexGrow: 1 } : undefined,
|
|
66
|
+
});
|
|
67
|
+
return (_jsx(Box, { ref: ref, "data-cell": true, css: ({ density }) => ({
|
|
68
|
+
gridColumn: cols === undefined ? '1 / -1' : `span ${cols}`,
|
|
69
|
+
gridRow: span > 1 ? `span ${span}` : undefined,
|
|
70
|
+
minWidth: 0,
|
|
71
|
+
// A cell takes the height of what is in it, unless it is meant to reach across rows.
|
|
72
|
+
alignSelf: stretches ? 'stretch' : 'start',
|
|
73
|
+
// One that reaches across rows, or fills, takes the height the page and the others give it; its
|
|
74
|
+
// own content does not size them (a player taller than three rooms squeezes, it does not
|
|
75
|
+
// stretch the third, or push the page past its bottom).
|
|
76
|
+
contain: stretches ? 'size' : undefined,
|
|
77
|
+
...(fill ? { position: 'relative' } : column(density.space)),
|
|
78
|
+
}), children: fill ? (_jsx(Box, { position: "absolute", css: ({ density }) => ({
|
|
79
|
+
top: 0,
|
|
80
|
+
left: 0,
|
|
81
|
+
right: 0,
|
|
82
|
+
height: height > 0 ? height : '100%',
|
|
83
|
+
...column(density.space),
|
|
84
|
+
}), children: children })) : (children) }));
|
|
85
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
import type { EntityRef } from '@hashsome/core';
|
|
3
|
+
import type { ThemeMode } from '../provider.tsx';
|
|
4
|
+
/** A floating button at the bottom left, for working on a dashboard on the device it is for: its
|
|
5
|
+
* popover shows the module grid over the page, makes the page fullscreen, and changes the theme
|
|
6
|
+
* (light, dark, the system's, and the sun's when the project has a sun entity). What it sets is kept on the device. Only there when
|
|
7
|
+
* `HashsomeProvider` has `debug` on (`HASHSOME_DEBUG=1`), and not exported. */
|
|
8
|
+
export declare function DebugMenu({ configured, sun, }: {
|
|
9
|
+
configured?: ThemeMode;
|
|
10
|
+
sun?: EntityRef | undefined;
|
|
11
|
+
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
|
|
2
|
+
import { Flex, Typography } from 'e-prim';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { THEME_CHOICES, useDebug } from '../debug.js';
|
|
5
|
+
import { Icon } from '../icon.js';
|
|
6
|
+
import { ChipRow } from './drawer-controls.js';
|
|
7
|
+
import { PlainButton } from './plain-button.js';
|
|
8
|
+
const THEME_LABELS = {
|
|
9
|
+
light: 'Light',
|
|
10
|
+
dark: 'Dark',
|
|
11
|
+
system: 'System',
|
|
12
|
+
sun: 'Sun',
|
|
13
|
+
};
|
|
14
|
+
const ON_OFF = [
|
|
15
|
+
{ value: 'off', label: 'Off' },
|
|
16
|
+
{ value: 'on', label: 'On' },
|
|
17
|
+
];
|
|
18
|
+
/** The theme the project configured, as the menu's own choice, for showing which is in effect when
|
|
19
|
+
* the menu has not chosen. A schedule by the clock is none of them. */
|
|
20
|
+
function configuredChoice(configured) {
|
|
21
|
+
if (configured === undefined) {
|
|
22
|
+
return 'dark';
|
|
23
|
+
}
|
|
24
|
+
if (typeof configured === 'string') {
|
|
25
|
+
return configured;
|
|
26
|
+
}
|
|
27
|
+
return 'sun' in configured ? 'sun' : undefined;
|
|
28
|
+
}
|
|
29
|
+
function Row({ label, children }) {
|
|
30
|
+
return (_jsxs(Flex, { direction: "column", gap: 2, children: [_jsx(Typography, { as: "span", variant: "label", color: "textMuted", children: label }), children] }));
|
|
31
|
+
}
|
|
32
|
+
/** A floating button at the bottom left, for working on a dashboard on the device it is for: its
|
|
33
|
+
* popover shows the module grid over the page, makes the page fullscreen, and changes the theme
|
|
34
|
+
* (light, dark, the system's, and the sun's when the project has a sun entity). What it sets is kept on the device. Only there when
|
|
35
|
+
* `HashsomeProvider` has `debug` on (`HASHSOME_DEBUG=1`), and not exported. */
|
|
36
|
+
export function DebugMenu({ configured, sun, }) {
|
|
37
|
+
const debug = useDebug();
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
const root = useRef(null);
|
|
40
|
+
const [isFull, setIsFull] = useState(() => typeof document !== 'undefined' && document.fullscreenElement !== null);
|
|
41
|
+
const canFullscreen = typeof document !== 'undefined' && document.fullscreenEnabled === true;
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const onChange = () => setIsFull(document.fullscreenElement !== null);
|
|
44
|
+
document.addEventListener('fullscreenchange', onChange);
|
|
45
|
+
return () => document.removeEventListener('fullscreenchange', onChange);
|
|
46
|
+
}, []);
|
|
47
|
+
// Closes on a touch anywhere else, and on Escape.
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!open) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const away = (event) => {
|
|
53
|
+
if (root.current && !root.current.contains(event.target)) {
|
|
54
|
+
setOpen(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const escape = (event) => {
|
|
58
|
+
if (event.key === 'Escape') {
|
|
59
|
+
setOpen(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
document.addEventListener('pointerdown', away);
|
|
63
|
+
document.addEventListener('keydown', escape);
|
|
64
|
+
return () => {
|
|
65
|
+
document.removeEventListener('pointerdown', away);
|
|
66
|
+
document.removeEventListener('keydown', escape);
|
|
67
|
+
};
|
|
68
|
+
}, [open]);
|
|
69
|
+
const toggleFullscreen = (on) => {
|
|
70
|
+
// Asked for in the touch itself: a browser only allows it as the answer to one.
|
|
71
|
+
if (on) {
|
|
72
|
+
void document.documentElement
|
|
73
|
+
.requestFullscreen({ navigationUI: 'hide' })
|
|
74
|
+
.catch(() => undefined);
|
|
75
|
+
}
|
|
76
|
+
else if (document.fullscreenElement) {
|
|
77
|
+
void document.exitFullscreen().catch(() => undefined);
|
|
78
|
+
}
|
|
79
|
+
debug.setFullscreen(on);
|
|
80
|
+
};
|
|
81
|
+
const theme = debug.themeChoice ?? configuredChoice(configured);
|
|
82
|
+
return (_jsxs(Flex, { ref: root, "data-debug-menu": true,
|
|
83
|
+
// As big as the button, whatever the page's own rules say: the document shell makes every `div`
|
|
84
|
+
// that is a child of `body` at least as tall as the window (`body > div { min-height: 100% }`),
|
|
85
|
+
// which this is, and the button would sit at the top of it.
|
|
86
|
+
css: {
|
|
87
|
+
position: 'fixed',
|
|
88
|
+
left: 12,
|
|
89
|
+
bottom: 12,
|
|
90
|
+
zIndex: 30,
|
|
91
|
+
width: 40,
|
|
92
|
+
height: 40,
|
|
93
|
+
minHeight: 0,
|
|
94
|
+
}, children: [open ? (_jsxs(Flex, { role: "dialog", "aria-label": "Debug", direction: "column", gap: 4, background: "surface", radius: "row", shadow: "drawer", p: 4, width: 288, css: { position: 'absolute', left: 0, bottom: 52, boxSizing: 'border-box' }, children: [_jsx(Row, { label: "Show grid", children: _jsx(ChipRow, { options: ON_OFF, value: debug.grid ? 'on' : 'off', onChange: (value) => debug.setGrid(value === 'on') }) }), _jsx(Row, { label: "Fullscreen", children: canFullscreen ? (_jsx(ChipRow, { options: ON_OFF, value: isFull ? 'on' : 'off', onChange: (value) => toggleFullscreen(value === 'on') })) : (_jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: "Not available in this browser." })) }), _jsx(Row, { label: "Theme", children: _jsx(ChipRow
|
|
95
|
+
// The sun only if the project has one to follow.
|
|
96
|
+
, {
|
|
97
|
+
// The sun only if the project has one to follow.
|
|
98
|
+
options: THEME_CHOICES.filter((choice) => choice !== 'sun' || sun !== undefined).map((choice) => ({ value: choice, label: THEME_LABELS[choice] })), value: theme, onChange: (value) => debug.setThemeChoice(value) }) })] })) : null, _jsx(PlainButton, { "aria-label": "Debug menu", title: "Debug menu", "aria-expanded": open, onClick: () => setOpen((current) => !current), center: true, radius: "full", background: "surface", color: "textMuted", shadow: "dock", width: 40, height: 40, css: { flex: 'none' }, children: _jsx(Icon, { name: "lu:bug", size: 18 }) })] }));
|
|
99
|
+
}
|