Compare commits

...

1 Commits

Author SHA1 Message Date
undergroundwires
704a3d0417 Fix card height inconsistency
TODO: Is CardLayout, card-layout, cardLayout etc., best names possiuble?

Fix card expansion panel heights not being equal. This is due to
limitations in CSS flex view.

Heights of all cards are `100%` which gives them uniform loook on same
row with all equal heights. However, their heights are set to `auto`
when of the cards are open. In that case, their sizes are no longer
equal in same row, cards with longer text/titles keeping more space.
This is because when a card is open, its expansion panel grows in its
own DOM element, increasing the height of the card.
at same height on each line, their heights change completely when one of

Heights gets fucked up when card is collapsing, this is big fix, fix this.

Supporting changes:

- Move card expander to its own component `CardExpansionPanel`.
- Introduce `UseCardLayout` hook to do calculations instead of CSS to
  circumvent limitations in CSS flex view.
2024-05-28 16:20:20 +02:00
4 changed files with 213 additions and 92 deletions

View File

@@ -0,0 +1,91 @@
<template>
<div>
<CardExpansionArrow />
<div class="card__expander">
<div class="card__expander__close-button">
<FlatButton
icon="xmark"
@click="collapse()"
/>
</div>
<div class="card__expander__content">
<ScriptsTree
:category-id="categoryId"
:has-top-padding="false"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
} from 'vue';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
import CardExpansionArrow from './CardExpansionArrow.vue';
export default defineComponent({
components: {
ScriptsTree,
FlatButton,
CardExpansionArrow,
},
props: {
categoryId: {
type: Number,
required: true,
},
},
emits: {
/* eslint-disable @typescript-eslint/no-unused-vars */
onCollapse: () => true,
/* eslint-enable @typescript-eslint/no-unused-vars */
},
setup(_, { emit }) {
function collapse() {
emit('onCollapse');
}
return {
collapse,
};
},
});
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
@use "./card-gap" as *;
.card__expander {
position: relative;
background-color: $color-primary-darker;
color: $color-on-primary;
margin-top: $spacing-absolute-xx-large;
display: flex;
align-items: center;
flex-direction: column;
.card__expander__content {
display: flex;
justify-content: center;
word-break: break-word;
max-width: 100%; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
width: 100%; // Expands the container to fill available horizontal space, enabling alignment of child items.
}
.card__expander__close-button {
font-size: $font-size-absolute-large;
align-self: flex-end;
margin-right: $spacing-absolute-small;
@include clickable;
color: $color-primary-light;
@include hover-or-touch {
color: $color-primary;
}
}
}
</style>

View File

@@ -27,6 +27,7 @@
:data-category="categoryId" :data-category="categoryId"
:category-id="categoryId" :category-id="categoryId"
:active-category-id="activeCategoryId" :active-category-id="activeCategoryId"
:card-layout="cardLayout"
@card-expansion-changed="onSelected(categoryId, $event)" @card-expansion-changed="onSelected(categoryId, $event)"
/> />
</div> </div>
@@ -46,6 +47,7 @@ import { injectKey } from '@/presentation/injectionSymbols';
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue'; import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
import { hasDirective } from './NonCollapsingDirective'; import { hasDirective } from './NonCollapsingDirective';
import CardListItem from './CardListItem.vue'; import CardListItem from './CardListItem.vue';
import { useCardLayout } from './UseCardLayout';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -61,8 +63,14 @@ export default defineComponent({
const categoryIds = computed<readonly number[]>( const categoryIds = computed<readonly number[]>(
() => currentState.value.collection.actions.map((category) => category.id), () => currentState.value.collection.actions.map((category) => category.id),
); );
const activeCategoryId = ref<number | undefined>(undefined); const activeCategoryId = ref<number | undefined>(undefined);
const cardLayout = useCardLayout({
containerWidth: computed(() => width.value ?? 0),
totalCards: computed(() => categoryIds.value.length),
});
function onSelected(categoryId: number, isExpanded: boolean) { function onSelected(categoryId: number, isExpanded: boolean) {
activeCategoryId.value = isExpanded ? categoryId : undefined; activeCategoryId.value = isExpanded ? categoryId : undefined;
} }
@@ -101,6 +109,7 @@ export default defineComponent({
width, width,
categoryIds, categoryIds,
activeCategoryId, activeCategoryId,
cardLayout,
onSelected, onSelected,
}; };
}, },

