Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR1284 - Fixing for sections in large layout #1284

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

MrBearPresident
Copy link
Collaborator

@MrBearPresident MrBearPresident commented Feb 22, 2025

Proposed change

Fixes for large-layouts in sections

  • Prevent child cards from a pop-up to follow pop-up sizing
  • Set height in separator cards to follow the height of the section
  • Divide the space between sub-button rows so it is always more evenly spaced.

Type of change

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (thank you!)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Example configuration

Pop-ups > yaml
type: vertical-stack
cards:
  - type: custom:bubble-card
    card_type: pop-up
    icon_container_color:
      color: ""
    hash: "#pop1"
    card_layout: large
    modules:
      - default
  - type: custom:bubble-card
    card_type: button
    icon_container_color:
      color: ""
    button_type: state
    entity: sun.sun
    card_layout: large
  - type: custom:bubble-card
    card_type: button
    icon_container_color:
      color: ""
    button_type: state
    card_layout: large
    entity: sun.sun
  - type: custom:bubble-card
    card_type: button
    icon_container_color:
      color: ""
    button_type: state
    card_layout: large
    entity: sun.sun
grid_options:
  columns: 12
  rows: 3
Separator > yaml
type: custom:bubble-card
card_type: separator
modules:
  - default
name: Home
icon: mdi:home
sub_button:
  - entity: sun.sun
  - entity: sun.sun
  - entity: sun.sun
  - entity: sun.sun
card_layout: large
grid_options:
  columns: 12
  rows: 2

Example printscreens/gif

Pop-ups > After

image

Separator > After

Image

Additional information

Additional documentation needed.

Needed to clearify that only large-layout change to sections size.

Checklist

  • The code change is tested and works locally.
  • There is no commented out code in this PR.
  • Tests screenshots/gifs have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

  • Documentation added/updated for readme.
    image
    image

@MrBearPresident MrBearPresident changed the title Fixing pop-up issue when large layout is used. #1283 PR1284 - Fixing for sections in large layout Feb 22, 2025
@MrBearPresident MrBearPresident self-assigned this Feb 22, 2025
@MrBearPresident MrBearPresident linked an issue Feb 22, 2025 that may be closed by this pull request
@MrBearPresident MrBearPresident marked this pull request as ready for review February 22, 2025 15:59
@Clooos
Copy link
Owner

Clooos commented Feb 23, 2025

I will check that soon, thank you for the fixes!

@Clooos
Copy link
Owner

Clooos commented Feb 24, 2025

Hi again! I wanted to let you know that I'm finally working on the new code structure for the cards, and I should be almost done! 🤞

This new structure will improve maintainability by a lot, because there is now a big part of the element creations, classes and of the CSS that is shared between all the cards! No need to modify the styles one by one anymore in all cards, it was about time! And don't worry, I will merge your PR first then I will adapt it to this new structure.

I've also added more containers to allow more flexibility/customizations, this will make the upcoming layouts a lot easier to make. And there should be no breaking change for all current custom styles 🙌

@MrBearPresident
Copy link
Collaborator Author

Hi again! I wanted to let you know that I'm finally working on the new code structure for the cards, and I should be almost done! 🤞

How are you planning to do the main structure? Like I suggested or did you think of something better?

@Clooos
Copy link
Owner

Clooos commented Feb 25, 2025

How are you planning to do the main structure? Like I suggested or did you think of something better?

This will look like that, here is for example the full create.js for the button card, don't hesitate to tell me if you have any feedback on that new structure:

import { createBaseStructure } from "../../components/card-structure.js";
import { addActions } from "../../tools/tap-actions.js";
import { getButtonType } from "./helpers.js";
import styles from "./styles.css";

