diff --git a/README.md b/README.md index 0e56afd9..d2f1b06a 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,9 @@ menu: | Option | Default | Overridable | Description | | - | - | - | - | -| `mode` | `hidden-top` | :white_check_mark: | The menu mode to show by default. See [menu modes](#menu-modes) below.| +| `style` | `hidden` | :white_check_mark: | The menu style to show by default, one of `none`, `hidden`, `hover`, `overlay`, or `outside`. See [menu styles](#menu-styles) below.| +| `position` | `top` | :white_check_mark: | Whether to show the menu on the `left`, `right`, `top` or `bottom` side of the card.| +| `alignment` | `left` | :white_check_mark: | Whether to align the menu buttons to the `left`, `right`, `top` or `bottom` of the menu. Some selections may have no effect depending on the value of `position` (e.g. it doesn't make sense to `left` align icons on a menu with `position` to the `left`).| | `button_size` | 40 | :white_check_mark: | The size of the menu buttons in pixels. Must be >= `20`.| | `buttons` | | :white_check_mark: | Whether to show or hide built-in buttons. See below. | @@ -186,7 +188,7 @@ menu: | Option | Default | Overridable | Description | | - | - | - | - | -| `frigate` | `true` | :white_check_mark: | Whether to show the `Frigate` menu button: brings the user to the default configured view (`view.default`), or collapses/expands the menu if the `menu.mode` is `hidden-*` . | +| `frigate` | `true` | :white_check_mark: | Whether to show the `Frigate` menu button: brings the user to the default configured view (`view.default`), or collapses/expands the menu if the `menu.style` is `hidden` . | | `cameras` | `true` | :white_check_mark: | Whether to show the camera selection submenu. Will only appear if multiple cameras are configured. | | `live` | `true` | :white_check_mark: | Whether to show the `live` view menu button: brings the user to the `live` view. See [views](#views) below.| | `clips` | `true` | :white_check_mark: | Whether to show the `clips` view menu button: brings the user to the `clips` view on tap and the most-recent `clip` view on hold. See [views](#views) below. This button will never be shown if the `camera_name` for the selected camera is not auto-detected/specified (e.g. non-Frigate cameras), or if the `camera_name` is `birdseye`.| @@ -655,7 +657,7 @@ Parameters for the `custom:frigate-card-conditional` element: |`frigate_ui`|Open the Frigate UI at the configured URL.| |`fullscreen`|Toggle fullscreen.| |`camera_select`|Select a given camera. Takes a single additional `camera` parameter with the [camera ID](#camera-ids) of the camera to select. Respects the value of `view.camera_select` to choose the appropriate view on the new camera.| -|`menu_toggle` | Show/hide the menu (for `hidden-*` mode menus). | +|`menu_toggle` | Show/hide the menu (for the `hidden` mode style). | @@ -747,17 +749,16 @@ next/previous controls, thumbnails, etc), but in some cases this is not possible (e.g. embedded WebRTC card controls) -- in these cases duplicate actions may occur with certain configurations (e.g. `tap`). -## Menu Modes +## Menu Styles -This card supports several menu configurations. +This card supports several menu styles. | Key | Description | Screenshot | | ------------- | --------------------------------------------- | - | -|`hidden-{top,bottom,right,left}` [default: `hidden-top`]| Hide the menu by default, expandable upon clicking the Frigate button. | Menu hidden | -|`overlay-{top,bottom,right,left}`| Overlay the menu over the card contents. The Frigate button shows the default view. | Menu overlaid | -|`hover-{top,bottom,right,left}`| Overlay the menu over the card contents when the mouse is over the card / touch on the card, otherwise it is not shown. The Frigate button shows the default view. | Menu overlaid | -|`above`| Render the menu above the card. The Frigate button shows the default view. | Menu above | -|`below`| Render the menu below the card. The Frigate button shows the default view. | Menu below | +|`hidden`| Hide the menu by default, expandable upon clicking the Frigate button. | Menu hidden | +|`overlay`| Overlay the menu over the card contents. The Frigate button shows the default view. | Menu overlaid | +|`hover`| Overlay the menu over the card contents when the mouse is over the card / touch on the card, otherwise it is not shown. The Frigate button shows the default view. | Menu overlaid | +|`outside`| Render the menu outside the card (i.e. above it if `position` is `top`, or below it if `position` is `bottom`). The Frigate button shows the default view. | Menu above | |`none`| No menu is shown. | No Menu | @@ -1189,7 +1190,7 @@ overrides: fullscreen: true overrides: menu: - mode: none + style: none ``` diff --git a/src/card.ts b/src/card.ts index 1b57c24d..56e0dbc1 100644 --- a/src/card.ts +++ b/src/card.ts @@ -1203,6 +1203,9 @@ export class FrigateCard extends LitElement { }; const actions = this._getMergedActions(); + const renderMenuAbove = + this._getConfig().menu.style === 'outside' && + this._getConfig().menu.position === 'top'; return html` this.requestUpdate()} > - ${this._getConfig().menu.mode == 'above' ? this._renderMenu() : ''} + ${renderMenuAbove ? this._renderMenu() : ''}
${this._cameras === undefined @@ -1261,7 +1264,7 @@ export class FrigateCard extends LitElement { }
- ${this._getConfig().menu.mode != 'above' ? this._renderMenu() : ''} + ${!renderMenuAbove ? this._renderMenu() : ''}
`; } diff --git a/src/components/menu.ts b/src/components/menu.ts index 37964c27..98b4a8d5 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -64,8 +64,11 @@ export class FrigateCardMenu extends LitElement { `${menuConfig.button_size}px`, ); } - // Store the menu mode as an attribute (used for CSS attribute selectors). - this.setAttribute('data-mode', menuConfig.mode); + // Store the menu style, position and alignment as attributes (used for + // styling). + this.setAttribute('data-style', menuConfig.style); + this.setAttribute('data-position', menuConfig.position); + this.setAttribute('data-alignment', menuConfig.alignment); } @state() protected _menuConfig?: MenuConfig; @@ -82,7 +85,7 @@ export class FrigateCardMenu extends LitElement { * @returns `true` if the menu is hiding, `false` otherwise. */ static isHidingMenu(menuConfig: MenuConfig | undefined): boolean { - return menuConfig?.mode.startsWith('hidden-') ?? false; + return menuConfig?.style === 'hidden' ?? false; } /** @@ -265,15 +268,14 @@ export class FrigateCardMenu extends LitElement { if (!this._menuConfig) { return; } - const mode = this._menuConfig.mode; - - if (mode == 'none') { + const style = this._menuConfig.style; + if (style === 'none') { return; } // If the hidden menu isn't expanded, only show the Frigate button. const buttons = - !mode.startsWith('hidden-') || this.expanded + style !== 'hidden' || this.expanded ? this.buttons : this.buttons.filter((button) => button.icon === FRIGATE_BUTTON_MENU_ICON); return html` ${buttons.map((button) => this._renderButton(button))} `; diff --git a/src/config-mgmt.ts b/src/config-mgmt.ts index fd8c69d1..e476d7c0 100644 --- a/src/config-mgmt.ts +++ b/src/config-mgmt.ts @@ -19,7 +19,8 @@ import { CONF_LIVE_WEBRTC_CARD, CONF_MENU, CONF_MENU_BUTTON_SIZE, - CONF_MENU_MODE, + CONF_MENU_POSITION, + CONF_MENU_STYLE, CONF_OVERRIDES, CONF_VIEW_DEFAULT, CONF_VIEW_TIMEOUT_SECONDS, @@ -203,7 +204,7 @@ const toPixelsOrDelete = function (value: unknown): number | null | undefined { // eslint-disable-next-line @typescript-eslint/no-unused-vars const deleteProperty = function (_value: unknown): number | null | undefined { return null; -} +}; /** * Move a property from one location to another. @@ -218,6 +219,7 @@ export const moveConfigValue = ( oldPath: string, newPath: string, transform?: (valueIn: unknown) => unknown, + keepOriginal?: boolean, ): boolean => { const inValue = getConfigValue(obj, oldPath); if (inValue === undefined) { @@ -228,11 +230,16 @@ export const moveConfigValue = ( return false; } if (outValue === null) { - deleteConfigValue(obj, oldPath); - return true; + if (!keepOriginal) { + deleteConfigValue(obj, oldPath); + return true; + } + return false; } if (outValue !== undefined) { - deleteConfigValue(obj, oldPath); + if (!keepOriginal) { + deleteConfigValue(obj, oldPath); + } setConfigValue(obj, newPath, outValue); return true; } @@ -260,9 +267,10 @@ const upgradeMoveTo = function ( oldPath: string, newPath: string, transform?: (valueIn: unknown) => unknown, + keepOriginal?: boolean, ): (obj: RawFrigateCardConfig) => boolean { return function (obj: RawFrigateCardConfig): boolean { - return moveConfigValue(obj, oldPath, newPath, transform); + return moveConfigValue(obj, oldPath, newPath, transform, keepOriginal); }; }; @@ -278,13 +286,14 @@ const upgradeMoveToWithOverrides = function ( oldPath: string, newPath: string, transform?: (valueIn: unknown) => unknown, + keepOriginal?: boolean, ): (obj: RawFrigateCardConfig) => boolean { return function (obj: RawFrigateCardConfig): boolean { - let modified = upgradeMoveTo(oldPath, newPath, transform)(obj); + let modified = upgradeMoveTo(oldPath, newPath, transform, keepOriginal)(obj); modified = upgradeArrayValue( CONF_OVERRIDES, - upgradeMoveTo(oldPath, newPath, transform), + upgradeMoveTo(oldPath, newPath, transform, keepOriginal), (obj) => obj.overrides as RawFrigateCardConfig | undefined, )(obj) || modified; return modified; @@ -334,8 +343,7 @@ const upgradeArrayValue = function ( /** * Upgrade from a singular camera model to multiple. - * @param key A string key. - * @returns A safe key. + * @returns An upgrade function. */ const upgradeToMultipleCameras = (): ((obj: RawFrigateCardConfig) => boolean) => { return function (obj: RawFrigateCardConfig): boolean { @@ -366,6 +374,74 @@ const upgradeToMultipleCameras = (): ((obj: RawFrigateCardConfig) => boolean) => }; }; +/** + * Upgrade from a menu-mode to a style & position. + * @returns An upgrade function. + */ +const upgradeMenuModeToStyleAndPosition = (): (( + obj: RawFrigateCardConfig, +) => boolean) => { + return function (obj: RawFrigateCardConfig): boolean { + let modified = false; + + // Change the 'start' of the mode into a style. + modified = + upgradeMoveToWithOverrides( + 'menu.mode', + CONF_MENU_STYLE, + (mode: unknown): string | undefined => { + if (typeof mode === 'string') { + const result = mode.match(/^(hover|hidden|overlay|above|below|none)/); + if (result) { + switch (result[1]) { + case 'hover': + case 'hidden': + case 'overlay': + case 'none': + return result[1]; + case 'above': + case 'below': + return 'outside'; + } + } + } + return undefined; + }, + true, + )(obj) || modified; + + // Change the 'end' of the mode into a position. + modified = + upgradeMoveToWithOverrides( + 'menu.mode', + CONF_MENU_POSITION, + (mode: unknown): string | undefined => { + if (typeof mode === 'string') { + const result = mode.match(/(above|below|left|right|top|bottom)$/); + if (result) { + switch (result[1]) { + case 'left': + case 'right': + case 'top': + case 'bottom': + return result[1]; + case 'above': + return 'top'; + case 'below': + return 'bottom'; + } + } + } + return undefined; + }, + true, + )(obj) || modified; + + // Delete the old `menu.mode` . + return upgradeWithOverrides('menu.mode', deleteProperty)(obj) || modified; + }; +}; + /** * Upgrade from a condition on the menu (to allow rendering) to a menu mode * override instead. @@ -415,7 +491,7 @@ const UPGRADES = [ upgradeMoveTo('autoplay_clip', 'event_viewer.autoplay_clip'), upgradeMoveTo('controls.nextprev', CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE), upgradeMoveTo('controls.nextprev_size', CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE), - upgradeMoveTo('menu_mode', CONF_MENU_MODE), + upgradeMoveTo('menu_mode', 'menu.mode'), upgradeMoveTo('menu_buttons', 'menu.buttons'), upgradeMoveTo('menu_button_size', CONF_MENU_BUTTON_SIZE), upgradeMoveTo('image', 'image.src', isNotObject), @@ -461,8 +537,6 @@ const UPGRADES = [ CONF_MENU_BUTTON_SIZE, createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN), ), - upgradeWithOverrides( - 'event_gallery.min_columns', - deleteProperty - ), + upgradeWithOverrides('event_gallery.min_columns', deleteProperty), + upgradeMenuModeToStyleAndPosition(), ]; diff --git a/src/const.ts b/src/const.ts index 6e18ebfe..11ea8e81 100644 --- a/src/const.ts +++ b/src/const.ts @@ -108,6 +108,9 @@ export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_CONTROLS = `${CONF_TIMELINE}.controls.thumbnails.show_controls` as const; export const CONF_MENU = 'menu' as const; +export const CONF_MENU_ALIGNMENT = `${CONF_MENU}.alignment` as const; +export const CONF_MENU_POSITION = `${CONF_MENU}.position` as const; +export const CONF_MENU_STYLE = `${CONF_MENU}.style` as const; export const CONF_MENU_BUTTONS_FRIGATE = `${CONF_MENU}.buttons.frigate` as const; export const CONF_MENU_BUTTONS_FRIGATE_UI = `${CONF_MENU}.buttons.frigate_ui` as const; export const CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN = @@ -119,7 +122,6 @@ export const CONF_MENU_BUTTONS_CLIPS = `${CONF_MENU}.buttons.clips` as const; export const CONF_MENU_BUTTONS_SNAPSHOTS = `${CONF_MENU}.buttons.snapshots` as const; export const CONF_MENU_BUTTONS_IMAGE = `${CONF_MENU}.buttons.image` as const; export const CONF_MENU_BUTTON_SIZE = `${CONF_MENU}.button_size` as const; -export const CONF_MENU_MODE = `${CONF_MENU}.mode` as const; export const CONF_DIMENSIONS = 'dimensions' as const; export const CONF_DIMENSIONS_ASPECT_RATIO = `${CONF_DIMENSIONS}.aspect_ratio` as const; diff --git a/src/editor.ts b/src/editor.ts index 9e66ae70..6fd12978 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -64,6 +64,7 @@ import { CONF_LIVE_LAZY_UNLOAD, CONF_LIVE_PRELOAD, CONF_LIVE_TRANSITION_EFFECT, + CONF_MENU_ALIGNMENT, CONF_MENU_BUTTONS_CLIPS, CONF_MENU_BUTTONS_FRIGATE, CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD, @@ -73,7 +74,8 @@ import { CONF_MENU_BUTTONS_LIVE, CONF_MENU_BUTTONS_SNAPSHOTS, CONF_MENU_BUTTON_SIZE, - CONF_MENU_MODE, + CONF_MENU_POSITION, + CONF_MENU_STYLE, CONF_TIMELINE_CLUSTERING_THRESHOLD, CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_CONTROLS, @@ -89,11 +91,7 @@ import { CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_SECONDS, } from './const.js'; -import { - arrayMove, - getCameraID, - getCameraTitle, -} from './common.js'; +import { arrayMove, getCameraID, getCameraTitle } from './common.js'; import { copyConfig, deleteConfigValue, @@ -219,23 +217,29 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor { value: 'current', label: localize('config.view.views.current') }, ]; - protected _menuModes: EditorSelectOption[] = [ + protected _menuStyles: EditorSelectOption[] = [ { value: '', label: '' }, - { value: 'none', label: localize('config.menu.modes.none') }, - { value: 'hidden-top', label: localize('config.menu.modes.hidden-top') }, - { value: 'hidden-left', label: localize('config.menu.modes.hidden-left') }, - { value: 'hidden-bottom', label: localize('config.menu.modes.hidden-bottom') }, - { value: 'hidden-right', label: localize('config.menu.modes.hidden-right') }, - { value: 'overlay-top', label: localize('config.menu.modes.overlay-top') }, - { value: 'overlay-left', label: localize('config.menu.modes.overlay-left') }, - { value: 'overlay-bottom', label: localize('config.menu.modes.overlay-bottom') }, - { value: 'overlay-right', label: localize('config.menu.modes.overlay-right') }, - { value: 'hover-top', label: localize('config.menu.modes.hover-top') }, - { value: 'hover-left', label: localize('config.menu.modes.hover-left') }, - { value: 'hover-bottom', label: localize('config.menu.modes.hover-bottom') }, - { value: 'hover-right', label: localize('config.menu.modes.hover-right') }, - { value: 'above', label: localize('config.menu.modes.above') }, - { value: 'below', label: localize('config.menu.modes.below') }, + { value: 'none', label: localize('config.menu.styles.none') }, + { value: 'hidden', label: localize('config.menu.styles.hidden') }, + { value: 'overlay', label: localize('config.menu.styles.overlay') }, + { value: 'hover', label: localize('config.menu.styles.hover') }, + { value: 'outside', label: localize('config.menu.styles.outside') }, + ]; + + protected _menuPositions: EditorSelectOption[] = [ + { value: '', label: '' }, + { value: 'left', label: localize('config.menu.positions.left') }, + { value: 'right', label: localize('config.menu.positions.right') }, + { value: 'top', label: localize('config.menu.positions.top') }, + { value: 'bottom', label: localize('config.menu.positions.bottom') }, + ]; + + protected _menuAlignments: EditorSelectOption[] = [ + { value: '', label: '' }, + { value: 'left', label: localize('config.menu.alignments.left') }, + { value: 'right', label: localize('config.menu.alignments.right') }, + { value: 'top', label: localize('config.menu.alignments.top') }, + { value: 'bottom', label: localize('config.menu.alignments.bottom') }, ]; protected _eventViewerNextPreviousControlStyles: EditorSelectOption[] = [ @@ -878,7 +882,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${options.menu.show ? html`
- ${this._renderOptionSelector(CONF_MENU_MODE, this._menuModes)} + ${this._renderOptionSelector(CONF_MENU_STYLE, this._menuStyles)} + ${this._renderOptionSelector(CONF_MENU_POSITION, this._menuPositions)} + ${this._renderOptionSelector(CONF_MENU_ALIGNMENT, this._menuAlignments)} ${this._renderNumberInput(CONF_MENU_BUTTON_SIZE, BUTTON_SIZE_MIN)} ${this._renderSwitch( CONF_MENU_BUTTONS_FRIGATE, diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 662f6694..71cd6167 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -164,23 +164,27 @@ "download": "Download event media", "cameras": "Select camera" }, - "mode": "Menu mode", - "modes": { + "style": "Menu style", + "styles": { "none": "No menu", - "hidden-top": "Hidden top", - "hidden-left": "Hidden left", - "hidden-bottom": "Hidden bottom", - "hidden-right": "Hidden right", - "overlay-top": "Overlay top", - "overlay-left": "Overlay left", - "overlay-bottom": "Overlay bottom", - "overlay-right": "Overlay right", - "hover-top": "Hover top", - "hover-left": "Hover left", - "hover-bottom": "Hover bottom", - "hover-right": "Hover right", - "above": "Above", - "below": "Below" + "hidden": "Hidden menu", + "overlay": "Overlay menu", + "hover": "Hover menu", + "outside": "Outside menu" + }, + "position": "Menu position", + "positions": { + "left": "Positioned on the left", + "right": "Positioned on the right", + "top": "Positioned on the top", + "bottom": "Positioned on the bottom" + }, + "alignment": "Menu alignment", + "alignments": { + "left": "Aligned to the left", + "right": "Aligned to the right", + "top": "Aligned to the top", + "bottom": "Aligned to the bottom" }, "button_size": "Menu button size in pixels" }, diff --git a/src/scss/card.scss b/src/scss/card.scss index f74a82a0..aa293986 100644 --- a/src/scss/card.scss +++ b/src/scss/card.scss @@ -29,11 +29,14 @@ height: inherit; margin: auto; overflow: auto; - -ms-overflow-style: none; /* Hide scrollbar: IE and Edge */ - scrollbar-width: none; /* Hide scrollbar: Firefox */ display: flex; align-items: center; justify-content: center; + + // Hide scrollbar: Firefox + scrollbar-width: none; + // Hide scrollbar: IE and Edge + -ms-overflow-style: none; } /* Hide scrollbar for Chrome, Safari and Opera */ @@ -51,16 +54,17 @@ } /* The 'hover' menu mode is styling applied outside of the menu itself */ -frigate-card-menu[data-mode^=hover-] { +frigate-card-menu[data-style='hover'] { z-index: 1; transition: opacity 0.5s ease; } -.outer + frigate-card-menu[data-mode^=hover-] { - opacity: 0.0; +.outer + frigate-card-menu[data-style='hover'] { + opacity: 0; } -.outer:hover + frigate-card-menu[data-mode^=hover-], frigate-card-menu[data-mode^=hover-]:hover { - opacity: 1.0; +.outer:hover + frigate-card-menu[data-style='hover'], +frigate-card-menu[data-mode='hover']:hover { + opacity: 1; } ha-card { @@ -78,7 +82,7 @@ ha-card { border-radius: var(--ha-card-border-radius, 4px); } -frigate-card-live.hidden { +frigate-card-live.hidden { // Live view will be rendered but hidden for live preloading. display: none; } diff --git a/src/scss/menu.scss b/src/scss/menu.scss index 45063aa3..73c5fd09 100644 --- a/src/scss/menu.scss +++ b/src/scss/menu.scss @@ -14,23 +14,41 @@ flex-wrap: wrap; } -:host([data-mode="above"]),:host([data-mode="below"]) { +:host([data-style='outside']) { width: 100%; background: var(--secondary-background-color); } -:host([data-mode="above"]) { +:host([data-mode='outside'][data-position='top']) { border-top-left-radius: var(--ha-card-border-radius, 4px); border-top-right-radius: var(--ha-card-border-radius, 4px); } -:host([data-mode="below"]) { +:host([data-mode='outside'][data-position='below']) { border-bottom-left-radius: var(--ha-card-border-radius, 4px); border-bottom-right-radius: var(--ha-card-border-radius, 4px); } -:host([data-mode$="-left"]) { - left: 0px; +:host([data-position='top']), +:host([data-position='left'][data-alignment='top']), +:host([data-position='right'][data-alignment='top']) { top: 0px; +} +:host([data-position='bottom']), +:host([data-position='left'][data-alignment='bottom']), +:host([data-position='right'][data-alignment='bottom']) { + bottom: 0px; +} +:host([data-position='left']), +:host([data-position='top'][data-alignment='left']), +:host([data-position='bottom'][data-alignment='left']) { + left: 0px; +} +:host([data-position='right']), +:host([data-position='top'][data-alignment='right']), +:host([data-position='bottom'][data-alignment='right']) { + right: 0px; +} +:host([data-position='left']) { // Awful hack: Flexbox column wrapping doesn't work properly in most major // browsers -- the element boundary does not expand to cover the full wrapped // content as it should. This results in the wrapped 'content' appearing @@ -43,66 +61,63 @@ // - https://bugs.chromium.org/p/chromium/issues/detail?id=507397 writing-mode: vertical-lr; } - -:host([data-mode$="-right"]) { - right: 0px; - top: 0px; - +:host([data-position='right']) { // See "Awful hack" above. writing-mode: vertical-rl; } -:host([data-mode$="-top"]) { - left: 0px; - top: 0px; +:host([data-position='left'][data-alignment='bottom']), +:host([data-position='right'][data-alignment='bottom']), +:host([data-position='top'][data-alignment='right']), +:host([data-position='bottom'][data-alignment='right']) { + flex-direction: row-reverse; } -:host([data-mode$="-bottom"]) { - left: 0px; - bottom: 0px; - +:host([data-position='bottom']) { // If the menu has more content that allows, "wrap upwards" to keep the // Frigate button in the same place. flex-wrap: wrap-reverse; } -:host([data-mode^="overlay-"]),:host([data-mode^="hover-"]),:host([data-mode^="hidden-"]) { +:host([data-style='overlay']), +:host([data-style='hover']), +:host([data-style='hidden']) { position: absolute; overflow: hidden; width: calc(var(--frigate-card-menu-button-size) + 6px); height: calc(var(--frigate-card-menu-button-size) + 6px); } -:host([data-mode="overlay-top"]), -:host([data-mode="overlay-bottom"]), -:host([data-mode="hover-top"]), -:host([data-mode="hover-bottom"]), -:host([data-mode="hidden-top"][expanded]), -:host([data-mode="hidden-bottom"][expanded]) { +:host([data-style='overlay'][data-position='top']), +:host([data-style='overlay'][data-position='bottom']), +:host([data-style='hover'][data-position='top']), +:host([data-style='hover'][data-position='bottom']), +:host([data-style='hidden'][data-position='top'][expanded]), +:host([data-style='hidden'][data-position='bottom'][expanded]) { width: 100%; height: auto; overflow: visible; - background: linear-gradient(90deg, rgba(0,0,0,0.3), rgba(0,0,0,0)); + background: linear-gradient(90deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0)); } -:host([data-mode="overlay-left"]), -:host([data-mode="overlay-right"]), -:host([data-mode="hover-left"]), -:host([data-mode="hover-right"]), -:host([data-mode="hidden-left"][expanded]), -:host([data-mode="hidden-right"][expanded]) { +:host([data-style='overlay'][data-position='left']), +:host([data-style='overlay'][data-position='right']), +:host([data-style='hover'][data-position='left']), +:host([data-style='hover'][data-position='right']), +:host([data-style='hidden'][data-position='left'][expanded]), +:host([data-style='hidden'][data-position='right'][expanded]) { height: 100%; width: auto; overflow: visible; - background: linear-gradient(180deg, rgba(0,0,0,0.3), rgba(0,0,0,0)); + background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0)); } -:host([data-mode="overlay-left"]) > *, -:host([data-mode="hover-left"]) > *, -:host([data-mode="hidden-left"]) > *, -:host([data-mode="overlay-right"]) > *, -:host([data-mode="hover-right"]) > *, -:host([data-mode="hidden-right"]) > * { +:host([data-style='overlay'][data-position='left']) > *, +:host([data-style='overlay'][data-position='right']) > *, +:host([data-style='hover'][data-position='left']) > *, +:host([data-style='hover'][data-position='right']) > *, +:host([data-style='hidden'][data-position='left']) > *, +:host([data-style='hidden'][data-position='right']) > * { // See "Awful hack" above. writing-mode: horizontal-tb; } diff --git a/src/types.ts b/src/types.ts index 00cd0af5..17e0aa35 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,23 +53,21 @@ const FRIGATE_CARD_VIEWS = [ export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number]; -const FRIGATE_MENU_MODES = [ +const FRIGATE_MENU_STYLES = [ 'none', - 'hidden-top', - 'hidden-left', - 'hidden-bottom', - 'hidden-right', - 'overlay-top', - 'overlay-left', - 'overlay-bottom', - 'overlay-right', - 'hover-top', - 'hover-left', - 'hover-bottom', - 'hover-right', - 'above', - 'below', + 'hidden', + 'overlay', + 'hover', + 'outside', ] as const; +const FRIGATE_MENU_POSITIONS = [ + 'left', + 'right', + 'top', + 'bottom', +] as const; +const FRIGATE_MENU_ALIGNMENTS = FRIGATE_MENU_POSITIONS; + const LIVE_PROVIDERS = ['auto', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const; export type LiveProvider = typeof LIVE_PROVIDERS[number]; @@ -615,7 +613,9 @@ export type LiveConfig = z.infer; * Menu configuration section. */ const menuConfigDefault = { - mode: 'hidden-top' as const, + style: 'hidden' as const, + position: 'top' as const, + alignment: 'left' as const, buttons: { frigate: true, cameras: true, @@ -633,7 +633,9 @@ const menuConfigDefault = { const menuConfigSchema = z .object({ - mode: z.enum(FRIGATE_MENU_MODES).default(menuConfigDefault.mode), + style: z.enum(FRIGATE_MENU_STYLES).default(menuConfigDefault.style), + position: z.enum(FRIGATE_MENU_POSITIONS).default(menuConfigDefault.position), + alignment: z.enum(FRIGATE_MENU_ALIGNMENTS).default(menuConfigDefault.alignment), buttons: z .object({ frigate: z.boolean().default(menuConfigDefault.buttons.frigate),