View File

@@ -29,26 +29,12 @@
/> />
</div> </div>
<CardExpandTransition> <CardExpandTransition>
<div v-show="isExpanded"> <CardExpansionPanel
<CardExpansionArrow /> v-show="isExpanded"
<div
class="card__expander"
@click.stop
>
<div class="card__expander__close-button">
<FlatButton
icon="xmark"
@click="collapse()"
/>
</div>
<div class="card__expander__content">
<ScriptsTree
:category-id="categoryId" :category-id="categoryId"
:has-top-padding="false" @on-collapse="collapse"
@click.stop
/> />
</div>
</div>
</div>
</CardExpandTransition> </CardExpandTransition>
</div> </div>
</template> </template>
@@ -56,30 +42,32 @@
<script lang="ts"> <script lang="ts">
import { import {
defineComponent, computed, shallowRef, defineComponent, computed, shallowRef,
type PropType,
} from 'vue'; } from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue'; import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
import { sleep } from '@/infrastructure/Threading/AsyncSleep'; import { sleep } from '@/infrastructure/Threading/AsyncSleep';
import CardSelectionIndicator from './CardSelectionIndicator.vue'; import CardSelectionIndicator from './CardSelectionIndicator.vue';
import CardExpandTransition from './CardExpandTransition.vue'; import CardExpandTransition from './CardExpandTransition.vue';
import CardExpansionArrow from './CardExpansionArrow.vue'; import CardExpansionPanel from './CardExpansionPanel.vue';
import type { CardLayout } from './UseCardLayout';
export default defineComponent({ export default defineComponent({
components: { components: {
ScriptsTree,
AppIcon, AppIcon,
CardSelectionIndicator, CardSelectionIndicator,
FlatButton, CardExpansionPanel,
CardExpandTransition, CardExpandTransition,
CardExpansionArrow,
}, },
props: { props: {
categoryId: { categoryId: {
type: Number, type: Number,
required: true, required: true,
}, },
cardLayout: {
type: Object as PropType<CardLayout>,
required: true,
},
activeCategoryId: { activeCategoryId: {
type: Number, type: Number,
default: undefined, default: undefined,
@@ -129,6 +117,7 @@ export default defineComponent({
cardTitle, cardTitle,
isExpanded, isExpanded,
cardElement, cardElement,
totalColumns: props.cardLayout.totalColumns,
collapse, collapse,
}; };
}, },
@@ -141,7 +130,6 @@ export default defineComponent({
@use "./card-gap" as *; @use "./card-gap" as *;
$card-inner-padding : $spacing-absolute-xx-large; $card-inner-padding : $spacing-absolute-xx-large;
$expanded-margin-top : $spacing-absolute-xx-large;
$card-horizontal-gap : $card-gap; $card-horizontal-gap : $card-gap;
.card { .card {
@@ -190,44 +178,13 @@ $card-horizontal-gap : $card-gap;
font-size: $font-size-absolute-normal; font-size: $font-size-absolute-normal;
} }
} }
.card__expander {
position: relative;
background-color: $color-primary-darker;
color: $color-on-primary;
display: flex;
align-items: center;
flex-direction: column;
.card__expander__content {
display: flex;
justify-content: center;
word-break: break-word;
max-width: 100%; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
width: 100%; // Expands the container to fill available horizontal space, enabling alignment of child items.
}
.card__expander__close-button {
font-size: $font-size-absolute-large;
align-self: flex-end;
margin-right: $spacing-absolute-small;
@include clickable;
color: $color-primary-light;
@include hover-or-touch {
color: $color-primary;
}
}
}
&.is-expanded { &.is-expanded {
.card__inner { .card__inner {
height: auto; height: auto;
background-color: $color-secondary; background-color: $color-secondary;
color: $color-on-secondary; color: $color-on-secondary;
} margin-bottom: $spacing-absolute-xx-large;
.card__expander {
margin-top: $expanded-margin-top;
} }
@include hover-or-touch { @include hover-or-touch {
@@ -253,36 +210,32 @@ $card-horizontal-gap : $card-gap;
} }
} }
} }
@mixin adaptive-card($cards-in-row) {
&.card { .card {
$total-times-gap-is-used-in-row: $cards-in-row - 1; $total-columns: v-bind(totalColumns);
$total-gap-width-in-row: $total-times-gap-is-used-in-row * $card-horizontal-gap; $total-times-gap-is-used-in-row: calc($total-columns - 1);
$total-gap-width-in-row: calc($total-times-gap-is-used-in-row * $card-horizontal-gap);
$available-row-width-for-cards: calc(100% - #{$total-gap-width-in-row}); $available-row-width-for-cards: calc(100% - #{$total-gap-width-in-row});
$available-width-per-card: calc(#{$available-row-width-for-cards} / #{$cards-in-row}); $available-width-per-card: calc(#{$available-row-width-for-cards} / $total-columns);
width:$available-width-per-card; width:$available-width-per-card;
.card__expander { :deep(.card__expander) {
$all-cards-width: 100% * $cards-in-row; $all-cards-width: calc(100% * $total-columns);
$additional-padding-width: $card-horizontal-gap * ($cards-in-row - 1); $additional-padding-width: calc($card-horizontal-gap * ($total-columns - 1));
width: calc(#{$all-cards-width} + #{$additional-padding-width}); width: calc(#{$all-cards-width} + #{$additional-padding-width});
} }
@for $nth-card from 2 through $cards-in-row { // From second card to rest // @for $nth-card from 2 through $total-columns { // From second card to rest
&:nth-of-type(#{$cards-in-row}n+#{$nth-card}) { // &:nth-of-type(#{$total-columns}n+#{$nth-card}) {
.card__expander { // :deep(.card__expander) {
$card-left: -100% * ($nth-card - 1); // $card-left: -100% * ($nth-card - 1);
$additional-space: $card-horizontal-gap * ($nth-card - 1); // $additional-space: $card-horizontal-gap * ($nth-card - 1);
margin-left: calc(#{$card-left} - #{$additional-space}); // margin-left: calc(#{$card-left} - #{$additional-space});
} // }
} // }
} // }
// Ensure new line after last row // Ensure new line after last row
$card-after-last: $cards-in-row + 1; $card-after-last: $total-columns + 1;
&:nth-of-type(#{$cards-in-row}n+#{$card-after-last}) { &:nth-of-type(#{$total-columns}n+#{$card-after-last}) {
clear: left; clear: left;
} }
} }
}
.big-screen { @include adaptive-card(3); }
.medium-screen { @include adaptive-card(2); }
.small-screen { @include adaptive-card(1); }
</style> </style>

View File

@@ -0,0 +1,68 @@
import { computed, type Ref } from 'vue';
export function useCardLayout(options: {
readonly containerWidth: Readonly<Ref<number>>;
readonly totalCards: Readonly<Ref<number>>;
}): Readonly<Ref<CardLayout>> {
return computed(() => {
return determineCardLayout(
options.containerWidth.value,
options.totalCards.value,
);
});
}
export interface CardLayout {
readonly totalRows: number;
readonly totalColumns: number;
readonly availableCardWidth: number;
}
function determineCardLayout(
containerWidth: number,
totalCards: number,
): CardLayout {
const containerSize = getContainerSize(containerWidth);
const totalColumns = countTotalColumns(containerSize);
const totalRows = countTotalRows(totalColumns, totalCards);
return {
totalColumns,
totalRows,
availableCardWidth: containerWidth / totalRows,
};
}
enum ContainerSize {
Small,
Medium,
Big,
}
function countTotalRows(totalColumns: number, totalCards: number): number {
return Math.ceil(totalCards / totalColumns);
}
function countTotalColumns(size: ContainerSize): number {
switch (size) {
case ContainerSize.Small:
return 1;
case ContainerSize.Medium:
return 2;
case ContainerSize.Big:
return 3;
default:
throw new Error(`Unknown size: ${size}`);
}
}
function getContainerSize(containerWidth: number): ContainerSize {
const smallBreakpoint = 500;
const bigBreakpoint = 750;
if (containerWidth <= smallBreakpoint) {
return ContainerSize.Small;
}
if (containerWidth < bigBreakpoint) {
return ContainerSize.Medium;
}
return ContainerSize.Big;
}