export function createStructure(context, appendTo = context.container) {
    const cardType = 'button';
    const buttonType = getButtonType(context);
    const isSlider = buttonType === 'slider';
    const actions = {};

    actions['switch'] = {
        button: {
            tap_action: { action: "toggle" },
            double_tap_action: { action: "toggle" },
            hold_action: { action: "more-info" }
        }
    };

    actions['state'] = {
        button: {
            tap_action: { action: "more-info" },
            double_tap_action: { action: "more-info" },
            hold_action: { action: "more-info" }
        }
    };

    actions['name'] = {
        button: {
            tap_action: { action: "none" },
            double_tap_action: { action: "none" },
            hold_action: { action: "none" }
        },
        icon: {
            tap_action: { action: "none" },
            double_tap_action: { action: "none" },
            hold_action: { action: "none" }
        }
    };

    const elements = createBaseStructure(context, {
        type: cardType,
        appendTo: appendTo,
        styles: styles,
        withSlider: isSlider,
        withFeedback: !isSlider,
        iconActions: actions[buttonType]?.icon,
        buttonActions: actions[buttonType]?.button,
    });

    // Add backward compatibility
    elements.background.classList.add('bubble-button-background');
    elements.mainContainer.classList.add('bubble-button-card-container');
    elements.cardWrapper.classList.add('bubble-button-card');

    if (appendTo !== context.container) {
        context.buttonType = buttonType;
    } else {
        context.cardType = cardType;
    }
}

The createBaseStructure function:

import { createElement, toggleEntity, throttle, forwardHaptic, isEntityType } from "../tools/utils.js";
import { addActions, addFeedback } from "../tools/tap-actions.js";
import { createSliderStructure } from "../components/slider.js";
import styles from "./styles.css";

const processedStylesCache = {};

export function createBaseStructure(context, config = {}) {
    context.elements = context.elements || {};
    
    const defaults = {
        type: 'base',
        appendTo: context.content,
        baseCardStyles: styles,
        styles: '',
        withFeedback: true,
        withImage: true,
        withCustomStyle: true,
        withState: true,
        withBackground: true,
        iconActions: false,
        buttonActions: false
    };
    const options = { ...defaults, ...config };

    context.elements.mainContainer = createElement('div', `bubble-${options.type}-container bubble-container`);
    context.elements.cardWrapper = createElement('div', `bubble-${options.type} bubble-wrapper`);
    context.elements.contentContainer = createElement('div', 'bubble-content-container');
    context.elements.buttonsContainer = createElement('div', 'bubble-buttons-container');

    context.elements.iconContainer = createElement('div', 'bubble-icon-container icon-container');
    context.elements.icon = createElement('ha-icon', 'bubble-icon icon');
    context.elements.image = createElement('div', 'bubble-entity-picture entity-picture');
    
    context.elements.nameContainer = createElement('div', 'bubble-name-container name-container');
    context.elements.name = createElement('div', 'bubble-name name');
    context.elements.state = createElement('div', 'bubble-state state');

    if(options.withBackground) {
        context.elements.background = createElement('div', 'bubble-background');
        context.elements.cardWrapper.prepend(context.elements.background);
    }

    context.elements.iconContainer.append(
        context.elements.icon,
        options.withImage ? context.elements.image : null
    );

    context.elements.nameContainer.append(
        context.elements.name,
        options.withState ? context.elements.state : null
    );

    context.elements.contentContainer.append(
        context.elements.iconContainer,
        context.elements.nameContainer
    );

    context.elements.cardWrapper.append(
        context.elements.contentContainer, 
        context.elements.buttonsContainer
    );

    if (options.withFeedback) {
        context.elements.feedbackContainer = createElement('div', 'bubble-feedback-container feedback-container');
        context.elements.feedback = createElement('div', 'bubble-feedback-element feedback-element');
        context.elements.feedback.style.display = 'none';
        context.elements.feedbackContainer.append(context.elements.feedback);
        context.elements.cardWrapper.append(context.elements.feedbackContainer);
        addFeedback(context.elements.background, context.elements.feedback);    
    }

    context.elements.mainContainer.appendChild(context.elements.cardWrapper);

    if (options.withSlider) {
        createSliderStructure(context);
    }

    if (options.styles) {
      if (!processedStylesCache[options.type]) {
        // Replace 'card-type' in CSS variables with the real card type
        processedStylesCache[options.type] = options.baseCardStyles.replace(/card-type/g, options.type);
      }

      context.elements.style = createElement('style');
      context.elements.style.innerText = processedStylesCache[options.type] + options.styles;
      context.elements.mainContainer.appendChild(context.elements.style);
    }
    
    if (options.withCustomStyle) {
        context.elements.customStyle = createElement('style');
        context.elements.mainContainer.appendChild(context.elements.customStyle);
    }

    if (options.iconActions === true) {
        addActions(context.elements.iconContainer, context.config);
    } else if (options.iconActions !== false) {
        addActions(context.elements.iconContainer, context.config, context.config.entity, options.iconActions);
    }

    if (options.buttonActions === true) {
        addActions(context.elements.background, context.config.button_action, context.config.entity);
    } else if (options.buttonActions !== undefined) {
        addActions(context.elements.background, context.config.button_action, context.config.entity, options.buttonActions);
    }

    if (options.appendTo === context.content) {
        context.content.appendChild(context.elements.mainContainer);
    } else {
        options.appendTo.appendChild(context.elements.mainContainer);
    }

    return context.elements;
}

