TSK-336: mobile UI adaptation (#3492)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2023-07-11 13:27:49 +03:00 committed by GitHub
parent 5dc40b1730
commit b2edbadf05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 167 additions and 89 deletions

View File

@ -25,7 +25,9 @@
handler,
registerFocus,
showPopup,
tooltip
tooltip,
deviceOptionsStore as deviceInfo,
checkAdaptiveMatching
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { Completion } from '../Completion'
@ -73,6 +75,8 @@
let isEmpty = true
$: setContent(content)
$: devSize = $deviceInfo.size
$: shrinkButtons = checkAdaptiveMatching(devSize, 'sm')
function setContent (content: string) {
textEditor?.setContent(content)
@ -379,7 +383,7 @@
{/if}
</div>
<div class="flex-between clear-mins" style:margin={'.75rem .75rem 0'}>
<div class="buttons-group large-gap">
<div class="buttons-group {shrinkButtons ? 'medium-gap' : 'large-gap'}">
{#each actions as a}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
@ -395,7 +399,7 @@
{/each}
</div>
{#if extraActions && extraActions.length > 0}
<div class="buttons-group large-gap">
<div class="buttons-group {shrinkButtons ? 'medium-gap' : 'large-gap'}">
{#each extraActions as a}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div

View File

@ -23,7 +23,9 @@
IconSize,
Scroller,
SelectPopup,
showPopup
showPopup,
deviceOptionsStore as deviceInfo,
checkAdaptiveMatching
} from '@hcengineering/ui'
import { Level } from '@tiptap/extension-heading'
import { createEventDispatcher } from 'svelte'
@ -445,14 +447,9 @@
}
)
}
$: buttonsGap =
buttonSize === 'large' || buttonSize === 'x-large' || buttonSize === 'full'
? 'large-gap'
: buttonSize === 'medium'
? 'medium-gap'
: buttonSize === 'small'
? 'small-gap'
: 'xsmall-gap'
$: devSize = $deviceInfo.size
$: buttonsGap = checkAdaptiveMatching(devSize, 'sm') ? 'small-gap' : 'large-gap'
$: buttonsHeight =
buttonSize === 'large' || buttonSize === 'x-large' || buttonSize === 'full'
? 'h-6 max-h-6'

View File

@ -263,7 +263,7 @@ input.search {
}
.justify-between { justify-content: space-between; }
.justify-start { justify-content: flex-start; }
.justify-end { justify-content: flex-end; }
.justify-end { justify-content: flex-end !important; }
.justify-center { justify-content: center; }
.justify-stretch { justify-content: stretch; }
.items-baseline { align-items: baseline; }

View File

@ -41,8 +41,8 @@
&.only-icon { width: 2.75rem; }
}
&.iconL:not(.iconR) { padding: 0 1rem 0 .75rem; }
&.iconR:not(.iconL) { padding: 0 .75rem 0 1rem; }
&.iconL:not(.iconR, .only-icon) { padding: 0 1rem 0 .75rem; }
&.iconR:not(.iconL, .only-icon) { padding: 0 .75rem 0 1rem; }
.btn-icon {
color: var(--theme-content-color);
transition: color .15s;
@ -88,7 +88,10 @@
&.bs-dashed { border-style: dashed; }
&.jf-left { justify-content: flex-start; }
&.jf-center { justify-content: center; }
&.only-icon { padding: 0; }
&.only-icon {
flex-shrink: 0 !important;
padding: 0;
}
&.regular {
background-color: var(--theme-button-default);

View File

@ -17,7 +17,16 @@
import { onMount, ComponentType } from 'svelte'
import { registerFocus } from '../focus'
import { tooltip } from '../tooltips'
import type { AnySvelteComponent, ButtonKind, ButtonShape, ButtonSize, LabelAndProps, IconProps } from '../types'
import type {
AnySvelteComponent,
ButtonKind,
ButtonShape,
ButtonSize,
LabelAndProps,
IconProps,
WidthType
} from '../types'
import { checkAdaptiveMatching, deviceOptionsStore as deviceInfo } from '..'
import Icon from './Icon.svelte'
import Label from './Label.svelte'
import Spinner from './Spinner.svelte'
@ -51,6 +60,7 @@
export let shrink: number = 0
export let accent: boolean = false
export let noFocus: boolean = false
export let adaptiveShrink: WidthType | null = null
$: iconSize =
iconProps && iconProps.size !== undefined ? iconProps.size : size && size === 'inline' ? 'inline' : 'small'
@ -62,6 +72,9 @@
(icon !== undefined || iconRight !== undefined || $$slots.icon || $$slots.iconRight)
$: primary = ['accented', 'brand', 'positive', 'negative'].some((p) => p === kind)
$: devSize = $deviceInfo.size
$: adaptive = adaptiveShrink !== null ? checkAdaptiveMatching(devSize, adaptiveShrink) : false
onMount(() => {
if (focus && input) {
input.focus()
@ -102,7 +115,7 @@
use:tooltip={showTooltip}
bind:this={input}
class="antiButton {kind} {size} jf-{justify} sh-{shape ?? 'no-shape'} bs-{borderStyle}"
class:only-icon={iconOnly}
class:only-icon={iconOnly || adaptive}
class:no-focus={noFocus}
class:accent
class:highlight
@ -138,7 +151,7 @@
<Spinner size={iconSize === 'inline' ? 'inline' : 'small'} />
</div>
{/if}
{#if label}
{#if label && !adaptive}
<span class="overflow-label label disabled pointer-events-none" class:ml-2={loading}>
<Label {label} params={labelParams} />
</span>

View File

@ -27,10 +27,11 @@
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let label: IntlString = ui.string.DropdownDefaultLabel
export let params: Record<string, any> = {}
export let items: DropdownIntlItem[]
export let selected: DropdownIntlItem['id'] | undefined = undefined
export let disabled: boolean = false
export let kind: ButtonKind = 'no-border'
export let kind: ButtonKind = 'regular'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = undefined
@ -86,7 +87,7 @@
on:click={openPopup}
>
<span slot="content" class="overflow-label disabled flex-grow text-left mr-2">
<Label label={selectedItem ? selectedItem.label : label} />
<Label label={selectedItem ? selectedItem.label : label} {params} />
</span>
<svelte:fragment slot="iconRight">
<DropdownIcon size={'small'} fill={'var(--theme-dark-color)'} />

View File

@ -50,7 +50,7 @@
dispatch('close', item.id)
}}
>
<div class="flex-grow caption-color nowrap"><Label label={item.label} /></div>
<div class="flex-grow caption-color nowrap"><Label label={item.label} params={item.params} /></div>
<div class="check">
{#if item.id === selected}<IconCheck size={'small'} />{/if}
</div>

View File

@ -19,6 +19,7 @@
items={modeList}
selected={props.mode}
kind={'separated'}
adaptiveShrink={'sm'}
on:select={(result) => {
if (result.detail !== undefined && result.detail.action) result.detail.action()
}}

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
import { deviceOptionsStore as deviceInfo } from '../../'
import { deviceOptionsStore as deviceInfo, checkAdaptiveMatching } from '../../'
import { resizeObserver } from '../resize'
import Button from './Button.svelte'
import Scroller from './Scroller.svelte'
@ -41,7 +41,10 @@
let asideFloat: boolean = false
let asideShown: boolean = true
let fullSize: boolean = false
$: twoRows = $deviceInfo.minWidth
$: devSize = $deviceInfo.size
$: twoRows = checkAdaptiveMatching(devSize, 'xs')
$: moveUtils = checkAdaptiveMatching(devSize, 'sm')
let oldWidth = ''
let hideTimer: number | undefined
@ -109,7 +112,9 @@
{#if !twoRows && !withoutTitle}<slot name="title" />{/if}
</div>
<div class="buttons-group xsmall-gap">
<slot name="utils" />
{#if !moveUtils}
<slot name="utils" />
{/if}
{#if isFullSize || useMaxWidth !== undefined || ($$slots.aside && isAside)}
<div class="buttons-divider" />
{/if}
@ -193,6 +198,11 @@
{/if}
{#if $$slots.aside && isAside && asideShown}
<div class="popupPanel-body__aside" class:float={asideFloat} class:shown={asideShown}>
{#if moveUtils}
<div class="buttons-group justify-end xsmall-gap" style:margin={'.5rem 2rem 0'}>
<slot name="utils" />
</div>
{/if}
<slot name="aside" />
</div>
{/if}

View File

@ -15,9 +15,11 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { tooltip } from '../tooltips'
import type { TabItem, IconSize } from '../types'
import type { TabItem, IconSize, WidthType, DropdownIntlItem } from '../types'
import Icon from './Icon.svelte'
import Label from './Label.svelte'
import DropdownLabelsIntl from './DropdownLabelsIntl.svelte'
import { checkAdaptiveMatching, deviceOptionsStore as deviceInfo } from '..'
export let selected: string | string[] = ''
export let multiselect: boolean = false
@ -25,6 +27,7 @@
export let kind: 'normal' | 'regular' | 'plain' | 'separated' | 'separated-free' = 'normal'
export let onlyIcons: boolean = false
export let size: 'small' | 'medium' = 'medium'
export let adaptiveShrink: WidthType | null = null
const dispatch = createEventDispatcher()
@ -41,52 +44,72 @@
let iconSize: IconSize
$: iconSize = onlyIcons ? (size === 'small' ? 'small' : 'medium') : size === 'small' ? 'x-small' : 'small'
$: devSize = $deviceInfo.size
$: adaptive = adaptiveShrink !== null ? checkAdaptiveMatching(devSize, adaptiveShrink) : false
let ddItems: DropdownIntlItem[]
$: ddItems = items.map((it) => ({ id: it.id, label: it.labelIntl, params: it.labelParams } as DropdownIntlItem))
</script>
{#if items.length > 0}
<div class="tablist-container {kind} {size}">
{#each items as item, i}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={tabs[i]}
class={kind === 'normal' || kind === 'regular' ? 'button' : 'plain'}
class:separated={kind === 'separated' || kind === 'separated-free'}
class:free={kind === 'separated-free'}
class:onlyIcons
class:selected={getSelected(item.id, selected)}
data-view={item.tooltip}
data-id={`tab-${item.id}`}
use:tooltip={{ label: item.tooltip ?? undefined, element: tabs[i] ?? undefined }}
on:click={() => {
if (multiselect) {
if (Array.isArray(selected)) {
if (selected.includes(item.id)) selected = selected.filter((it) => it !== item.id)
else selected.push(item.id)
}
} else selected = item.id
dispatch('select', item)
items = items
}}
>
{#if item.icon}
<div class="icon">
<Icon icon={item.icon} size={iconSize} fill={item.color ?? 'currentColor'} />
</div>
{:else if item.color}
<div class="color" style:background-color={item.color} />
{/if}
{#if item.label || item.labelIntl}
<span class="overflow-label" class:ml-1-5={item.icon || item.color}>
{#if item.label}
{item.label}
{:else if item.labelIntl}
<Label label={item.labelIntl} params={item.labelParams} />
{/if}
</span>
{/if}
</div>
{/each}
</div>
{#if adaptive}
<DropdownLabelsIntl
items={ddItems}
{size}
selected={Array.isArray(selected) ? selected[0] : selected}
on:selected={(e) => {
const item = items.filter((it) => it.id === e.detail)[0]
if (Array.isArray(selected)) selected[0] = item.id
else selected = item.id
dispatch('select', item)
}}
/>
{:else}
<div class="tablist-container {kind} {size}">
{#each items as item, i}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={tabs[i]}
class={kind === 'normal' || kind === 'regular' ? 'button' : 'plain'}
class:separated={kind === 'separated' || kind === 'separated-free'}
class:free={kind === 'separated-free'}
class:onlyIcons
class:selected={getSelected(item.id, selected)}
data-view={item.tooltip}
data-id={`tab-${item.id}`}
use:tooltip={{ label: item.tooltip ?? undefined, element: tabs[i] ?? undefined }}
on:click={() => {
if (multiselect) {
if (Array.isArray(selected)) {
if (selected.includes(item.id)) selected = selected.filter((it) => it !== item.id)
else selected.push(item.id)
}
} else selected = item.id
dispatch('select', item)
items = items
}}
>
{#if item.icon}
<div class="icon">
<Icon icon={item.icon} size={iconSize} fill={item.color ?? 'currentColor'} />
</div>
{:else if item.color}
<div class="color" style:background-color={item.color} />
{/if}
{#if item.label || item.labelIntl}
<span class="overflow-label" class:ml-1-5={item.icon || item.color}>
{#if item.label}
{item.label}
{:else if item.labelIntl}
<Label label={item.labelIntl} params={item.labelParams} />
{/if}
</span>
{/if}
</div>
{/each}
</div>
{/if}
{/if}
<style lang="scss">

View File

@ -2,6 +2,7 @@
import platform, { addEventListener, getMetadata, OK, PlatformEvent, Status } from '@hcengineering/platform'
import { onDestroy } from 'svelte'
import type { AnyComponent, WidthType } from '../../types'
import { deviceSizes, deviceWidths } from '../../types'
// import { applicationShortcutKey } from '../../utils'
import { getCurrentLocation, location, navigate, locationStorageKeyId } from '../../location'
@ -104,8 +105,6 @@
let remove: any = null
const sizes: Record<WidthType, boolean> = { xs: false, sm: false, md: false, lg: false, xl: false, xxl: false }
const css: Record<WidthType, string> = { xs: '', sm: '', md: '', lg: '', xl: '', xxl: '' }
const deviceSizes: WidthType[] = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl']
const deviceWidths = [480, 680, 760, 1024, 1208, -1]
deviceSizes.forEach((ds, i) => {
if (i === 0) css[ds] = `(max-width: ${deviceWidths[i]}px)`
else if (i === deviceSizes.length - 1) css[ds] = `(min-width: ${deviceWidths[i - 1]}.01px)`

View File

@ -266,6 +266,7 @@ export interface DropdownTextItem {
export interface DropdownIntlItem {
id: string | number
label: IntlString
params?: Record<string, any>
}
export interface PopupOptions {
@ -304,6 +305,8 @@ export const issueSP: FadeOptions = { multipler: { top: 2.75, bottom: 0 } }
export const emojiSP: FadeOptions = { multipler: { top: 1.5, bottom: 0 } }
export type WidthType = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
export const deviceSizes: WidthType[] = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl']
export const deviceWidths = [480, 680, 760, 1024, 1208, -1]
export interface DeviceOptions {
docWidth: number

View File

@ -19,7 +19,7 @@ import { IntlString, setMetadata } from '@hcengineering/platform'
import autolinker from 'autolinker'
import { writable } from 'svelte/store'
import { Notification, NotificationPosition, NotificationSeverity, notificationsStore } from '.'
import { AnyComponent, AnySvelteComponent } from './types'
import { AnyComponent, AnySvelteComponent, WidthType, deviceSizes } from './types'
/**
* @public
@ -65,6 +65,14 @@ export function isSafari (): boolean {
return navigator.userAgent.toLowerCase().includes('safari/')
}
/**
* @public
*/
export function checkAdaptiveMatching (size: WidthType | null, limit: WidthType): boolean {
const range = new Set(deviceSizes.slice(0, deviceSizes.findIndex((ds) => ds === limit) + 1))
return size !== null ? range.has(size) : false
}
export function floorFractionDigits (n: number | string, amount: number): number {
return Number(Number(n).toFixed(amount))
}

View File

@ -163,7 +163,7 @@
</script>
<div
class="ac-header full divide"
class="ac-header full divide caption-height"
class:header-with-mode-selector={modeSelectorProps !== undefined}
class:header-without-label={!labelTasks}
>

View File

@ -155,6 +155,7 @@
isAside={true}
isSub={false}
withoutActivity={false}
withoutTitle
{embedded}
bind:innerWidth
on:open
@ -252,15 +253,12 @@
/>
<Button
kind={'ghost'}
icon={IconMixin}
selected={showAllMixins}
on:click={() => {
showAllMixins = !showAllMixins
}}
>
<svelte:fragment slot="icon">
<IconMixin size={'small'} />
</svelte:fragment>
</Button>
/>
</svelte:fragment>
<svelte:fragment slot="custom-attributes">

View File

@ -56,6 +56,7 @@
icon={view.icon.ViewButton}
label={view.string.View}
{kind}
adaptiveShrink={'sm'}
showTooltip={{ label: view.string.CustomizeView, direction: 'bottom' }}
bind:input={btn}
on:click={clickHandler}

View File

@ -38,6 +38,7 @@
label={view.string.Show}
{kind}
shrink={1}
adaptiveShrink={'sm'}
showTooltip={{ label: view.string.CustomizeView, direction: 'bottom' }}
bind:input={btn}
on:click={clickHandler}

View File

@ -16,7 +16,7 @@
import { AnyAttribute, Doc, getObjectValue } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { getClient, updateAttribute } from '@hcengineering/presentation'
import { CheckBox, Component, IconCircles, tooltip } from '@hcengineering/ui'
import { CheckBox, Component, IconCircles, tooltip, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import { createEventDispatcher, onMount } from 'svelte'
import view from '../../plugin'
@ -63,6 +63,8 @@
return (value: any) => onChange(value, docObject, attribute.key, attr)
}
$: mobile = $deviceInfo.isMobile
onMount(() => {
dispatch('on-mount')
})
@ -116,16 +118,18 @@
{@const displayProps = attributeModel.displayProps}
{#if !groupByKey || displayProps?.excludeByKey !== groupByKey}
{#if displayProps?.grow}
{#each model.filter((p) => p.displayProps?.suffix === true) as attrModel}
<ListPresenter
{docObject}
attributeModel={attrModel}
{props}
{compactMode}
value={getObjectValue(attrModel.key, docObject)}
onChange={getOnChange(docObject, attrModel)}
/>
{/each}
{#if !(compactMode && mobile)}
{#each model.filter((p) => p.displayProps?.suffix === true) as attrModel}
<ListPresenter
{docObject}
attributeModel={attrModel}
{props}
{compactMode}
value={getObjectValue(attrModel.key, docObject)}
onChange={getOnChange(docObject, attrModel)}
/>
{/each}
{/if}
<GrowPresenter />
{#if !compactMode}
<div class="compression-bar">
@ -176,6 +180,18 @@
<IconCircles />
</div>
<div class="scroll-box gap-2">
{#if mobile}
{#each model.filter((p) => p.displayProps?.suffix === true) as attrModel}
<ListPresenter
{docObject}
attributeModel={attrModel}
{props}
{compactMode}
value={getObjectValue(attrModel.key, docObject)}
onChange={getOnChange(docObject, attrModel)}
/>
{/each}
{/if}
<div class="compression-bar">
{#each model.filter((m) => m.displayProps?.compression) as attributeModel, j}
{@const displayProps = attributeModel.displayProps}