Fixed layouts for ListView and Section (#8703)

Signed-off-by: Alexander Platov <alexander.platov@hardcoreeng.com>
This commit is contained in:
Alexander Platov 2025-04-25 05:37:38 +03:00 committed by GitHub
parent d112acd6e2
commit 1959b9f107
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 232 additions and 181 deletions

View File

@ -598,6 +598,9 @@
height: 3.5rem; height: 3.5rem;
min-height: 3.5rem; min-height: 3.5rem;
} }
&.spaceBeforeContent {
margin-bottom: .75rem;
}
&__icon { &__icon {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -657,14 +660,22 @@
.invisible { display: none; } .invisible { display: none; }
&-empty { &-empty {
display: flex; display: flex;
justify-content: center; min-width: 0;
align-items: center; min-height: 0;
padding: 1rem;
font-size: .75rem;
color: var(--dark-color);
border: 1px dashed var(--divider-color);
border-radius: 0.75rem;
&:not(.none-appearance) {
justify-content: center;
align-items: center;
flex-wrap: wrap;
padding: 1rem;
font-size: .75rem;
color: var(--dark-color);
border: 1px dashed var(--divider-color);
border-radius: 0.75rem;
}
&.none-appearance {
flex-direction: column;
}
&.solid { border-style: solid; } &.solid { border-style: solid; }
&.items { &.items {
justify-content: start; justify-content: start;
@ -677,6 +688,9 @@
} }
} }
} }
:is(.antiSection, .step-tb-6, .mt-6) + :is(.antiSection, .step-tb-6) {
margin-top: 1.5rem;
}
// Button on selected card in Kanban // Button on selected card in Kanban
.card-container.checked .button.inline.link-bordered { .card-container.checked .button.inline.link-bordered {

View File

@ -2276,7 +2276,8 @@
border-bottom: 1px solid var(--theme-list-subheader-divider); border-bottom: 1px solid var(--theme-list-subheader-divider);
} }
&.row.lastCat { &.row.lastCat {
border-radius: 0 0 0.25rem 0.25rem; border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
border-bottom: 1px solid var(--theme-list-border-color); border-bottom: 1px solid var(--theme-list-border-color);
} }
@ -2401,12 +2402,24 @@
} }
/* ListView - global style */ /* ListView - global style */
.category-container.disableHeader .listGrid.row:first-child {
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
border-top: 1px solid var(--theme-list-border-color);
}
.list-container .category-container.collapsed:not(.category-last, .category-one) .categoryHeader.subLevel,
.list-container .category-container:not(.category-last, .category-one, .zero-container) .category-container.category-last .listGrid.last.lastCat,
.list-container .category-container:not(.category-last, .category-one, .zero-container) .category-container.category-last .categoryHeader.subLevel.lastCat {
border-radius: 0;
}
.list-container .category-container .categoryHeader.subLevel.closed { .list-container .category-container .categoryHeader.subLevel.closed {
border-radius: 0 0 0.25rem 0.25rem; border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
border-bottom: 1px solid var(--theme-list-border-color); border-bottom: 1px solid var(--theme-list-border-color);
} }
.list-container .category-container .categoryHeader.closed:not(.subLevel) { .list-container .category-container .categoryHeader.closed:not(.subLevel) {
border-radius: 0 0 0.25rem 0.25rem; border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
&::before, &::before,
&::after { &::after {

View File

@ -24,13 +24,14 @@
export let icon: Asset | AnySvelteComponent | undefined = undefined export let icon: Asset | AnySvelteComponent | undefined = undefined
export let showHeader: boolean = true export let showHeader: boolean = true
export let spaceBeforeContent: boolean = false
export let high: boolean = false export let high: boolean = false
export let invisible: boolean = false export let invisible: boolean = false
</script> </script>
<div class="antiSection" {id}> <div class="antiSection" {id}>
{#if showHeader} {#if showHeader}
<div class="antiSection-header" class:high class:invisible> <div class="antiSection-header" class:high class:invisible class:spaceBeforeContent>
{#if icon} {#if icon}
<div class="antiSection-header__icon"> <div class="antiSection-header__icon">
<Icon {icon} size={'small'} /> <Icon {icon} size={'small'} />

View File

@ -252,45 +252,54 @@
$: void updateActivityMessages(object._id, isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending) $: void updateActivityMessages(object._id, isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending)
</script> </script>
<div class="step-tb-6"> <Section label={activity.string.Activity} icon={activity.icon.Activity}>
<Section label={activity.string.Activity} icon={activity.icon.Activity}> <svelte:fragment slot="header">
<svelte:fragment slot="header"> {#if isLoading}
{#if isLoading} <div class="ml-1">
<div class="ml-1"> <Spinner size="small" />
<Spinner size="small" /> </div>
</div> {/if}
{/if} <ActivityFilter
<ActivityFilter messages={allMessages}
messages={allMessages} {object}
{object} on:update={(e) => {
on:update={(e) => { filteredMessages = e.detail
filteredMessages = e.detail }}
}} bind:isNewestFirst
bind:isNewestFirst />
/> </svelte:fragment>
</svelte:fragment>
<svelte:fragment slot="content"> <svelte:fragment slot="content">
{#if isNewestFirst && showCommenInput} {#if isNewestFirst && showCommenInput}
<div class="ref-input newest-first"> <div class="ref-input newest-first">
<ActivityExtensionComponent <ActivityExtensionComponent
kind="input" kind="input"
{extensions} {extensions}
props={{ object, boundary, focusIndex, withTypingInfo: true }} props={{ object, boundary, focusIndex, withTypingInfo: true }}
/> />
</div> </div>
{/if} {/if}
<div <div
class="p-activity select-text" class="p-activity select-text"
id={activity.string.Activity} id={activity.string.Activity}
class:newest-first={isNewestFirst} class:newest-first={isNewestFirst}
bind:this={activityBox} bind:this={activityBox}
> >
{#if filteredMessages.length} {#if filteredMessages.length}
<Grid column={1} rowGap={0}> <Grid column={1} rowGap={0}>
{#each filteredMessages as message, index} {#each filteredMessages as message, index}
{@const canGroup = canGroupMessages(message, filteredMessages[index - 1])} {@const canGroup = canGroupMessages(message, filteredMessages[index - 1])}
{#if selectedMessageId} {#if selectedMessageId}
<ActivityMessagePresenter
value={message}
doc={object}
hideLink={true}
type={canGroup ? 'short' : 'default'}
isHighlighted={selectedMessageId === message._id}
withShowMore
/>
{:else}
<Lazy>
<ActivityMessagePresenter <ActivityMessagePresenter
value={message} value={message}
doc={object} doc={object}
@ -299,34 +308,23 @@
isHighlighted={selectedMessageId === message._id} isHighlighted={selectedMessageId === message._id}
withShowMore withShowMore
/> />
{:else} </Lazy>
<Lazy> {/if}
<ActivityMessagePresenter {/each}
value={message} </Grid>
doc={object}
hideLink={true}
type={canGroup ? 'short' : 'default'}
isHighlighted={selectedMessageId === message._id}
withShowMore
/>
</Lazy>
{/if}
{/each}
</Grid>
{/if}
</div>
{#if showCommenInput && !isNewestFirst}
<div class="ref-input oldest-first">
<ActivityExtensionComponent
kind="input"
{extensions}
props={{ object, boundary, focusIndex, withTypingInfo: true }}
/>
</div>
{/if} {/if}
</svelte:fragment> </div>
</Section> {#if showCommenInput && !isNewestFirst}
</div> <div class="ref-input oldest-first">
<ActivityExtensionComponent
kind="input"
{extensions}
props={{ object, boundary, focusIndex, withTypingInfo: true }}
/>
</div>
{/if}
</svelte:fragment>
</Section>
<style lang="scss"> <style lang="scss">
.ref-input { .ref-input {

View File

@ -74,34 +74,32 @@
}) })
</script> </script>
<div class="step-tb-6"> <Section label={activity.string.Activity} icon={activity.icon.Activity}>
<Section label={activity.string.Activity} icon={activity.icon.Activity}> <svelte:fragment slot="header">
<svelte:fragment slot="header"> {#if isLoading}
{#if isLoading} <div class="ml-1">
<div class="ml-1"> <Spinner size="small" />
<Spinner size="small" />
</div>
{/if}
</svelte:fragment>
<svelte:fragment slot="content">
<div class="p-activity select-text" id={activity.string.Activity}>
{#if messages.length}
<Grid column={1} rowGap={1}>
{#each messages as message (message.id)}
<MessagePresenter {message} card={object} />
{/each}
</Grid>
{/if}
</div> </div>
{#if showInput} {/if}
<div class="ref-input oldest-first"> </svelte:fragment>
<MessageInput cardId={object._id} cardType={object._class} placeholder={activity.string.Message} />
</div> <svelte:fragment slot="content">
<div class="p-activity select-text" id={activity.string.Activity}>
{#if messages.length}
<Grid column={1} rowGap={1}>
{#each messages as message (message.id)}
<MessagePresenter {message} card={object} />
{/each}
</Grid>
{/if} {/if}
</svelte:fragment> </div>
</Section> {#if showInput}
</div> <div class="ref-input oldest-first">
<MessageInput cardId={object._id} cardType={object._class} placeholder={activity.string.Message} />
</div>
{/if}
</svelte:fragment>
</Section>
<style lang="scss"> <style lang="scss">
.ref-input { .ref-input {

View File

@ -19,7 +19,7 @@
import { import {
Label, Label,
Scroller, Scroller,
Button, ModernButton,
getCurrentLocation, getCurrentLocation,
IconAdd, IconAdd,
navigate, navigate,
@ -142,21 +142,24 @@
const selection = listProvider.selection const selection = listProvider.selection
</script> </script>
<Section label={card.string.Children} icon={card.icon.Card}> <Section label={card.string.Children} icon={card.icon.Card} spaceBeforeContent>
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<ViewletsSettingButton bind:viewOptions viewletQuery={{ _id: viewletId }} kind={'tertiary'} bind:viewlet /> <div class="buttons-group xsmall-gap">
{#if !$restrictionStore.readonly && !readonly} <ViewletsSettingButton bind:viewOptions viewletQuery={{ _id: viewletId }} kind={'tertiary'} bind:viewlet />
<Button {#if !$restrictionStore.readonly && !readonly}
id="add-child-card" <ModernButton
icon={IconAdd} id="add-child-card"
label={card.string.CreateChild} icon={IconAdd}
kind={'ghost'} label={card.string.CreateChild}
showTooltip={{ label: card.string.CreateChild, direction: 'bottom' }} size={'small'}
on:click={() => { kind={'tertiary'}
void createCard() tooltip={{ label: card.string.CreateChild, direction: 'bottom' }}
}} on:click={() => {
/> void createCard()
{/if} }}
/>
{/if}
</div>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="content"> <svelte:fragment slot="content">
{#if (object?.children ?? 0) > 0 && viewOptions !== undefined && viewlet} {#if (object?.children ?? 0) > 0 && viewOptions !== undefined && viewlet}
@ -194,7 +197,7 @@
</div> </div>
</Scroller> </Scroller>
{:else if !readonly} {:else if !readonly}
<div class="antiSection-empty solid clear-mins mt-3"> <div class="antiSection-empty solid">
<!-- 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 -->
<span class="over-underline content-color" on:click={createCard}> <span class="over-underline content-color" on:click={createCard}>

View File

@ -102,7 +102,7 @@
let docsProvided = false let docsProvided = false
</script> </script>
<Section icon={process.icon.Process} label={process.string.Processes}> <Section icon={process.icon.Process} label={process.string.Processes} spaceBeforeContent>
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<div class="buttons-group xsmall-gap"> <div class="buttons-group xsmall-gap">
<ViewletsSettingButton bind:viewOptions viewletQuery={{ _id: viewletId }} kind={'tertiary'} bind:viewlet /> <ViewletsSettingButton bind:viewOptions viewletQuery={{ _id: viewletId }} kind={'tertiary'} bind:viewlet />
@ -112,7 +112,7 @@
<svelte:fragment slot="content"> <svelte:fragment slot="content">
<div <div
class="antiSection-empty solid flex-col flex-gap-2 mt-3" class="antiSection-empty {docsProvided && docs.length === 0 ? 'solid' : 'none-appearance flex-gap-2'}"
use:resizeObserver={(evt) => { use:resizeObserver={(evt) => {
listWidth = evt.clientWidth listWidth = evt.clientWidth
}} }}
@ -146,10 +146,8 @@
}} }}
/> />
{#if docsProvided && docs.length === 0} {#if docsProvided && docs.length === 0}
<div class="flex-col-center"> <div class="flex-center content-color">
<div class="caption-color"> <Label label={process.string.NoProcesses} />
<Label label={process.string.NoProcesses} />
</div>
</div> </div>
{/if} {/if}
{/if} {/if}

View File

@ -148,12 +148,12 @@
$: snapshots = backupInfo?.info?.snapshots ?? [] $: snapshots = backupInfo?.info?.snapshots ?? []
</script> </script>
<div class="hulyComponent p-2 flex-no-shrink"> <div class="hulyComponent flex-no-shrink">
<Header adaptive={'disabled'}> <Header adaptive={'disabled'}>
<Breadcrumb icon={setting.icon.Setting} label={setting.string.Backup} size={'large'} isCurrent /> <Breadcrumb icon={setting.icon.Setting} label={setting.string.Backup} size={'large'} isCurrent />
</Header> </Header>
<Scroller noStretch> <Scroller padding={'1.5rem'} noStretch>
{#if loading} {#if loading}
<Loading size={'small'} /> <Loading size={'small'} />
{:else if backupInfo == null} {:else if backupInfo == null}

View File

@ -17,7 +17,7 @@
<script lang="ts"> <script lang="ts">
import { MessageBox } from '@hcengineering/presentation' import { MessageBox } from '@hcengineering/presentation'
import { Question, QuestionKind, Survey } from '@hcengineering/survey' import { Question, QuestionKind, Survey } from '@hcengineering/survey'
import { createFocusManager, EditBox, FocusHandler, Icon, Label, showPopup } from '@hcengineering/ui' import { createFocusManager, EditBox, FocusHandler, showPopup, Section } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import survey from '../plugin' import survey from '../plugin'
import EditQuestion from './EditQuestion.svelte' import EditQuestion from './EditQuestion.svelte'
@ -144,53 +144,47 @@
}} }}
/> />
</div> </div>
<div class="antiSection step-tb-6"> <Section label={survey.string.Questions} icon={IconQuestion} spaceBeforeContent>
<div class="antiSection-header mb-3"> <svelte:fragment slot="content">
<div class="antiSection-header__icon"> {#each questionList as question, index (index)}
<Icon icon={IconQuestion} size={'small'} /> {@const isNewQuestion = index === questionList.length - 1}
</div> <div
<span class="antiSection-header__title"> role="listitem"
<Label label={survey.string.Questions} /> on:dragover={(ev) => {
</span> onQuestionDragOver(ev, index)
</div>
{#each questionList as question, index (index)}
{@const isNewQuestion = index === questionList.length - 1}
<div
role="listitem"
on:dragover={(ev) => {
onQuestionDragOver(ev, index)
}}
on:dragleave={(ev) => {
onQuestionDragLeave(ev, index)
}}
on:drop={onQuestionDrop}
class:dragged-over={draggedIndex !== undefined &&
draggedOverIndex === index &&
draggedOverIndex !== draggedIndex &&
draggedOverIndex !== draggedIndex + 1}
>
<EditQuestion
bind:this={questionComponents[index]}
{question}
{isNewQuestion}
on:delete={() => {
deleteQuestion(index)
}} }}
on:change={(e) => { on:dragleave={(ev) => {
isNewQuestion ? handleNewQuestionChange(e.detail) : handleQuestionChange(index, e.detail) onQuestionDragLeave(ev, index)
}} }}
{readonly} on:drop={onQuestionDrop}
on:dragStart={() => { class:dragged-over={draggedIndex !== undefined &&
draggedIndex = index draggedOverIndex === index &&
}} draggedOverIndex !== draggedIndex &&
on:dragEnd={() => { draggedOverIndex !== draggedIndex + 1}
draggedIndex = undefined >
draggedOverIndex = undefined <EditQuestion
}} bind:this={questionComponents[index]}
/> {question}
</div> {isNewQuestion}
{/each} on:delete={() => {
</div> deleteQuestion(index)
}}
on:change={(e) => {
isNewQuestion ? handleNewQuestionChange(e.detail) : handleQuestionChange(index, e.detail)
}}
{readonly}
on:dragStart={() => {
draggedIndex = index
}}
on:dragEnd={() => {
draggedIndex = undefined
draggedOverIndex = undefined
}}
/>
</div>
{/each}
</svelte:fragment>
</Section>
{/if} {/if}
<style lang="scss"> <style lang="scss">

View File

@ -441,7 +441,11 @@
in:fade|local={{ duration: 50 }} in:fade|local={{ duration: 50 }}
bind:this={div} bind:this={div}
class="category-container" class="category-container"
class:collapsed
class:category-one={oneCat}
class:category-last={lastCat}
class:zero-container={level === 0} class:zero-container={level === 0}
class:disableHeader
on:drop|preventDefault={drop} on:drop|preventDefault={drop}
on:dragover={dragOverCat} on:dragover={dragOverCat}
on:dragenter={dragEnterCat} on:dragenter={dragEnterCat}

View File

@ -93,6 +93,7 @@
$: selectionIds = new Set($selection.map((it) => it._id)) $: selectionIds = new Set($selection.map((it) => it._id))
$: selected = items.filter((it) => selectionIds.has(it._id)) $: selected = items.filter((it) => selectionIds.has(it._id))
// $: if (itemsProj.length === 0 && !collapsed) collapsed = true
</script> </script>
{#if headerComponent || groupByKey === noCategory} {#if headerComponent || groupByKey === noCategory}
@ -104,7 +105,7 @@
class="flex-between categoryHeader row" class="flex-between categoryHeader row"
class:flat class:flat
class:noDivide={showColors} class:noDivide={showColors}
class:collapsed class:collapsed={collapsed || itemsProj.length === 0}
class:subLevel={level !== 0} class:subLevel={level !== 0}
class:lastCat class:lastCat
class:cursor-pointer={items.length > 0} class:cursor-pointer={items.length > 0}
@ -124,7 +125,11 @@
> >
<div class="flex-row-center flex-grow" style:color={headerComponent ? headerTextColor : 'inherit'}> <div class="flex-row-center flex-grow" style:color={headerComponent ? headerTextColor : 'inherit'}>
<!-- {#if level === 0} --> <!-- {#if level === 0} -->
<div class="chevron"><IconCollapseArrow size={level === 0 ? 'small' : 'tiny'} /></div> <div class="chevron" class:empty={itemsProj.length === 0}>
{#if itemsProj.length > 0}
<IconCollapseArrow size={level === 0 ? 'small' : 'tiny'} />
{/if}
</div>
<!-- {/if} --> <!-- {/if} -->
{#if groupByKey === noCategory} {#if groupByKey === noCategory}
<span class="fs-bold content-color overflow-label pointer-events-none"> <span class="fs-bold content-color overflow-label pointer-events-none">
@ -217,20 +222,42 @@
min-height: 2.75rem; min-height: 2.75rem;
min-width: 0; min-width: 0;
background: var(--theme-bg-color); background: var(--theme-bg-color);
border-radius: 0.25rem 0.25rem 0 0;
&:not(.subLevel) {
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
}
.on-hover { .on-hover {
visibility: hidden; visibility: hidden;
} }
.chevron { .chevron {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0; flex-shrink: 0;
min-width: 0; min-width: 1rem;
min-height: 1rem;
margin-right: 0.75rem; margin-right: 0.75rem;
color: var(--theme-caption-color); color: var(--theme-caption-color);
transform-origin: center; transform-origin: center;
transform: rotate(90deg); transform: rotate(90deg);
transition: transform 0.15s ease-in-out; transition: transform 0.15s ease-in-out;
&.empty {
position: relative;
&::after {
content: '';
position: absolute;
top: 0.375rem;
left: 0.375rem;
width: 0.25rem;
height: 0.25rem;
background-color: var(--theme-dark-color);
border-radius: 50%;
}
}
} }
&::before, &::before,
&::after { &::after {
@ -240,7 +267,8 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
border-radius: 0.25rem 0.25rem 0 0; border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
pointer-events: none; pointer-events: none;
} }
&::after { &::after {
@ -291,9 +319,9 @@
&.flat { &.flat {
background: var(--header-bg-color); background: var(--header-bg-color);
background-blend-mode: darken; background-blend-mode: darken;
min-height: 2.25rem; min-height: 2.5rem;
height: 2.25rem; height: 2.5rem;
padding: 0 0.25rem 0 0.25rem; padding: 0 0.375rem 0 0.75rem;
} }
} }
</style> </style>