And the shared styles (this is removing a lot of duplicate CSS in all cards!):

/* 'card-type' in CSS variables is replaced with the real card type 
   in card-structure.js for easier maintenance */

* {
    -webkit-tap-highlight-color: transparent !important;
    -ms-overflow-style: none; /* for Internet Explorer, Edge */
    scrollbar-width: none; /* for Firefox */
}

*::-webkit-scrollbar {
    display: none; /* for Chrome, Safari, and Opera */
}

ha-card {
    margin-top: 0;
    background: none;
    opacity: 1;
}

.bubble-container {
    position: relative;
    width: 100%;
    height: 50px;
    background-color: var(--bubble-card-type-main-background-color, var(--bubble-main-background-color, var(--background-color-2, var(--secondary-background-color))));
    border-radius: var(--bubble-card-type-border-radius, var(--bubble-border-radius, calc(var(--row-height,56px)/2)));
    box-shadow: var(--bubble-card-type-box-shadow, var(--bubble-box-shadow, none));
    overflow: scroll;
    touch-action: pan-y;
    border: var(--bubble-card-type-border, var(--bubble-border, none));
    box-sizing: border-box;
}

.bubble-wrapper {
    display: flex;
    position: absolute;
    justify-content: space-between;
    align-items: center;
    height: 100%;
    width: 100%;
    transition: all 1.5s;
    border-radius: var(--bubble-card-type-border-radius, var(--bubble-border-radius, calc(var(--row-height,56px)/2)));
    background-color: rgba(0,0,0,0);
    overflow: visible;
}

.bubble-content-container {
    display: contents;
    flex-grow: 1;
    overflow: hidden;
}

.bubble-buttons-container {
    display: contents;
}

.bubble-sub-button-container {
    right: 8px !important;
}

.bubble-background {
    display: flex;
    position: absolute;
    height: 100%;
    width: 100%;
    transition: background-color 1.5s;
    border-radius: var(--bubble-card-type-border-radius, var(--bubble-border-radius, calc(var(--row-height,56px)/2)));
    -webkit-mask-image: radial-gradient(circle, rgba(0, 0, 0, 1) 98%, rgba(0, 0, 0, 0) 100%);
    mask-image: radial-gradient(circle, rgba(0, 0, 0, 1) 98%, rgba(0, 0, 0, 0) 100%);
}

.bubble-icon-container {
    display: flex;
    flex-wrap: wrap;
    align-content: center;
    justify-content: center;
    min-width: 38px;
    min-height: 38px;
    margin: 6px;
    border-radius: var(--bubble-card-type-icon-border-radius, var(--bubble-icon-border-radius, var(--bubble-border-radius, 50%)));
    background-color: var(--bubble-card-type-icon-background-color, var(--bubble-icon-background-color, var(--bubble-secondary-background-color, var(--card-background-color, var(--ha-card-background)))));
    overflow: hidden;
    position: relative;
    cursor: pointer;
}

