Adaptive button in the Status Bar (#7497)
Some checks are pending
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions

Signed-off-by: Alexander Platov <alexander.platov@hardcoreeng.com>
This commit is contained in:
Alexander Platov 2024-12-18 07:34:06 +03:00 committed by GitHub
parent 5933f16f95
commit 35be71c182
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 244 additions and 163 deletions

View File

@ -171,6 +171,48 @@
& > * { pointer-events: none; }
}
// StatusBar Button
.hulyStatusBarButton,
.hulyStatusBarButton .hulyStatusBarButton-icons {
display: flex;
align-items: center;
flex-wrap: nowrap;
min-width: 0;
min-height: 0;
}
.hulyStatusBarButton {
gap: var(--spacing-1);
padding: var(--spacing-0_25) var(--spacing-0_25) var(--spacing-0_25) var(--spacing-1);
height: 1.625rem;
font-weight: 500;
background-color: var(--theme-button-pressed);
border: 1px solid transparent;
border-radius: var(--extra-small-BorderRadius);
cursor: pointer;
&-label {
white-space: nowrap;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
min-width: 0;
font-weight: 700;
color: var(--theme-caption-color);
}
&-icons { gap: var(--spacing-0_25); }
&:hover {
background-color: var(--theme-button-hovered);
border-color: var(--theme-navpanel-divider);
}
&.active {
order: -1;
background-color: var(--highlight-select);
border-color: var(--highlight-select-border);
&:hover { background-color: var(--highlight-select-hover); }
}
}
// Old style Button
.antiButton {
display: flex;

View File

@ -317,6 +317,67 @@
}
}
/* CombineAvatars */
.hulyCombineAvatars-container {
display: flex;
align-items: center;
.hulyCombineAvatar {
&.inline:not(:first-child) { margin-left: calc(1px - (0.875rem / 2)); }
&.tiny:not(:first-child) { margin-left: calc(1px - (1.13rem / 2)); }
&.card:not(:first-child) { margin-left: calc(1px - (1.25rem / 2)); }
&.x-small:not(:first-child) { margin-left: calc(1px - (1.5rem / 2)); }
&.smaller:not(:first-child) { margin-left: calc(1px - (1.75rem / 2)); }
&.small:not(:first-child) { margin-left: calc(1px - 1rem); }
&.medium:not(:first-child) { margin-left: calc(1px - (2.25rem / 2)); }
&.large:not(:first-child) { margin-left: calc(1px - (4.5rem / 2)); }
&.x-large:not(:first-child) { margin-left: calc(1px - (7.5rem / 2)); }
&.inline,
&.tiny,
&.card,
&.x-small { font-size: 0.625rem; }
&.inline,
&.tiny,
&.card,
&.x-small,
&.smaller,
&.small {
&:not(:last-child) {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M16,24.5v-17c0-3.2,1.8-6.1,4.5-7.5H8C3.6,0,0,3.6,0,8v16c0,4.4,3.6,8,8,8h12.5C17.8,30.6,16,27.7,16,24.5z'/%3E%3C/svg%3E%0A") no-repeat;
}
}
&[data-over^='+']:last-child {
position: relative;
&::after {
content: attr(data-over);
position: absolute;
top: 50%;
left: 50%;
color: var(--theme-caption-color);
transform: translate(-53%, -52%);
z-index: 2;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--theme-bg-color);
border: 1px solid var(--theme-divider-color);
border-radius: 0.25rem;
opacity: 0.9;
z-index: 1;
}
}
}
}
/* Header */
.hulyHeader-container {
display: flex;

View File

@ -48,7 +48,7 @@
style:max-width={orientation === 'horizontal' ? maxSize : 'auto'}
style:max-height={orientation === 'vertical' ? maxSize : 'auto'}
class:active={highlighted}
use:tooltip={{ label: label ? getEmbeddedLabel(label) : labelIntl }}
use:tooltip={{ label: label ? getEmbeddedLabel(label) : labelIntl, direction: 'bottom' }}
on:click
on:contextmenu={handleContextMenu}
>

View File

@ -49,93 +49,16 @@
</script>
{#if items !== undefined}
<div class="avatars-container">
<div class="hulyCombineAvatars-container">
{#if includeEmpty}
<div class="combine-avatar {size}" data-over={getDataOver(persons.length === 0, items)}>
<div class="hulyCombineAvatar {size}" data-over={getDataOver(persons.length === 0, items)}>
<EmptyAvatar {size} />
</div>
{/if}
{#each persons as person, i}
<div class="combine-avatar {size}" data-over={getDataOver(persons.length === i + 1, items)}>
<div class="hulyCombineAvatar {size}" data-over={getDataOver(persons.length === i + 1, items)}>
<Avatar {person} {size} name={person.name} showStatus={false} />
</div>
{/each}
</div>
{/if}
<style lang="scss">
.avatars-container {
display: flex;
align-items: center;
.combine-avatar.inline:not(:first-child) {
margin-left: calc(1px - (0.875rem / 2));
}
.combine-avatar.tiny:not(:first-child) {
margin-left: calc(1px - (1.13rem / 2));
}
.combine-avatar.card:not(:first-child) {
margin-left: calc(1px - (1.25rem / 2));
}
.combine-avatar.x-small:not(:first-child) {
margin-left: calc(1px - (1.5rem / 2));
}
.combine-avatar.smaller:not(:first-child) {
margin-left: calc(1px - (1.75rem / 2));
}
.combine-avatar.small:not(:first-child) {
margin-left: calc(1px - 1rem);
}
.combine-avatar.inline,
.combine-avatar.tiny,
.combine-avatar.card,
.combine-avatar.x-small,
.combine-avatar.smaller,
.combine-avatar.small {
&:not(:last-child) {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M16,24.5v-17c0-3.2,1.8-6.1,4.5-7.5H8C3.6,0,0,3.6,0,8v16c0,4.4,3.6,8,8,8h12.5C17.8,30.6,16,27.7,16,24.5z'/%3E%3C/svg%3E%0A")
no-repeat;
}
}
.combine-avatar.medium:not(:first-child) {
margin-left: calc(1px - (2.25rem / 2));
}
.combine-avatar.large:not(:first-child) {
margin-left: calc(1px - (4.5rem / 2));
}
.combine-avatar.x-large:not(:first-child) {
margin-left: calc(1px - (7.5rem / 2));
}
.combine-avatar.inline,
.combine-avatar.tiny,
.combine-avatar.card,
.combine-avatar.x-small {
font-size: 0.625rem;
}
.combine-avatar[data-over^='+']:last-child {
position: relative;
&::after {
content: attr(data-over);
position: absolute;
top: 50%;
left: 50%;
color: var(--theme-caption-color);
transform: translate(-53%, -52%);
z-index: 2;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--theme-bg-color);
border: 1px solid var(--theme-divider-color);
border-radius: 0.25rem;
opacity: 0.9;
z-index: 1;
}
}
}
</style>

View File

@ -13,8 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { formatName } from '@hcengineering/contact'
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { personByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref, toIdMap } from '@hcengineering/core'
import {
Invite,
@ -26,7 +25,6 @@
Room,
RoomType
} from '@hcengineering/love'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
closePopup,
@ -35,7 +33,7 @@
location,
PopupResult,
showPopup,
tooltip
closeTooltip
} from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import workbench from '@hcengineering/workbench'
@ -58,6 +56,7 @@
import RequestPopup from './RequestPopup.svelte'
import RequestingPopup from './RequestingPopup.svelte'
import RoomPopup from './RoomPopup.svelte'
import RoomButton from './RoomButton.svelte'
const client = getClient()
@ -139,8 +138,11 @@
$: checkRequests(requests, $myInfo)
function openRoom (ev: MouseEvent, room: Room): void {
showPopup(RoomPopup, { room }, ev.currentTarget as HTMLElement)
function openRoom (room: Room): (e: MouseEvent) => void {
return (e: MouseEvent) => {
closeTooltip()
showPopup(RoomPopup, { room }, eventToHTMLElement(e))
}
}
let activeInvite: Invite | undefined = undefined
@ -285,92 +287,27 @@
<div class="flex-row-center flex-gap-2">
{#if activeRooms.length > 0}
<!-- <div class="divider" />-->
{#each activeRooms as active}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="container flex-row-center"
class:active={joined.find((r) => r._id === active._id)}
on:click={(ev) => {
openRoom(ev, active)
}}
>
<div class="mr-2 overflow-label">{getRoomName(active, $personByIdStore)}</div>
<div class="flex-row-center avatars">
{#each active.participants as participant}
<div use:tooltip={{ label: getEmbeddedLabel(formatName(participant.name)) }}>
<Avatar name={participant.name} size={'card'} person={$personByIdStore.get(participant.person)} />
</div>
{/each}
</div>
</div>
{#each activeRooms as active, i}
<RoomButton
label={getRoomName(active, $personByIdStore)}
participants={active.participants}
active={joined.find((r) => r._id === active._id) != null}
on:click={openRoom(active)}
/>
{/each}
{/if}
{#if reception && receptionParticipants.length > 0}
{#if activeRooms.length > 0}
<div class="divider" />
{/if}
<div class="container flex-row-center flex-gap-2">
<div>{getRoomName(reception, $personByIdStore)}</div>
<div class="flex-row-center avatars">
{#each receptionParticipants as participant (participant._id)}
<div
use:tooltip={{ label: getEmbeddedLabel(formatName(participant.name)) }}
on:click={getParticipantClickHandler(participant)}
>
<Avatar name={participant.name} size={'card'} person={$personByIdStore.get(participant.person)} />
</div>
{/each}
</div>
</div>
<RoomButton
label={getRoomName(reception, $personByIdStore)}
participants={receptionParticipants.map((p) => ({ ...p, onclick: getParticipantClickHandler(p) }))}
/>
{/if}
</div>
<style lang="scss">
.container {
padding: 0.125rem 0.125rem 0.125rem 0.5rem;
height: 1.625rem;
font-weight: 500;
background-color: var(--theme-button-pressed);
border: 1px solid transparent;
border-radius: 0.25rem;
cursor: pointer;
.label {
font-weight: 700;
color: var(--theme-caption-color);
}
&.main {
order: -3;
padding-right: 0.25rem;
& + .divider {
order: -2;
}
}
&:hover {
background-color: var(--theme-button-hovered);
border-color: var(--theme-navpanel-divider);
}
&.active {
order: -1;
position: relative;
display: flex;
align-items: center;
padding: 0.125rem 0.125rem 0.125rem 0.5rem;
background-color: var(--highlight-select);
border-color: var(--highlight-select-border);
&:hover {
background-color: var(--highlight-select-hover);
}
}
}
.avatars {
gap: 0.125rem;
}
.divider {
height: 1.5rem;
border: 1px solid var(--theme-divider-color);

View File

@ -0,0 +1,42 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { ParticipantInfo } from '@hcengineering/love'
import { Scroller } from '@hcengineering/ui'
export let items: (ParticipantInfo & { onclick?: (e: MouseEvent) => void })[]
</script>
<Scroller padding={'.25rem'} gap={'flex-gap-2'}>
{#each items as participant (participant.person)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex-row-center flex-no-shrink flex-gap-2"
class:cursor-pointer={participant.onclick !== undefined}
on:click={(e) => participant.onclick?.(e)}
>
<div class="min-w-6">
<Avatar
name={$personByIdStore.get(participant.person)?.name ?? participant.name}
size={items.length < 10 ? 'small' : 'card'}
person={$personByIdStore.get(participant.person)}
/>
</div>
{participant.name}
</div>
{/each}
</Scroller>

View File

@ -0,0 +1,76 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { tooltip, deviceOptionsStore as deviceInfo, checkAdaptiveMatching } from '@hcengineering/ui'
import { ParticipantInfo } from '@hcengineering/love'
import ParticipantsList from './ParticipantsList.svelte'
export let label: string
export let participants: (ParticipantInfo & { onclick?: (e: MouseEvent) => void })[]
export let active: boolean = false
export let limit: number = 4
$: overLimit = participants.length > limit
$: adaptive = checkAdaptiveMatching($deviceInfo.size, 'md') || overLimit
</script>
{#if adaptive}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="hulyStatusBarButton"
class:active
use:tooltip={{ component: ParticipantsList, props: { items: participants }, direction: 'bottom' }}
on:click
>
<span class="hulyStatusBarButton-label">{label}</span>
<div class="hulyCombineAvatars-container">
{#each participants.slice(0, limit) as participant, i (participant._id)}
<div
class="hulyCombineAvatar tiny"
data-over={i === limit - 1 && overLimit ? `+${participants.length - limit + 1}` : undefined}
>
<Avatar
name={$personByIdStore.get(participant.person)?.name ?? participant.name}
size={'card'}
person={$personByIdStore.get(participant.person)}
/>
</div>
{/each}
</div>
</div>
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="hulyStatusBarButton" class:active on:click>
<span class="hulyStatusBarButton-label">{label}</span>
<div class="hulyStatusBarButton-icons">
{#each participants as participant (participant._id)}
<div
use:tooltip={{ label: getEmbeddedLabel(participant.name), direction: 'bottom' }}
on:click={participant.onclick}
>
<Avatar
name={$personByIdStore.get(participant.person)?.name ?? participant.name}
size={'card'}
person={$personByIdStore.get(participant.person)}
/>
</div>
{/each}
</div>
</div>
{/if}