Updated mobile layout: collapsing WorkbenchTabs, floating aSide. UI fixes. (#7066)

Signed-off-by: Alexander Platov <alexander.platov@hardcoreeng.com>
This commit is contained in:
Alexander Platov 2024-10-30 19:19:04 +03:00 committed by GitHub
parent e4bf53ad1b
commit 11a6236712
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 349 additions and 181 deletions

View File

@ -501,6 +501,7 @@ input.search {
.ml-8 { margin-left: 2rem; } .ml-8 { margin-left: 2rem; }
.ml-10 { margin-left: 2.5rem; } .ml-10 { margin-left: 2.5rem; }
.ml-12 { margin-left: 3rem; } .ml-12 { margin-left: 3rem; }
.ml-14 { margin-left: 3.5rem; }
.ml-22 { margin-left: 5.5rem; } .ml-22 { margin-left: 5.5rem; }
.ml-auto { margin-left: auto; } .ml-auto { margin-left: auto; }
.mr-0-5 { margin-right: .125rem; } .mr-0-5 { margin-right: .125rem; }
@ -744,6 +745,7 @@ input.search {
.min-h-16 { min-height: 4rem; } .min-h-16 { min-height: 4rem; }
.min-h-30 { min-height: 7.5rem; } .min-h-30 { min-height: 7.5rem; }
.min-h-60 { min-height: 15rem; } .min-h-60 { min-height: 15rem; }
.max-w-0 { max-width: 0; }
.max-w-2 { max-width: .5rem; } .max-w-2 { max-width: .5rem; }
.max-w-4 { max-width: 1rem; } .max-w-4 { max-width: 1rem; }
.max-w-9 { max-width: 2.25rem; } .max-w-9 { max-width: 2.25rem; }

View File

@ -141,23 +141,40 @@
} }
.antiPanel-navigator { .antiPanel-navigator {
position: relative; position: relative;
min-width: 12.5rem;
max-width: 22.5rem;
width: 17.5rem;
background-color: var(--theme-navpanel-color); background-color: var(--theme-navpanel-color);
&:not(.right) {
min-width: 12.5rem;
max-width: 22.5rem;
width: 17.5rem;
}
&.right {
overflow: hidden;
border-radius: var(--medium-BorderRadius) 0 0 var(--medium-BorderRadius);
}
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
.antiPanel-navigator { .antiPanel-navigator {
position: fixed; position: fixed;
top: calc(var(--status-bar-height) + 1px);
height: calc(100% - var(--status-bar-height) - 1px);
background-color: var(--theme-navpanel-color); background-color: var(--theme-navpanel-color);
filter: drop-shadow(2px 0 1px rgba(0, 0, 0, .2));
z-index: 450; z-index: 450;
&:not(.right) {
top: calc(var(--status-bar-height) + 1px);
height: calc(100% - var(--status-bar-height) - 1px);
filter: drop-shadow(2px 0 5px rgba(0, 0, 0, .2));
&.portrait { left: 0; } &.portrait { left: 0; }
&.landscape { left: var(--app-panel-width); } &.landscape { left: var(--app-panel-width); }
}
&.right {
top: var(--status-bar-height);
right: 0;
height: calc(100% - var(--status-bar-height) - 1px);
background-color: var(--theme-statusbar-color);
filter: drop-shadow(-2px 0 5px rgba(0, 0, 0, .2));
}
} }
} }
.antiPanel-component { .antiPanel-component {

View File

@ -78,6 +78,7 @@
scrollbar-width: none; scrollbar-width: none;
--body-font-size: .875rem; --body-font-size: .875rem;
--status-bar-height: 36px; --status-bar-height: 36px;
--status-bar-normal-height: 36px;
--panel-aside-width: 25rem; // 20rem; --panel-aside-width: 25rem; // 20rem;
--font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto; --font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
--mono-font: 'IBM Plex Mono', monospace; --mono-font: 'IBM Plex Mono', monospace;
@ -89,6 +90,10 @@
&::after, &::after,
&::before { box-sizing: border-box; } &::before { box-sizing: border-box; }
@media (max-width: 480px) {
--status-bar-height: 70px;
}
} }
:root { :root {
--app-height: 100%; --app-height: 100%;

View File

@ -36,7 +36,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class="container main flex-row-center flex-gap-2 {orientation} {kind}" class="container main flex-between flex-gap-2 {orientation} {kind}"
style:max-width={orientation === 'horizontal' ? maxSize : 'auto'} style:max-width={orientation === 'horizontal' ? maxSize : 'auto'}
style:max-height={orientation === 'vertical' ? maxSize : 'auto'} style:max-height={orientation === 'vertical' ? maxSize : 'auto'}
class:active={highlighted} class:active={highlighted}
@ -52,7 +52,7 @@
</div> </div>
{/if} {/if}
<span class="overflow-label"> <span class="overflow-label flex-grow">
{#if label} {#if label}
{label} {label}
{:else if labelIntl} {:else if labelIntl}
@ -100,11 +100,14 @@
&.horizontal { &.horizontal {
padding: 0.125rem 0.125rem 0.125rem 0.5rem; padding: 0.125rem 0.125rem 0.125rem 0.5rem;
height: 1.625rem; height: 1.625rem;
min-height: 1.625rem;
min-width: 6rem;
} }
&.vertical { &.vertical {
padding: 0.5rem 0.125rem 0.125rem 0.125rem; padding: 0.5rem 0.125rem 0.125rem 0.125rem;
width: 1.625rem; width: 1.625rem;
min-height: 6rem;
writing-mode: vertical-rl; writing-mode: vertical-rl;
text-orientation: sideways; text-orientation: sideways;
} }

View File

@ -168,7 +168,7 @@
<Header <Header
type={'type-panel'} type={'type-panel'}
noPrint={!printHeader} noPrint={!printHeader}
{adaptive} adaptive={$deviceInfo.isMobile ? 'disabled' : adaptive}
{hideBefore} {hideBefore}
{hideSearch} {hideSearch}
{hideActions} {hideActions}
@ -261,21 +261,21 @@
</Header> </Header>
<div class="popupPanel-body {$deviceInfo.isMobile ? 'mobile' : 'main'}" class:asideShown> <div class="popupPanel-body {$deviceInfo.isMobile ? 'mobile' : 'main'}" class:asideShown>
{#if $deviceInfo.isMobile} {#if $deviceInfo.isMobile}
<Scroller horizontal padding={'.5rem .75rem'}> <div
<div class="popupPanel-body__mobile"
class="popupPanel-body__mobile" use:resizeObserver={(element) => {
use:resizeObserver={(element) => { innerWidth = element.clientWidth
innerWidth = element.clientWidth }}
}} >
> <Scroller horizontal padding={'.5rem .75rem'}>
{#if $$slots.header && isHeader} {#if $$slots.header && isHeader}
<div class="popupPanel-body__header mobile bottom-divider" class:max={useMaxWidth}> <div class="popupPanel-body__header mobile bottom-divider" class:max={useMaxWidth}>
<slot name="header" /> <slot name="header" />
</div> </div>
{/if} {/if}
<slot /> <slot />
</div> </Scroller>
</Scroller> </div>
{:else} {:else}
<div <div
class="popupPanel-body__main" class="popupPanel-body__main"

View File

@ -14,10 +14,12 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte' import { onMount } from 'svelte'
import { resizeObserver } from '..'
export let scroller: HTMLElement export let scroller: HTMLElement
export let gap: 'none' | 'small' | 'big' = 'small' export let gap: 'none' | 'small' | 'big' = 'small'
export let padding: string | undefined = undefined
let divBar: HTMLElement let divBar: HTMLElement
let isScrolling: boolean = false let isScrolling: boolean = false
@ -31,7 +33,7 @@
$: stepStyle = gap === 'small' ? 'gap-1' : gap === 'big' ? 'gap-2' : '' $: stepStyle = gap === 'small' ? 'gap-1' : gap === 'big' ? 'gap-2' : ''
const checkBar = (): void => { const checkBar = (): void => {
if (divBar && scroller) { if (divBar !== undefined && scroller !== undefined) {
const trackW = scroller.clientWidth const trackW = scroller.clientWidth
const scrollW = scroller.scrollWidth const scrollW = scroller.scrollWidth
const proc = scrollW / trackW const proc = scrollW / trackW
@ -40,13 +42,13 @@
if (mask === 'none') divBar.style.visibility = 'hidden' if (mask === 'none') divBar.style.visibility = 'hidden'
else { else {
divBar.style.visibility = 'visible' divBar.style.visibility = 'visible'
if (divBar) { if (divBar !== undefined) {
if (timer) { if (timer != null) {
clearTimeout(timer) clearTimeout(timer)
divBar.style.opacity = '1' divBar.style.opacity = '1'
} }
timer = setTimeout(() => { timer = setTimeout(() => {
if (divBar) divBar.style.opacity = '0' if (divBar != null) divBar.style.opacity = '0'
}, 2000) }, 2000)
} }
} }
@ -54,8 +56,8 @@
} }
} }
const onScroll = (event: MouseEvent): void => { const onScroll = (event: PointerEvent): void => {
if (isScrolling && divBar && scroller) { if (isScrolling && divBar !== undefined && scroller !== undefined) {
const rectScroll = scroller.getBoundingClientRect() const rectScroll = scroller.getBoundingClientRect()
let X = event.clientX - dX let X = event.clientX - dX
if (X < rectScroll.left) X = rectScroll.left if (X < rectScroll.left) X = rectScroll.left
@ -67,22 +69,22 @@
scroller.scrollLeft = (scroller.scrollWidth - scroller.clientWidth) * procBar scroller.scrollLeft = (scroller.scrollWidth - scroller.clientWidth) * procBar
} }
} }
const onScrollEnd = (event: MouseEvent): void => { const onScrollEnd = (event: PointerEvent): void => {
const el: HTMLElement = event.currentTarget as HTMLElement const el: HTMLElement = event.currentTarget as HTMLElement
if (el && isScrolling) { if (el !== undefined && isScrolling) {
document.removeEventListener('mousemove', onScroll) document.removeEventListener('pointermove', onScroll)
document.body.style.userSelect = 'auto' document.body.style.userSelect = 'auto'
document.body.style.webkitUserSelect = 'auto' document.body.style.webkitUserSelect = 'auto'
} }
document.removeEventListener('mouseup', onScrollEnd) document.removeEventListener('pointerup', onScrollEnd)
isScrolling = false isScrolling = false
} }
const onScrollStart = (event: MouseEvent): void => { const onScrollStart = (event: PointerEvent): void => {
const el: HTMLElement = event.currentTarget as HTMLElement const el: HTMLElement = event.currentTarget as HTMLElement
if (el && scroller) { if (el !== undefined && scroller !== undefined) {
dX = event.clientX - el.getBoundingClientRect().x dX = event.clientX - el.getBoundingClientRect().x
document.addEventListener('mouseup', onScrollEnd) document.addEventListener('pointerup', onScrollEnd)
document.addEventListener('mousemove', onScroll) document.addEventListener('pointermove', onScroll)
document.body.style.userSelect = 'none' document.body.style.userSelect = 'none'
document.body.style.webkitUserSelect = 'none' document.body.style.webkitUserSelect = 'none'
isScrolling = true isScrolling = true
@ -90,8 +92,8 @@
} }
const checkMask = (): void => { const checkMask = (): void => {
maskLeft = !!(scroller && scroller.scrollLeft > 1) maskLeft = scroller !== undefined && scroller.scrollLeft > 1
maskRight = !!(scroller && scroller.scrollWidth - scroller.scrollLeft - scroller.clientWidth > 1) maskRight = scroller !== undefined && scroller.scrollWidth - scroller.scrollLeft - scroller.clientWidth > 1
if (maskLeft || maskRight) { if (maskLeft || maskRight) {
if (maskLeft && maskRight) mask = 'both' if (maskLeft && maskRight) mask = 'both'
else if (maskLeft) mask = 'left' else if (maskLeft) mask = 'left'
@ -102,30 +104,12 @@
} }
onMount(() => { onMount(() => {
if (scroller) { if (scroller !== undefined) checkMask()
const observer = new IntersectionObserver(
() => {
checkMask()
},
{ root: null, threshold: 0.1 }
)
const tempEl = scroller.querySelector('*') as HTMLElement
if (tempEl) observer.observe(tempEl)
checkMask()
scroller.addEventListener('scroll', checkMask)
}
}) })
onDestroy(() => {
if (scroller) scroller.removeEventListener('scroll', checkMask)
})
const _resize = (): void => {
checkMask()
}
</script> </script>
<svelte:window on:resize={_resize} /> <div class="scrollerbar-container" use:resizeObserver={checkMask}>
<div class="scrollerbar-container"> <div bind:this={scroller} class="antiStatesBar mask-{mask} {stepStyle}" style:padding on:scroll={checkMask}>
<div bind:this={scroller} class="antiStatesBar mask-{mask} {stepStyle}">
<slot /> <slot />
</div> </div>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
@ -133,8 +117,8 @@
class="bar" class="bar"
class:hovered={isScrolling} class:hovered={isScrolling}
bind:this={divBar} bind:this={divBar}
on:mousedown={onScrollStart} on:pointerdown={onScrollStart}
on:mouseleave={checkMask} on:pointerleave={checkMask}
/> />
<div class="track" class:hovered={isScrolling} /> <div class="track" class:hovered={isScrolling} />
</div> </div>

View File

@ -16,7 +16,7 @@
<script lang="ts"> <script lang="ts">
export let size: 'x-small' | 'small' | 'medium' | 'large' export let size: 'x-small' | 'small' | 'medium' | 'large'
export let filled: boolean = false export let filled: boolean = false
const fill: string = 'currentColor' export let fill: string = 'currentColor'
</script> </script>
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> <svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">

View File

@ -15,7 +15,7 @@
--> -->
<script lang="ts"> <script lang="ts">
export let size: 'x-small' | 'small' | 'medium' | 'large' export let size: 'x-small' | 'small' | 'medium' | 'large'
const fill: string = 'currentColor' export let fill: string = 'currentColor'
</script> </script>
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> <svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
export let size: 'small' | 'medium' | 'large' export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor' export let fill: string = 'currentColor'
</script> </script>
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> <svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">

View File

@ -5,7 +5,16 @@
import { deviceSizes, deviceWidths } from '../../types' import { deviceSizes, deviceWidths } from '../../types'
// import { applicationShortcutKey } from '../../utils' // import { applicationShortcutKey } from '../../utils'
import { Theme } from '@hcengineering/theme' import { Theme } from '@hcengineering/theme'
import { IconArrowLeft, IconArrowRight, checkMobile, deviceOptionsStore as deviceInfo } from '../../' import {
IconArrowLeft,
IconArrowRight,
checkMobile,
deviceOptionsStore as deviceInfo,
checkAdaptiveMatching,
ButtonIcon,
IconDetailsFilled,
IconDetails
} from '../../'
import { desktopPlatform, getCurrentLocation, location, locationStorageKeyId, navigate } from '../../location' import { desktopPlatform, getCurrentLocation, location, locationStorageKeyId, navigate } from '../../location'
import uiPlugin from '../../plugin' import uiPlugin from '../../plugin'
import Component from '../Component.svelte' import Component from '../Component.svelte'
@ -129,6 +138,14 @@
} }
} }
updateDeviceSize() updateDeviceSize()
$: secondRow = checkAdaptiveMatching($deviceInfo.size, 'xs')
$: asideFloat = $deviceInfo.navigator.float
$: asideOpen = $deviceInfo.aside.visible
$: appsMini =
$deviceInfo.isMobile &&
(($deviceInfo.isPortrait && $deviceInfo.docWidth <= 480) ||
(!$deviceInfo.isPortrait && $deviceInfo.docHeight <= 480))
</script> </script>
<svelte:window bind:innerWidth={docWidth} bind:innerHeight={docHeight} /> <svelte:window bind:innerWidth={docWidth} bind:innerHeight={docHeight} />
@ -161,9 +178,15 @@
</button> </button>
</div> </div>
{/if} {/if}
<div class="flex-row-center left-items flex-gap-0-5" style:-webkit-app-region={'no-drag'}> {#if !secondRow}
<RootBarExtension position="left" /> <div
</div> class="flex-row-center left-items flex-gap-0-5"
class:ml-14={appsMini}
style:-webkit-app-region={'no-drag'}
>
<RootBarExtension position="left" />
</div>
{/if}
<div <div
class="flex-row-center justify-center status-info" class="flex-row-center justify-center status-info"
style:margin-left={(isPortrait && docWidth <= 480) || (!isPortrait && docHeight <= 480) ? '1.5rem' : '0'} style:margin-left={(isPortrait && docWidth <= 480) || (!isPortrait && docHeight <= 480) ? '1.5rem' : '0'}
@ -177,17 +200,54 @@
{/if} {/if}
</div> </div>
<div class="flex-row-reverse" style:-webkit-app-region={'no-drag'}> <div class="flex-row-reverse" style:-webkit-app-region={'no-drag'}>
{#if asideFloat && !secondRow}
<div class="antiHSpacer x2" />
<ButtonIcon
icon={asideOpen ? IconDetailsFilled : IconDetails}
iconProps={{ fill: 'var(--theme-dark-color)' }}
kind={'tertiary'}
size={'extra-small'}
hasMenu
pressed={$deviceInfo.aside.visible}
on:click={() => {
$deviceInfo.aside.visible = !$deviceInfo.aside.visible
}}
/>
{/if}
<div class="clock"> <div class="clock">
<Clock /> <Clock />
</div> </div>
<div class="flex-row-center gap-statusbar"> <div class="flex-row-center gap-statusbar">
<RootBarExtension position="right" /> {#if !secondRow}
<RootBarExtension position="right" />
{/if}
<FontSizeSelector /> <FontSizeSelector />
<ThemeSelector /> <ThemeSelector />
<LangSelector /> <LangSelector />
</div> </div>
</div> </div>
</div> </div>
{#if secondRow}
<div class="flex-between h-full content-color gap-3 px-2 second-row" style:-webkit-app-region={'no-drag'}>
<div class="flex-row-center flex-gap-0-5">
<RootBarExtension position="left" />
</div>
<div class="flex-row-center flex-gap-0-5">
<RootBarExtension position="right" />
<ButtonIcon
icon={asideOpen ? IconDetailsFilled : IconDetails}
iconProps={{ fill: 'var(--theme-dark-color)' }}
kind={'tertiary'}
size={'extra-small'}
hasMenu
pressed={$deviceInfo.aside.visible}
on:click={() => {
$deviceInfo.aside.visible = !$deviceInfo.aside.visible
}}
/>
</div>
</div>
{/if}
</div> </div>
<div class="app"> <div class="app">
{#if application} {#if application}
@ -213,6 +273,7 @@
.antiStatusBar { .antiStatusBar {
-webkit-app-region: drag; -webkit-app-region: drag;
min-width: 0;
min-height: var(--status-bar-height); min-height: var(--status-bar-height);
height: var(--status-bar-height); height: var(--status-bar-height);
// min-width: 600px; // min-width: 600px;
@ -248,6 +309,21 @@
margin: 0 12px 0 8px; margin: 0 12px 0 8px;
} }
.second-row {
display: none;
}
@media (max-width: 480px) {
display: flex;
flex-direction: column;
gap: 2px;
padding: 2px 0;
width: 100%;
.second-row {
display: flex;
}
}
@media print { @media print {
display: none; display: none;
} }

View File

@ -17,7 +17,7 @@
{#each sorted as ext (ext[1].id)} {#each sorted as ext (ext[1].id)}
{#if ext[0] === position} {#if ext[0] === position}
<div id={ext[1].id} style:margin-right={'1px'}> <div id={ext[1].id} class="clear-mins" style:margin-right={'1px'}>
<Component <Component
is={ext[1].component} is={ext[1].component}
props={ext[1].props} props={ext[1].props}

View File

@ -311,6 +311,7 @@ export const deviceOptionsStore = writable<DeviceOptions>({
isPortrait: false, isPortrait: false,
isMobile: false, isMobile: false,
navigator: { visible: true, float: false, direction: 'vertical' }, navigator: { visible: true, float: false, direction: 'vertical' },
aside: { visible: true },
fontSize: 0, fontSize: 0,
size: null, size: null,
sizes: { xs: false, sm: false, md: false, lg: false, xl: false, xxl: false }, sizes: { xs: false, sm: false, md: false, lg: false, xl: false, xxl: false },

View File

@ -166,7 +166,7 @@ export const settingsSeparators: DefSeparators = [
export const mainSeparators: DefSeparators = [ export const mainSeparators: DefSeparators = [
{ minSize: 30, size: 'auto', maxSize: 'auto' }, { minSize: 30, size: 'auto', maxSize: 'auto' },
{ minSize: 20, size: 30, maxSize: 80, float: 'sidebar' } { minSize: 25, size: 30, maxSize: 80, float: 'sidebar' }
] ]
export const secondNavSeparators: DefSeparators = [{ minSize: 7, size: 7.5, maxSize: 15, float: 'navigator' }, null] export const secondNavSeparators: DefSeparators = [{ minSize: 7, size: 7.5, maxSize: 15, float: 'navigator' }, null]

View File

@ -378,6 +378,7 @@ export interface DeviceOptions {
isPortrait: boolean isPortrait: boolean
isMobile: boolean isMobile: boolean
navigator: { visible: boolean, float: boolean, direction: 'vertical' | 'horizontal' } navigator: { visible: boolean, float: boolean, direction: 'vertical' | 'horizontal' }
aside: { visible: boolean }
fontSize: number fontSize: number
size: WidthType | null size: WidthType | null
sizes: Record<WidthType, boolean> sizes: Record<WidthType, boolean>

View File

@ -5,7 +5,8 @@ import {
getLocation, getLocation,
type Location, type Location,
navigate, navigate,
languageStore languageStore,
deviceOptionsStore as deviceInfo
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { type Ref, type Doc, type Class, generateId } from '@hcengineering/core' import { type Ref, type Doc, type Class, generateId } from '@hcengineering/core'
import activity, { type ActivityMessage } from '@hcengineering/activity' import activity, { type ActivityMessage } from '@hcengineering/activity'
@ -179,6 +180,10 @@ export async function replyToThread (message: ActivityMessage, e: Event): Promis
const fromSidebar = isElementFromSidebar(e.target as HTMLElement) const fromSidebar = isElementFromSidebar(e.target as HTMLElement)
const loc = getCurrentLocation() const loc = getCurrentLocation()
const dev = get(deviceInfo)
dev.aside.visible = true
deviceInfo.set(dev)
threadMessagesStore.set(message) threadMessagesStore.set(message)
if (fromSidebar) { if (fromSidebar) {

View File

@ -23,7 +23,7 @@
export let targetEmp: Person export let targetEmp: Person
export let key: string export let key: string
export let onChange: (key: string, value: boolean) => void export let onChange: (key: string, value: boolean) => void
export let selected = false export let selected: boolean = false
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()

View File

@ -16,13 +16,14 @@
import { Person } from '@hcengineering/contact' import { Person } from '@hcengineering/contact'
import { Doc, Mixin, Ref } from '@hcengineering/core' import { Doc, Mixin, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { CheckBox, Label } from '@hcengineering/ui' import { Label, RadioButton } from '@hcengineering/ui'
import { FixedColumn } from '@hcengineering/view-resources'
export let value: Person export let value: Person
export let targetEmp: Person export let targetEmp: Person
export let cast: Ref<Mixin<Doc>> | undefined = undefined export let cast: Ref<Mixin<Doc>> | undefined = undefined
export let key: string export let key: string
export let selected = false export let selected: boolean = false
export let onChange: (key: string, value: boolean) => void export let onChange: (key: string, value: boolean) => void
const client = getClient() const client = getClient()
@ -38,51 +39,51 @@
return (value as any)[key] === (targetEmp as any)[key] return (value as any)[key] === (targetEmp as any)[key]
} }
$: attribute = hierarchy.findAttribute(cast ?? value._class, key) $: attribute = hierarchy.findAttribute(cast ?? value._class, key)
const change = (sel: boolean): void => {
selected = sel
onChange(key, sel)
}
</script> </script>
{#if !isEqual(value, targetEmp, key)} {#if !isEqual(value, targetEmp, key)}
<div class="box flex-row-center flex-between"> <div class="box flex-row-center flex-gap-4 flex-grow">
<div class="ml-4"> <FixedColumn key={'mergeLabel'} addClass={'ml-4'}>
{#if attribute?.label} {#if attribute?.label}
<Label label={attribute.label} /> <Label label={attribute.label} />
{:else} {:else}
{key} {key}
{/if} {/if}
</div> </FixedColumn>
<div class="flex-center"> <FixedColumn key={'mergeFirst'} addClass="flex-row-center flex-gap-2 cursor-pointer">
<div class="mr-2"> <RadioButton
<CheckBox bind:group={selected}
circle value={false}
checked={selected} action={() => {
on:value={(e) => { if (selected) change(false)
selected = false }}
onChange(key, false) >
}} <slot name="item" item={value} />
/> </RadioButton>
</div> </FixedColumn>
<slot name="item" item={value} />
</div> <FixedColumn key={'mergeSecond'} addClass="flex-row-center flex-gap-2 cursor-pointer">
<div class="flex-row-center" /> <RadioButton
<div class="flex-center"> bind:group={selected}
<div class="mr-2"> value={true}
<CheckBox action={() => {
circle if (!selected) change(true)
checked={!selected} }}
on:value={(e) => { >
selected = true <slot name="item" item={targetEmp} />
onChange(key, true) </RadioButton>
}} </FixedColumn>
/>
</div>
<slot name="item" item={targetEmp} />
</div>
</div> </div>
{/if} {/if}
<style lang="scss"> <style lang="scss">
.box { .box {
margin: 0.5rem; margin: 0.5rem 0;
padding: 0.5rem; padding: 0.5rem;
flex-shrink: 0; flex-shrink: 0;
border: 1px dashed var(--accent-color); border: 1px dashed var(--accent-color);

View File

@ -225,7 +225,7 @@
function selectMixin (mixin: Ref<Mixin<Doc>>, field: string, targetValue: boolean) { function selectMixin (mixin: Ref<Mixin<Doc>>, field: string, targetValue: boolean) {
const upd = mixinUpdate[mixin] ?? {} const upd = mixinUpdate[mixin] ?? {}
if (!targetValue) { if (!targetValue) {
;(upd as any)[field] = (value as any)[field] ;(upd as any)[field] = (sourcePerson as any)[mixin][field]
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (upd as any)[field] delete (upd as any)[field]
@ -354,7 +354,7 @@
shape={'circle'} shape={'circle'}
/> />
</div> </div>
>> <span class="mx-4">&gt;&gt;</span>
<div class="flex-row-center"> <div class="flex-row-center">
<UserBox <UserBox
_class={contact.class.Person} _class={contact.class.Person}
@ -387,13 +387,7 @@
<Avatar person={item} size={'x-large'} icon={contact.icon.Person} name={item.name} /> <Avatar person={item} size={'x-large'} icon={contact.icon.Person} name={item.name} />
</svelte:fragment> </svelte:fragment>
</MergeComparer> </MergeComparer>
<MergeComparer <MergeComparer key="name" value={sourcePerson} targetEmp={targetPerson} onChange={select} selected>
key="name"
value={sourcePerson}
targetEmp={targetPerson}
onChange={select}
selected={update.name !== undefined}
>
<svelte:fragment slot="item" let:item> <svelte:fragment slot="item" let:item>
{getName(client.getHierarchy(), item)} {getName(client.getHierarchy(), item)}
</svelte:fragment> </svelte:fragment>
@ -405,7 +399,7 @@
targetEmp={targetPerson} targetEmp={targetPerson}
onChange={select} onChange={select}
_class={contact.mixin.Employee} _class={contact.mixin.Employee}
selected={toAny(update)[attribute[0]] !== undefined} selected
/> />
{/each} {/each}
{#each mixins as mixin} {#each mixins as mixin}
@ -419,7 +413,7 @@
selectMixin(mixin, key, value) selectMixin(mixin, key, value)
}} }}
_class={mixin} _class={mixin}
selected={toAny(mixinUpdate)?.[mixin]?.[attribute] !== undefined} selected
/> />
{/each} {/each}
{/each} {/each}

View File

@ -48,4 +48,4 @@
}) })
</script> </script>
<div bind:this={parentElement}></div> <div bind:this={parentElement} class="hidden"></div>

View File

@ -17,8 +17,10 @@
import { uploads } from '../store' import { uploads } from '../store'
</script> </script>
<div class="flex-row-center flex-gap-2"> {#if $uploads.length > 0}
{#each $uploads as upload} <div class="flex-row-center flex-gap-2">
<FileUploadStatusBar {upload} /> {#each $uploads as upload}
{/each} <FileUploadStatusBar {upload} />
</div> {/each}
</div>
{/if}

View File

@ -7,8 +7,8 @@
let parentElement: HTMLDivElement let parentElement: HTMLDivElement
onMount(() => { onMount(() => {
pushRootBarComponent('right', uploader.component.FileUploadExt) pushRootBarComponent('right', uploader.component.FileUploadExt, 10)
}) })
</script> </script>
<div bind:this={parentElement}></div> <div bind:this={parentElement} class="hidden"></div>

View File

@ -1,3 +1,4 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- <!--
// Copyright © 2020, 2021 Anticrm Platform Contributors. // Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc. // Copyright © 2021 Hardcore Engineering Inc.
@ -43,11 +44,13 @@
} }
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div <div
class="flex-no-shrink{addClass ? ` ${addClass}` : ''}" class="flex-no-shrink{addClass ? ` ${addClass}` : ''}"
style:text-align={justify !== '' ? justify : ''} style:text-align={justify !== '' ? justify : ''}
style:min-width={`${$fixedWidthStore[key] ?? 0}px`} style:min-width={`${$fixedWidthStore[key] ?? 0}px`}
use:resizeObserver={resize} use:resizeObserver={resize}
on:click
> >
<slot /> <slot />
</div> </div>

View File

@ -69,8 +69,8 @@
} }
&.small, &.small,
&.small .icon-container { &.small .icon-container {
width: calc(var(--status-bar-height) - 8px); width: calc(var(--status-bar-normal-height) - 8px);
height: calc(var(--status-bar-height) - 8px); height: calc(var(--status-bar-normal-height) - 8px);
border-radius: 0.25rem; border-radius: 0.25rem;
} }
&.small.selected { &.small.selected {

View File

@ -54,8 +54,8 @@
height: 2rem; height: 2rem;
} }
&.mini { &.mini {
width: 1.5rem; width: 1.75rem;
height: 1.5rem; height: 1.75rem;
} }
&.red { &.red {
background-color: rgb(246, 105, 77); background-color: rgb(246, 105, 77);

View File

@ -158,6 +158,7 @@
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {}) const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
$deviceInfo.navigator.visible = getMetadata(workbench.metadata.NavigationExpandedDefault) ?? true $deviceInfo.navigator.visible = getMetadata(workbench.metadata.NavigationExpandedDefault) ?? true
$deviceInfo.aside.visible = getMetadata(workbench.metadata.NavigationExpandedDefault) ?? true
async function toggleNav (): Promise<void> { async function toggleNav (): Promise<void> {
$deviceInfo.navigator.visible = !$deviceInfo.navigator.visible $deviceInfo.navigator.visible = !$deviceInfo.navigator.visible
@ -635,10 +636,12 @@
$: if ($deviceInfo.docWidth <= 1024 && !$deviceInfo.navigator.float) { $: if ($deviceInfo.docWidth <= 1024 && !$deviceInfo.navigator.float) {
$deviceInfo.navigator.visible = false $deviceInfo.navigator.visible = false
$deviceInfo.navigator.float = true $deviceInfo.navigator.float = true
$deviceInfo.aside.visible = false
} else if ($deviceInfo.docWidth > 1024 && $deviceInfo.navigator.float) { } else if ($deviceInfo.docWidth > 1024 && $deviceInfo.navigator.float) {
if (getMetadata(workbench.metadata.NavigationExpandedDefault) === undefined) { if (getMetadata(workbench.metadata.NavigationExpandedDefault) === undefined) {
$deviceInfo.navigator.float = false $deviceInfo.navigator.float = false
$deviceInfo.navigator.visible = true $deviceInfo.navigator.visible = true
$deviceInfo.aside.visible = true
} }
} }
const checkOnHide = (): void => { const checkOnHide = (): void => {
@ -975,13 +978,28 @@
</div> </div>
{/if} {/if}
</div> </div>
{#if $sidebarStore.variant === SidebarVariant.EXPANDED} {#if !$deviceInfo.navigator.float}
<Separator name={'main'} index={0} color={'transparent'} separatorSize={0} short /> {#if $sidebarStore.variant === SidebarVariant.EXPANDED}
<Separator name={'main'} index={0} color={'transparent'} separatorSize={0} short />
{/if}
<WidgetsBar />
{/if} {/if}
<WidgetsBar />
</div> </div>
<Dock /> <Dock />
<div bind:this={cover} class="cover" /> <div bind:this={cover} class="cover" />
{#if $deviceInfo.navigator.float}
<div
class="antiPanel-navigator right no-print {$deviceInfo.navigator.direction === 'horizontal'
? 'portrait'
: 'landscape'}"
style:display={$deviceInfo.aside.visible ? 'flex' : 'none'}
>
<Separator name={'main'} index={0} color={'transparent'} separatorSize={0} short float={'sidebar'} />
<div class="antiPanel-wrap__content hulyNavPanel-container">
<WidgetsBar />
</div>
</div>
{/if}
<TooltipInstance /> <TooltipInstance />
<PanelInstance bind:this={panelInstance} contentPanel={elementPanel}> <PanelInstance bind:this={panelInstance} contentPanel={elementPanel}>
<svelte:fragment slot="panel-header"> <svelte:fragment slot="panel-header">
@ -993,7 +1011,9 @@
<ActionContext context={{ mode: 'popup' }} /> <ActionContext context={{ mode: 'popup' }} />
</svelte:fragment> </svelte:fragment>
</Popup> </Popup>
<ComponentExtensions extension={workbench.extensions.WorkbenchExtensions} /> <div class="hidden max-w-0 max-h-0">
<ComponentExtensions extension={workbench.extensions.WorkbenchExtensions} />
</div>
<BrowserNotificatator /> <BrowserNotificatator />
{/if} {/if}
@ -1071,11 +1091,11 @@
} }
.logo-container.mini { .logo-container.mini {
left: 4px; left: 4px;
width: 1.5rem; width: 1.75rem;
height: 1.5rem; height: 1.75rem;
} }
.topmenu-container.mini { .topmenu-container.mini {
left: calc(1.5rem + 8px); left: calc(1.75rem + 8px);
} }
} }

View File

@ -13,26 +13,75 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { createTab, tabsStore } from '../workbench' import { createTab, tabsStore, tabIdStore } from '../workbench'
import { WorkbenchTabs } from '../index'
import WorkbenchTabPresenter from './WorkbenchTabPresenter.svelte' import WorkbenchTabPresenter from './WorkbenchTabPresenter.svelte'
import { IconAdd, ButtonIcon } from '@hcengineering/ui' import {
IconAdd,
IconMoreH,
ButtonIcon,
ScrollerBar,
deviceOptionsStore as deviceInfo,
checkAdaptiveMatching,
showPopup
} from '@hcengineering/ui'
export let popup: boolean = false
let scroller: HTMLElement
let element: HTMLButtonElement
let pressed: boolean = false
$: devSize = $deviceInfo.size
$: mini = checkAdaptiveMatching(devSize, 'md')
$: selectedTab = $tabsStore.find((ts) => ts._id === $tabIdStore)
const showTabs = (): void => {
pressed = true
showPopup(WorkbenchTabs, { popup: true }, element, () => {
pressed = false
})
}
</script> </script>
<div class="root flex-gap-1"> <div
{#each $tabsStore as tab (tab._id)} class={popup ? 'selectPopup' : 'flex-row-center flex-gap-2'}
<WorkbenchTabPresenter {tab} /> style:padding={popup ? '.5rem' : mini ? '.25rem .25rem .25rem 0' : '0 .25rem 0 0'}
{/each} >
<div class="ml-1-5 plus-button mr-1"> {#if popup}
<ButtonIcon icon={IconAdd} size="min" kind="tertiary" on:click={createTab} /> <div class="scroll">
</div> <div class="box flex-gap-1">
{#each $tabsStore.filter((ts) => ts._id !== $tabIdStore) as tab (tab._id)}
<WorkbenchTabPresenter {tab} />
{/each}
</div>
</div>
{:else if !mini}
<ScrollerBar bind:scroller padding={'.25rem 0'}>
{#each $tabsStore as tab (tab._id)}
<WorkbenchTabPresenter {tab} />
{/each}
</ScrollerBar>
{:else if selectedTab !== undefined}
<WorkbenchTabPresenter tab={selectedTab} />
<ButtonIcon
bind:element
icon={IconMoreH}
iconProps={{ fill: 'var(--theme-dark-color)' }}
size={'extra-small'}
kind={'tertiary'}
hasMenu
{pressed}
on:click={showTabs}
/>
{/if}
{#if !popup}
<ButtonIcon
icon={IconAdd}
iconProps={{ fill: 'var(--theme-dark-color)' }}
size={'extra-small'}
kind={'tertiary'}
on:click={createTab}
/>
{/if}
</div> </div>
<style>
.root {
display: flex;
align-items: center;
}
.plus-button {
height: 0.875rem;
}
</style>

View File

@ -17,7 +17,7 @@
import { WidgetPreference, SidebarEvent, TxSidebarEvent, OpenSidebarWidgetParams } from '@hcengineering/workbench' import { WidgetPreference, SidebarEvent, TxSidebarEvent, OpenSidebarWidgetParams } from '@hcengineering/workbench'
import { Tx } from '@hcengineering/core' import { Tx } from '@hcengineering/core'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { panelstore } from '@hcengineering/ui' import { panelstore, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import workbench from '../../plugin' import workbench from '../../plugin'
import { createWidgetTab, openWidget, sidebarStore, SidebarVariant } from '../../sidebar' import { createWidgetTab, openWidget, sidebarStore, SidebarVariant } from '../../sidebar'
@ -61,7 +61,12 @@
}) })
</script> </script>
<div class="antiPanel-application vertical root" class:mini id="sidebar"> <div
id="sidebar"
class="antiPanel-application vertical sidebar-container"
class:mini
class:float={$deviceInfo.navigator.float}
>
{#if mini} {#if mini}
<SidebarMini {widgets} {preferences} /> <SidebarMini {widgets} {preferences} />
{:else if $sidebarStore.variant === SidebarVariant.EXPANDED} {:else if $sidebarStore.variant === SidebarVariant.EXPANDED}
@ -70,15 +75,25 @@
</div> </div>
<style lang="scss"> <style lang="scss">
.root { .sidebar-container {
flex-direction: row; flex-direction: row;
min-width: 25rem; min-width: 25rem;
border-radius: 0 var(--medium-BorderRadius) var(--medium-BorderRadius) 0; border-radius: 0 var(--medium-BorderRadius) var(--medium-BorderRadius) 0;
&.mini { &.mini:not(.float) {
width: 3.5rem !important; width: 3.5rem !important;
min-width: 3.5rem !important; min-width: 3.5rem !important;
max-width: 3.5rem !important; max-width: 3.5rem !important;
} }
&.mini.float {
justify-content: flex-end;
}
}
@media (max-width: 1024px) {
.sidebar-container {
width: 100%;
border: 1px solid var(--theme-navpanel-divider);
border-radius: var(--medium-BorderRadius);
}
} }
</style> </style>

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Widget, WidgetPreference, WidgetType } from '@hcengineering/workbench' import { Widget, WidgetPreference, WidgetType } from '@hcengineering/workbench'
import { IconSettings, ModernButton, showPopup } from '@hcengineering/ui' import { IconSettings, ModernButton, showPopup, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import { Ref } from '@hcengineering/core' import { Ref } from '@hcengineering/core'
import WidgetPresenter from './/WidgetPresenter.svelte' import WidgetPresenter from './/WidgetPresenter.svelte'
@ -31,7 +31,8 @@
function handleSelectWidget (widget: Widget): void { function handleSelectWidget (widget: Widget): void {
if (selected === widget._id) { if (selected === widget._id) {
minimizeSidebar(true) if ($deviceInfo.navigator.float) $deviceInfo.aside.visible = false
else minimizeSidebar(true)
} else { } else {
openWidget(widget, $sidebarStore.widgetsState.get(widget._id)?.data, { active: true, openedByUser: true }) openWidget(widget, $sidebarStore.widgetsState.get(widget._id)?.data, { active: true, openedByUser: true })
} }
@ -100,6 +101,7 @@
width: 3.5rem; width: 3.5rem;
min-width: 3.5rem; min-width: 3.5rem;
max-width: 3.5rem; max-width: 3.5rem;
background-color: var(--theme-navpanel-color);
border-radius: 0 var(--medium-BorderRadius) var(--medium-BorderRadius) 0; border-radius: 0 var(--medium-BorderRadius) var(--medium-BorderRadius) 0;
overflow-y: auto; overflow-y: auto;
} }

View File

@ -37,6 +37,7 @@ export { default as NavHeader } from './components/NavHeader.svelte'
export { default as SpecialElement } from './components/navigator/SpecialElement.svelte' export { default as SpecialElement } from './components/navigator/SpecialElement.svelte'
export { default as SpaceView } from './components/SpaceView.svelte' export { default as SpaceView } from './components/SpaceView.svelte'
export { default as TreeSeparator } from './components/navigator/TreeSeparator.svelte' export { default as TreeSeparator } from './components/navigator/TreeSeparator.svelte'
export { default as WorkbenchTabs } from './components/WorkbenchTabs.svelte'
export { SpecialView } export { SpecialView }
export * from './utils' export * from './utils'

View File

@ -23,7 +23,9 @@ export class TalentDetailsPage extends CommonRecruitingPage {
readonly buttonFinalContact = (): Locator => this.formMergeContacts().locator('button', { hasText: 'Final contact' }) readonly buttonFinalContact = (): Locator => this.formMergeContacts().locator('button', { hasText: 'Final contact' })
readonly buttonMergeRow = (): Locator => this.formMergeContacts().locator('div.box') readonly buttonMergeRow = (hasText: string): Locator =>
this.formMergeContacts().locator('div.box.flex-row-center div.antiRadio', { hasText }).locator('label')
readonly buttonPopupMergeContacts = (): Locator => readonly buttonPopupMergeContacts = (): Locator =>
this.formMergeContacts().locator('button:has-text("Merge contacts")') this.formMergeContacts().locator('button:has-text("Merge contacts")')
@ -66,32 +68,17 @@ export class TalentDetailsPage extends CommonRecruitingPage {
await this.buttonFinalContact().click() await this.buttonFinalContact().click()
await this.selectMenuItem(this.page, talentName.finalContactName) await this.selectMenuItem(this.page, talentName.finalContactName)
await expect( await expect(this.buttonMergeRow(talentName.name)).toBeVisible()
this.buttonMergeRow().locator('div.flex-center', { hasText: talentName.name }).locator('label.checkbox-container')
).toBeVisible()
await this.buttonMergeRow()
.locator('div.flex-center', { hasText: talentName.name })
.locator('label.checkbox-container')
.click()
await this.buttonMergeRow(talentName.name).click()
if (talentName.mergeLocation) { if (talentName.mergeLocation) {
await this.buttonMergeRow() await this.buttonMergeRow(talentName.location).click()
.locator('div.flex-center', { hasText: talentName.location })
.locator('label.checkbox-container')
.click()
} }
if (talentName.mergeTitle) { if (talentName.mergeTitle) {
await this.buttonMergeRow() await this.buttonMergeRow(talentName.title).click()
.locator('div.flex-center', { hasText: talentName.title })
.locator('label.checkbox-container')
.click()
} }
if (talentName.mergeSource) { if (talentName.mergeSource) {
await this.buttonMergeRow() await this.buttonMergeRow(talentName.source).click()
.locator('div.flex-center', { hasText: talentName.source })
.locator('label.checkbox-container')
.click()
} }
await this.buttonPopupMergeContacts().click() await this.buttonPopupMergeContacts().click()