.is-off .bubble-icon {
    opacity: 0.6;
}

.is-on .bubble-icon {
  filter: brightness(1.1);
  opacity: 1;
}

.bubble-entity-picture {
    background-size: cover;
    background-position: center;
    height: 100%;
    width: 100%;
    position: absolute;
}

.bubble-name,
.bubble-state {
    display: flex;
    position: relative;
    white-space: nowrap;
}

.bubble-name-container {
    display: flex;
    line-height: 18px;
    flex-direction: column;
    justify-content: center;
    flex-grow: 1;
    margin: 0 16px 0 4px;
    pointer-events: none;
    position: relative;
    overflow: hidden;
}

.bubble-name {
    font-size: 13px;
    font-weight: 600;
}

.bubble-state {
    font-size: 12px;
    font-weight: normal;
    opacity: 0.7;
}

.bubble-range-fill {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 100%;
    left: -100%;
    transition: all .3s;
    z-index: 0;
}

.is-dragging .bubble-range-fill {
    transition: none;
}

.is-light .bubble-range-fill {
    opacity: 0.5;
}

.is-unavailable .bubble-wrapper,
.is-unavailable .bubble-range-slider {
    cursor: not-allowed;
}

.is-unavailable {
    opacity: 0.5;
}

.bubble-feedback-container,
.bubble-feedback-element {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    overflow: hidden;
    border-radius: var(--bubble-card-type-border-radius, var(--bubble-border-radius, calc(var(--row-height,56px)/2)));
}

.bubble-feedback-element {
    opacity: 0;
    background-color: rgb(0,0,0);
    border-radius: 0;
}

@keyframes tap-feedback {
    0% {transform: translateX(-100%); opacity: 0;}
    64% {transform: translateX(0); opacity: 0.1;}
    100% {transform: translateX(100%); opacity: 0;}
}

.large .bubble-container {
    height: calc( var(--row-height,56px) * var(--row-size,1) + var(--row-gap,8px) * ( var(--row-size,1) - 1 ));
    border-radius: var(--bubble-card-type-border-radius, var(--bubble-border-radius, calc(var(--row-height,56px)/2)));
}

.large .bubble-icon-container {
    --mdc-icon-size: 24px;
    min-width: 42px !important;
    min-height: 42px !important;
    margin-left: 8px;
}

.large .bubble-sub-button-container:has(> :last-child:nth-child(2)) :nth-child(2) {
    grid-row: 1 / calc(var(--row-size,1) + 1);
}

.rows-2 .bubble-sub-button-container {
    flex-direction: column;
    gap: 4px !important;
    display: grid !important;
    grid-template-columns: repeat(1, 1fr);
    grid-template-rows: repeat(calc(2*var(--row-size,1)), minmax(auto, max-content));
    grid-auto-flow: column;
    width: auto;
}

.rows-2 .bubble-sub-button {
    height: 20px !important;
}

.large.rows-2 .bubble-sub-button-container:has(> :last-child:nth-child(2)) :nth-child(2) {
    grid-row: 1 / calc(2*var(--row-size,1) + 1);
}

@MrBearPresident
Copy link
Collaborator Author

In the resulting structure is not much changed, except the extra content-container and cardwrapper? Or am I missing things?

image

The content-container makes it harder to do stuff like the room card (#927), there is a way around it but just so you know.
Are you planning to put other card specific buttons in the separate container under the cardwrapper? Or are you planning a similar route to the media-player-card? where all the Card specific items land under the buttons container.
I would advice against putting everything in a big bucket but splitting them up.

@Clooos
Copy link
Owner

Clooos commented Feb 25, 2025

Indeed the structure has not changed a lot, the card that will change the most is the cover card.

For the media player, I've already refactored it, and indeed I'm only using this new function for the base structure, all buttons and card specific things are handled mostly like before.

And for your room card, I'm actually using it in my test dashboard to see if anything breaks, and it's still looking the same with the new structure 👌

Edit: I'm a bit tired so I'm not sure to exactly understand what you mean for the media player, so here is the create.js:

let volumeLevel = 0;

export function createStructure(context) {
    const cardType = 'media-player';

    const elements = createBaseStructure(context, {
        type: cardType,
        withFeedback: false,
        styles: styles
    });

    elements.mediaInfoContainer = createElement('div', 'bubble-media-info-container');
    elements.playPauseButton = createElement('ha-icon', 'bubble-play-pause-button');
    elements.previousButton = createElement('ha-icon', 'bubble-previous-button');
    elements.previousButton.setAttribute("icon", "mdi:skip-previous");
    elements.nextButton = createElement('ha-icon', 'bubble-next-button');
    elements.nextButton.setAttribute("icon", "mdi:skip-next");
    elements.volumeButton = createElement('ha-icon', 'bubble-volume-button');
    elements.volumeButton.setAttribute("icon", "mdi:volume-high");
    elements.powerButton = createElement('ha-icon', 'bubble-power-button');
    elements.powerButton.setAttribute("icon", "mdi:power-standby");
    elements.muteButton = createElement('ha-icon', 'bubble-mute-button is-hidden');
    elements.muteButton.setAttribute("icon", "mdi:volume-off");
    elements.title = createElement('div', 'bubble-title');
    elements.artist = createElement('div', 'bubble-artist');

    // Add backward compatibility
    elements.background.classList.add('bubble-cover-background');

    elements.iconContainer.appendChild(elements.muteButton);
    elements.mediaInfoContainer.append(elements.title, elements.artist);
    elements.contentContainer.append(elements.mediaInfoContainer);
    elements.buttonsContainer.append(
        elements.powerButton,
        elements.previousButton,
        elements.nextButton,
        elements.volumeButton,
        elements.playPauseButton
    );

    addActions(elements.icon, context.config, context.config.entity);
    addActions(elements.image, context.config, context.config.entity);

    elements.volumeSliderContainer = createElement('div', 'bubble-volume-slider is-hidden');
    createSliderStructure(context, {
        targetElement: elements.volumeSliderContainer,
        sliderLiveUpdate: false,
        withValueDisplay: true,
    });

    elements.cardWrapper.appendChild(elements.volumeSliderContainer);

    elements.volumeButton.addEventListener('click', () => {
        elements.volumeSliderContainer.classList.toggle('is-hidden');
        elements.muteButton.classList.toggle('is-hidden');
        elements.icon.classList.toggle('is-hidden');
        elements.image.classList.toggle('is-hidden');
        changeVolumeIcon(context);
    });

    elements.powerButton.addEventListener('click', () => {
        const isOn = isStateOn(context);
        context._hass.callService('media_player', isOn ? 'turn_off' : 'turn_on', {
            entity_id: context.config.entity
        });
    });

    elements.muteButton.addEventListener('click', () => {
        const isVolumeMuted = getAttribute(context, "is_volume_muted") === true;
        context._hass.callService('media_player', 'volume_mute', {
            entity_id: context.config.entity,
            is_volume_muted: !isVolumeMuted
        });
        elements.muteButton.clicked = true;
    });

    elements.previousButton.addEventListener('click', () => {
        context._hass.callService('media_player', 'media_previous_track', {
            entity_id: context.config.entity
        });
    });

    elements.nextButton.addEventListener('click', () => {
        context._hass.callService('media_player', 'media_next_track', {
            entity_id: context.config.entity
        });
    });

    elements.playPauseButton.addEventListener('click', () => {
        context._hass.callService('media_player', 'media_play_pause', {
            entity_id: context.config.entity
        });
        elements.playPauseButton.clicked = true;
    });

    elements.mainContainer.addEventListener('click', () => forwardHaptic("selection"));

    context.cardType = cardType;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants