TSK-1324: update kanban layout (#3118)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2023-05-02 06:46:47 +03:00 committed by GitHub
parent d937a8034f
commit 439f4bbd6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 365 additions and 188 deletions

View File

@ -309,7 +309,7 @@
{@const stateObjects = getGroupByValues(groupByDocs, state)}
<div
class="panel-container step-lr75"
class="panel-container"
bind:this={stateRefs[si]}
on:dragover={(event) => panelDragOver(event, state)}
on:drop={() => {
@ -318,9 +318,9 @@
}}
>
{#if $$slots.header !== undefined}
<slot name="header" state={toAny(state)} count={stateObjects.length} />
<slot name="header" state={toAny(state)} count={stateObjects.length} index={si} />
{/if}
<Scroller padding={'.5rem 0'} on:dragover on:drop>
<Scroller padding={'.25rem .5rem'} on:dragover on:drop>
<slot name="beforeCard" {state} />
<KanbanRow
bind:this={stateRows[si]}
@ -361,10 +361,13 @@
position: relative;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.kanban-content {
display: flex;
padding: 1.5rem 2rem 0;
padding: 1.5rem;
min-width: 0;
}
@keyframes anim-border {
@ -384,23 +387,5 @@
background-color: transparent;
border: 1px solid transparent;
border-radius: 0.25rem;
.header {
display: flex;
flex-direction: column;
height: 4rem;
min-height: 4rem;
.bar {
height: 0.375rem;
border-radius: 0.25rem;
}
.label {
padding: 0 0.5rem 0 1rem;
height: 100%;
font-weight: 500;
color: var(--caption-color);
}
}
}
</style>

View File

@ -80,7 +80,7 @@
<div
bind:this={stateRefs[i]}
transition:slideD|local={{ isDragging }}
class="step-tb75"
class="p-1 flex-no-shrink clear-mins"
on:dragover|preventDefault={(evt) => cardDragOver(evt, object)}
on:drop|preventDefault={(evt) => cardDrop(evt, object)}
>
@ -107,14 +107,12 @@
</div>
{/each}
{#if stateObjects.length > limitedObjects.length}
<div class="step-tb75">
<div class="p-1 flex-no-shrink clear-mins">
{#if loading}
<Spinner />
{:else}
<div class="card-container h-18 flex-row-center flex-between p-4">
<span class="p-1">
{limitedObjects.length}/{stateObjects.length}
</span>
<div class="card-container flex-between p-4">
<span class="caption-color">{limitedObjects.length}</span> / {stateObjects.length}
<Button
size={'small'}
icon={IconMoreH}
@ -140,7 +138,7 @@
// }
&.checked {
background-color: var(--highlight-select);
box-shadow: inset 0 0 1px 1px var(--highlight-select-border);
box-shadow: 0 0 1px 1px var(--highlight-select-border);
&:hover {
background-color: var(--highlight-select-hover);
@ -148,7 +146,7 @@
}
&.selection,
&.checked.selection {
box-shadow: inset 0 0 1px 1px var(--primary-button-enabled);
box-shadow: 0 0 1px 1px var(--primary-button-enabled);
animation: anim-border 1s ease-in-out;
&:hover {
@ -168,10 +166,10 @@
}
@keyframes anim-border {
from {
box-shadow: inset 0 0 1px 1px var(--primary-edit-border-color);
box-shadow: 0 0 1px 1px var(--primary-edit-border-color);
}
to {
box-shadow: inset 0 0 1px 1px var(--primary-bg-color);
box-shadow: 0 0 1px 1px var(--primary-bg-color);
}
}
</style>

View File

@ -98,6 +98,7 @@
--theme-list-subheader-color: #262634;
--theme-list-row-color: #21212F;
--theme-list-button-color: #262633;
--theme-list-button-hover: #2F2F3A;
--theme-list-divider-color: rgba(255, 255, 255, .09);
--theme-list-subheader-divider: transparent;
@ -107,8 +108,10 @@
--theme-kanban-card-bg-color: rgba(222, 222, 240, .04);
--theme-kanban-card-border: transparent;
--theme-kanban-card-footer: #D9D9D9;
--theme-kanban-card-footer: rgba(217, 217, 217, .07);
--theme-kanban-button-color: #262634;
--theme-kanban-button-hover: #2F2F3B;
--theme-tablist-color: rgba(0, 0, 0, .02);
--theme-checkbox-color: #000;
--theme-checkbox-bg-color: #FFF;
@ -254,6 +257,7 @@
--theme-list-subheader-color: #EEEEF0;
--theme-list-row-color: #F7F7F8;
--theme-list-button-color: #F2F2F4;
--theme-list-button-hover: #E8E8EA;
--theme-list-divider-color: rgba(0, 0, 0, .07);
--theme-list-subheader-divider: rgba(0, 0, 0, .06);
@ -264,7 +268,9 @@
--theme-kanban-card-bg-color: rgba(0, 0, 0, .03);
--theme-kanban-card-border: rgba(0, 0, 0, .04);
--theme-kanban-card-footer: rgba(0, 0, 0, .04);
--theme-kanban-button-color: #E5E5E7;
--theme-kanban-button-hover: #DCDCDE;
--theme-tablist-color: rgba(0, 0, 0, .02);
--theme-checkbox-color: #000;
--theme-checkbox-bg-color: #FFF;

View File

@ -407,6 +407,10 @@ input.search {
&:not(.reverse) > *:not(:first-child) { margin-left: .5rem; }
&.reverse > *:not(:last-child) { margin-right: .5rem; }
}
.gap-3 {
&:not(.reverse) > *:not(:first-child) { margin-left: .75rem; }
&.reverse > *:not(:last-child) { margin-right: .75rem; }
}
.gap-around-2 > * { margin: .25rem; }
.gap-around-4 > * { margin: .5rem; }
@ -417,17 +421,19 @@ input.search {
flex-wrap: nowrap;
white-space: nowrap;
width: fit-content;
color: var(--caption-color);
font-size: .75rem;
color: var(--theme-darker-color);
cursor: pointer;
.icon {
margin-right: .25rem;
color: var(--dark-color);
color: var(--theme-content-color);
&.small-size {
width: 1.5rem;
height: 1.5rem;
}
}
&:hover .icon { color: var(--caption-color); }
&:hover .icon { color: var(--theme-caption-color); }
}
/* Margins & Paddings */

View File

@ -733,3 +733,10 @@
& > * { margin-left: .375rem; }
}
}
/* Kanban - global style */
.kanban-container .card-container .card-labels > * { margin: .25rem .25rem 0 0; }
.kanban-container .card-container .card-labels.labels > *:last-child {
flex-shrink: 0;
margin-right: 0;
}

View File

@ -366,15 +366,19 @@
&.link-bordered {
padding: 0 0.5rem;
color: var(--theme-content-color);
border-color: var(--theme-divider-color);
background-color: var(--theme-kanban-button-color);
border-color: var(--theme-button-border);
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-button-hovered);
background-color: var(--theme-kanban-button-hover);
border-color: var(--theme-list-divider-color);
.btn-icon {
color: var(--theme-caption-color);
}
}
&.small {
padding: 0 0.25rem;
}
}
&.list {
padding: 0 0.625em;

View File

@ -37,7 +37,7 @@
cy={8}
r={7}
class="progress-circle"
style:stroke={'var(--divider-color)'}
style:stroke={'var(--theme-divider-color)'}
style:opacity={'.5'}
style:transform={`rotate(${-78 + ((dashOffset + 1) * 360) / (lenghtC + 1)}deg)`}
style:stroke-dasharray={lenghtC}

View File

@ -266,10 +266,13 @@
&.link-bordered {
padding: 0 0.375rem;
color: var(--theme-content-color);
background-color: var(--theme-kanban-button-color);
border-color: var(--theme-button-border);
border-radius: 0.25rem;
&:hover {
color: var(--theme-caption-color);
background-color: var(--theme-button-hovered);
background-color: var(--theme-kanban-button-hover);
border-color: var(--theme-list-divider-color);
.btn-icon {
color: var(--theme-caption-color);
}

View File

@ -18,7 +18,7 @@
import { tooltip } from '../../tooltips'
import DatePresenter from './DatePresenter.svelte'
import { getDaysDifference } from './internal/DateUtils'
import { ButtonKind } from '../../types'
import { ButtonKind, ButtonSize } from '../../types'
export let value: number | null = null
export let shouldRender: boolean = true
@ -26,6 +26,7 @@
export let kind: ButtonKind = 'link'
export let editable: boolean = true
export let shouldIgnoreOverdue: boolean = false
export let size: ButtonSize = 'medium'
const today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0))
$: isOverdue = value !== null && value < today.getTime()
@ -90,6 +91,6 @@
}
: undefined}
>
<DatePresenter {value} {editable} icon={iconModifier} {kind} on:change={handleDueDateChanged} />
<DatePresenter {value} {editable} icon={iconModifier} {kind} {size} on:change={handleDueDateChanged} />
</div>
{/if}

View File

@ -170,7 +170,17 @@ export type TooltipAlignment = 'top' | 'bottom' | 'left' | 'right'
export type VerticalAlignment = 'top' | 'bottom'
export type HorizontalAlignment = 'left' | 'right'
export type IconSize = 'inline' | 'tiny' | 'x-small' | 'smaller' | 'small' | 'medium' | 'large' | 'x-large' | 'full'
export type IconSize =
| 'inline'
| 'tiny'
| 'card'
| 'x-small'
| 'smaller'
| 'small'
| 'medium'
| 'large'
| 'x-large'
| 'full'
export interface DateOrShift {
date?: number
@ -311,3 +321,8 @@ export interface DialogStep {
readonly component: AnyComponent | AnySvelteComponent
props?: Record<string, any>
}
export interface AccentColor {
textColor: string
backgroundColor: string
}

View File

@ -33,11 +33,11 @@
component: AttachmentPopup,
props: { objectId: object._id, attachments: value, object }
}}
class="sm-tool-icon ml-1 mr-1"
class="sm-tool-icon"
>
<span class="icon"><IconAttachment {size} /></span>
{#if showCounter}
&nbsp;{value}
{value}
{/if}
</div>
</DocNavLink>

View File

@ -33,7 +33,7 @@
component: CommentPopup,
props: { objectId: object._id, object }
}}
class="sm-tool-icon ml-1 mr-1"
class="sm-tool-icon"
>
<span class="icon"><IconThread {size} /></span>
{#if showCounter}

View File

@ -128,7 +128,7 @@
}}
/>
{:else}
<Icon icon={icon ?? AvatarIcon} {size} />
<Icon icon={icon ?? AvatarIcon} size={size === 'card' ? 'x-small' : size} />
{/if}
</div>
@ -160,6 +160,10 @@
height: 1.13rem;
}
.ava-card {
width: 1.25rem; // 20
height: 1.25rem;
}
.ava-x-small {
width: 1.5rem; // 24
height: 1.5rem;
@ -197,6 +201,8 @@
.ava-inline .ava-mask,
.ava-inline.no-img,
.ava-card .ava-mask,
.ava-card.no-img,
.ava-x-small .ava-mask,
.ava-x-small.no-img,
.ava-smaller .ava-mask,

View File

@ -11,7 +11,7 @@
export let object: WithLookup<Doc>
export let full: boolean
export let ckeckFilled: boolean = false
export let kind: 'short' | 'full' | 'list' = 'short'
export let kind: 'short' | 'full' | 'list' | 'kanban' = 'short'
export let isEditable: boolean = false
export let action: (evt: MouseEvent) => Promise<void> | void = async () => {}
export let compression: boolean = false
@ -46,10 +46,10 @@
})
</script>
{#if kind === 'list'}
{#if kind === 'list' || kind === 'kanban'}
{#each items as value}
<div class="label-box no-shrink" title={value.title}>
<TagReferencePresenter attr={undefined} {value} kind={'labels'} />
<TagReferencePresenter attr={undefined} {value} kind={kind === 'kanban' ? 'kanban-labels' : 'labels'} />
</div>
{/each}
{:else}

View File

@ -38,7 +38,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
style={`--tag-color:${getPlatformColor(tag?.color ?? element?.color ?? 0)}`}
class="tag-item-inline"
class="tag-item-inline overflow-label max-w-40"
on:click
use:tooltip={{
label: element?.description ? tags.string.TagTooltip : undefined,
@ -60,7 +60,7 @@
direction: 'right'
}}
>
{name}
<span class="overflow-label max-w-40">{name}</span>
<span class="ml-1">
{#if tag && tagIcon && schema !== '0'}
<Icon icon={tagIcon} size={'small'} />

View File

@ -102,30 +102,30 @@
align-items: center;
flex-shrink: 0;
padding: 0 0.375rem;
height: 1.375rem;
min-width: 1.375rem;
height: 1.5rem;
min-width: 1.5rem;
font-weight: 500;
font-size: 0.75rem;
line-height: 0.75rem;
white-space: nowrap;
color: var(--accent-color);
background-color: var(--board-card-bg-color);
border: 1px solid var(--divider-color);
color: var(--theme-content-color);
background-color: var(--theme-kanban-button-color);
border: 1px solid var(--theme-button-border);
border-radius: 0.25rem;
transition-property: border, background-color, color, box-shadow;
transition-duration: 0.15s;
&:hover {
color: var(--accent-color);
background-color: var(--button-bg-hover);
border-color: var(--button-border-hover);
color: var(--theme-caption-color);
background-color: var(--theme-kanban-button-hover);
border-color: var(--theme-list-divider-color);
transition-duration: 0;
}
&:focus {
border-color: var(--primary-edit-border-color) !important;
}
&:disabled {
color: rgb(var(--caption-color) / 40%);
color: rgb(var(--theme-caption-color) / 40%);
cursor: not-allowed;
}
}

View File

@ -32,7 +32,7 @@
{#if items.length}
<div class="flex-row-center flex-wrap">
{#each items as value}
<div class="step-container">
<div class="step-container clear-mins">
<TagReferencePresenter
{attr}
{value}
@ -43,7 +43,7 @@
</div>
{/each}
{#if !readonly}
<div class="step-container">
<div class="step-container clear-mins">
<button class="tag-button" on:click|stopPropagation={tagsHandler}>
<div class="icon"><Icon icon={IconAdd} size={'full'} /></div>
<span class="overflow-label label"><Label {label} /></span>

View File

@ -96,20 +96,20 @@
align-items: center;
justify-content: stretch;
padding: 0.5rem 2.5rem;
background-color: var(--board-bg-color);
border-top: 1px solid var(--divider-color);
background-color: var(--theme-comp-header-color);
border-top: 1px solid var(--theme-divider-color);
}
.done-item {
height: 3rem;
color: var(--caption-color);
color: var(--theme-caption-color);
border: 1px dashed transparent;
border-radius: 0.75rem;
padding: 0.5rem;
&.hovered {
background-color: var(--body-color);
border-color: var(--divider-color);
background-color: var(--theme-bg-color);
border-color: var(--theme-divider-color);
}
}
</style>

View File

@ -28,7 +28,15 @@
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient, statusStore } from '@hcengineering/presentation'
import { Kanban, SpaceWithStates, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task'
import { getEventPositionElement, Label, showPopup } from '@hcengineering/ui'
import {
getEventPositionElement,
Label,
showPopup,
deviceOptionsStore as deviceInfo,
hslToRgb,
rgbToHsl,
AccentColor
} from '@hcengineering/ui'
import {
AttributeModel,
CategoryOption,
@ -160,6 +168,14 @@
viewOptionsModel: ViewOptionModel[] | undefined
) {
categories = await getCategories(client, _class, docs, groupByKey, $statusStore, viewlet.descriptor)
categories.forEach((_, i) => {
if (accentColors[i] === undefined) {
accentColors[i] = {
textColor: 'var(--theme-caption-color)',
backgroundColor: '175, 175, 175'
}
}
})
for (const viewOption of viewOptionsModel ?? []) {
if (viewOption.actionTarget !== 'category') continue
const categoryFunc = viewOption as CategoryOption
@ -226,6 +242,19 @@
})
const getDoneUpdate = (e: any) => ({ doneState: e.detail._id } as DocumentUpdate<Doc>)
$: lth = $deviceInfo.theme === 'theme-light'
const accentColors: AccentColor[] = []
const setAccentColor = (n: number, ev: CustomEvent) => {
const accColor = rgbToHsl(ev.detail.r, ev.detail.g, ev.detail.b)
const textColor = !lth ? { r: 255, g: 255, b: 255 } : hslToRgb(accColor.h, accColor.s, 0.3)
const bgColor = !lth ? hslToRgb(accColor.h, accColor.s, 0.55) : hslToRgb(accColor.h, accColor.s, 0.9)
accentColors[n] = {
textColor: !lth ? 'var(--theme-caption-color)' : `rgb(${textColor.r}, ${textColor.g}, ${textColor.b})`,
backgroundColor: `${bgColor.r}, ${bgColor.g}, ${bgColor.b}`
}
}
</script>
{#await cardPresenter then presenter}
@ -254,27 +283,43 @@
}}
on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)}
>
<svelte:fragment slot="header" let:state let:count>
<svelte:fragment slot="header" let:state let:count let:index>
<!-- {@const status = $statusStore.get(state._id)} -->
<div class="header flex-col">
<div class="flex-row-center">
{#if groupByKey === noCategory}
<span class="text-base fs-bold overflow-label content-accent-color pointer-events-none">
{#key lth}
<div
style:--kanban-header-rgb-color={accentColors[index].backgroundColor ?? '175, 175, 175'}
class="header flex-row-center"
class:gradient={!lth}
>
<span
class="clear-mins fs-bold overflow-label pointer-events-none"
style:color={accentColors[index].textColor ?? 'var(--theme-caption-color)'}
>
{#if groupByKey === noCategory}
<Label label={view.string.NoGrouping} />
</span>
{:else if headerComponent}
<svelte:component this={headerComponent.presenter} value={state} {space} kind={'list-header'} />
{/if}
<span class="ml-1">
{:else if headerComponent}
<svelte:component
this={headerComponent.presenter}
value={state}
{space}
size={'small'}
kind={'list-header'}
colorInherit={lth}
accent
on:accent-color={(ev) => setAccentColor(index, ev)}
/>
{/if}
</span>
<span class="counter ml-1">
{count}
</span>
</div>
</div>
{/key}
</svelte:fragment>
<svelte:fragment slot="card" let:object let:dragged>
<svelte:component this={presenter} {object} {dragged} {groupByKey} />
</svelte:fragment>
// eslint-disable-next-line no-undef
<!-- eslint-disable-next-line no-undef -->
<svelte:fragment slot="doneBar" let:onDone>
<KanbanDragDone
{kanban}
@ -288,32 +333,27 @@
{/await}
<style lang="scss">
.names {
font-size: 0.8125rem;
}
.header {
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--divider-color);
margin: 0 0.75rem 0.5rem;
padding: 0 0.5rem 0 1.25rem;
height: 2.5rem;
min-height: 2.5rem;
border: 1px solid var(--theme-divider-color);
border-radius: 0.25rem;
.label {
color: var(--caption-color);
.counter {
color: rgba(var(--caption-color), 0.8);
}
&:not(.gradient) {
background: rgba(var(--kanban-header-rgb-color), 1);
}
&.gradient {
background: linear-gradient(
90deg,
rgba(var(--kanban-header-rgb-color), 0.15),
rgba(var(--kanban-header-rgb-color), 0.05)
);
}
.counter {
color: var(--theme-dark-color);
}
}
.tracker-card {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
// padding: 0.5rem 1rem;
min-height: 6.5rem;
}
.states-bar {
flex-shrink: 10;
width: fit-content;
margin: 0.625rem 1rem 0;
}
</style>

View File

@ -14,22 +14,33 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'
import type { State } from '@hcengineering/task'
import { getColorNumberByText, getPlatformColor } from '@hcengineering/ui'
import { getColorNumberByText, getPlatformColor, hexToRgb } from '@hcengineering/ui'
export let value: State | undefined
export let shouldShowAvatar = true
export let inline: boolean = false
export let colorInherit: boolean = false
export let accent: boolean = false
const dispatch = createEventDispatcher()
const defaultFill = 'currentColor'
$: fill = value ? getPlatformColor(value.color ?? getColorNumberByText(value.name)) : defaultFill
const dispatchAccentColor = (fill: string) =>
dispatch('accent-color', fill !== defaultFill ? hexToRgb(fill) : { r: 127, g: 127, b: 127 })
$: dispatchAccentColor(fill)
onMount(() => {
dispatchAccentColor(fill)
})
</script>
{#if value}
<div class="flex-presenter" class:inline-presenter={inline}>
{#if shouldShowAvatar}
<div
class="state-container"
class:inline
style="background-color: {getPlatformColor(value.color ?? getColorNumberByText(value.name))}"
/>
<div class="state-container" class:inline style="background-color: {fill}" />
{/if}
<span class="label nowrap">{value.name}</span>
</div>

View File

@ -22,6 +22,8 @@
export let value: Ref<State> | StatusValue
export let onChange: ((value: Ref<State>) => void) | undefined = undefined
export let colorInherit: boolean = false
export let accent: boolean = false
</script>
{#if value}
@ -29,6 +31,6 @@
{#if onChange !== undefined && state !== undefined}
<StateEditor value={state._id} space={state.space} {onChange} kind="link" size="medium" />
{:else}
<StatePresenter value={state} />
<StatePresenter value={state} {colorInherit} {accent} on:accent-color />
{/if}
{/if}

View File

@ -18,7 +18,7 @@
const dispatch = createEventDispatcher()
const dispatchAccentColor = (fill: string) =>
dispatch('accent-color', fill !== defaultFill ? hexToRgb(fill) : 'var(--theme-halfcontent-color)')
dispatch('accent-color', fill !== defaultFill ? hexToRgb(fill) : { r: 127, g: 127, b: 127 })
$: dispatchAccentColor(fill)

View File

@ -19,7 +19,7 @@
import { IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Issue, IssueTemplate } from '@hcengineering/tracker'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { eventToHTMLElement, showPopup, IconSize } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import { getObjectPresenter } from '@hcengineering/view-resources'
import tracker from '../../plugin'
@ -30,6 +30,7 @@
export let isEditable: boolean = true
export let shouldShowLabel: boolean = false
export let defaultName: IntlString | undefined = undefined
export let avatarSize: IconSize = 'x-small'
const client = getClient()
@ -88,7 +89,7 @@
this={presenter.presenter}
{value}
{defaultName}
avatarSize={'x-small'}
{avatarSize}
isInteractive={true}
shouldShowPlaceholder={true}
shouldShowName={shouldShowLabel}

View File

@ -17,10 +17,11 @@
import { Issue } from '@hcengineering/tracker'
import { getClient } from '@hcengineering/presentation'
import tracker from '../../plugin'
import { ButtonKind, DueDatePresenter } from '@hcengineering/ui'
import { ButtonKind, ButtonSize, DueDatePresenter } from '@hcengineering/ui'
export let value: WithLookup<Issue>
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'medium'
export let isEditable = true
const client = getClient()
@ -51,6 +52,7 @@
shouldRender={shouldRenderPresenter}
onChange={handleDueDateChanged}
editable={isEditable}
{size}
{kind}
shouldIgnoreOverdue={ignoreOverDue}
/>

View File

@ -41,7 +41,11 @@
Loading,
showPanel,
showPopup,
tooltip
tooltip,
deviceOptionsStore as deviceInfo,
hslToRgb,
rgbToHsl,
AccentColor
} from '@hcengineering/ui'
import {
AttributeModel,
@ -67,6 +71,8 @@
setGroupByValues
} from '@hcengineering/view-resources'
import view from '@hcengineering/view-resources/src/plugin'
import { AttachmentsPresenter } from '@hcengineering/attachment-resources'
import { CommentsPresenter } from '@hcengineering/chunter-resources'
import { onMount } from 'svelte'
import tracker from '../../plugin'
import ComponentEditor from '../components/ComponentEditor.svelte'
@ -189,6 +195,14 @@
viewOptionsModel: ViewOptionModel[] | undefined
) {
categories = await getCategories(client, _class, docs, groupByKey, $statusStore, viewlet.descriptor)
categories.forEach((_, i) => {
if (accentColors[i] === undefined) {
accentColors[i] = {
textColor: 'var(--theme-caption-color)',
backgroundColor: '175, 175, 175'
}
}
})
for (const viewOption of viewOptionsModel ?? []) {
if (viewOption.actionTarget !== 'category') continue
const categoryFunc = viewOption as CategoryOption
@ -243,6 +257,19 @@
space: doc.space
}
}
$: lth = $deviceInfo.theme === 'theme-light'
const accentColors: AccentColor[] = []
const setAccentColor = (n: number, ev: CustomEvent) => {
const accColor = rgbToHsl(ev.detail.r, ev.detail.g, ev.detail.b)
const textColor = !lth ? { r: 255, g: 255, b: 255 } : hslToRgb(accColor.h, accColor.s, 0.3)
const bgColor = !lth ? hslToRgb(accColor.h, accColor.s, 0.55) : hslToRgb(accColor.h, accColor.s, 0.9)
accentColors[n] = {
textColor: !lth ? 'var(--theme-caption-color)' : `rgb(${textColor.r}, ${textColor.g}, ${textColor.b})`,
backgroundColor: `${bgColor.r}, ${bgColor.g}, ${bgColor.b}`
}
}
</script>
{#if categories.length === 0}
@ -274,23 +301,39 @@
}}
on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)}
>
<svelte:fragment slot="header" let:state let:count>
<svelte:fragment slot="header" let:state let:count let:index>
<!-- {@const status = $statusStore.get(state._id)} -->
<div class="header flex-col">
<div class="flex-row-center flex-between">
{#key lth}
<div
style:--kanban-header-rgb-color={accentColors[index].backgroundColor ?? '175, 175, 175'}
class="header flex-between"
class:gradient={!lth}
>
<div class="flex-row-center gap-1">
{#if groupByKey === noCategory}
<span class="text-base fs-bold overflow-label content-accent-color pointer-events-none">
<span
class="clear-mins fs-bold overflow-label pointer-events-none"
style:color={accentColors[index].textColor ?? 'var(--theme-caption-color)'}
>
{#if groupByKey === noCategory}
<Label label={view.string.NoGrouping} />
</span>
{:else if headerComponent}
<svelte:component this={headerComponent.presenter} value={state} {space} kind={'list-header'} />
{/if}
<span class="ml-1">
{:else if headerComponent}
<svelte:component
this={headerComponent.presenter}
value={state}
{space}
size={'small'}
kind={'list-header'}
colorInherit={lth}
accent
on:accent-color={(ev) => setAccentColor(index, ev)}
/>
{/if}
</span>
<span class="counter">
{count}
</span>
</div>
<div class="flex-row-center gap-1">
<div class="tools gap-1">
<Button
icon={IconAdd}
kind={'transparent'}
@ -301,7 +344,7 @@
/>
</div>
</div>
</div>
{/key}
</svelte:fragment>
<svelte:fragment slot="card" let:object>
{@const issue = toIssue(object)}
@ -313,61 +356,75 @@
showPanel(tracker.component.EditIssue, object._id, object._class, 'content')
}}
>
<div class="flex-col ml-4 mr-8">
<div class="flex clear-mins names">
<div class="card-header flex-between">
<div class="flex-row-center text-sm">
<!-- {#if groupByKey !== 'status'} -->
<div class="mr-1">
<StatusEditor value={issue} kind="list" isEditable={false} />
</div>
<!-- {/if} -->
<IssuePresenter value={issue} />
<ParentNamesPresenter value={issue} />
</div>
<div class="flex-row-center gap-1 mt-1">
{#if groupByKey !== 'status'}
<StatusEditor value={issue} kind="list" isEditable={false} />
{/if}
<span class="fs-bold caption-color lines-limit-2">
{object.title}
</span>
</div>
</div>
<div class="abs-rt-content">
<AssigneePresenter
value={issue.assignee ? $employeeByIdStore.get(issue.assignee) : null}
defaultClass={contact.class.Employee}
object={issue}
isEditable={true}
/>
<div class="flex-center mt-2">
<div class="flex-row-center gap-2 reverse flex-no-shrink">
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
<AssigneePresenter
value={issue.assignee ? $employeeByIdStore.get(issue.assignee) : null}
defaultClass={contact.class.Employee}
object={issue}
isEditable={true}
avatarSize={'card'}
/>
</div>
</div>
<div class="xsmall-gap states-bar">
<div class="card-content text-md caption-color lines-limit-2">
{object.title}
</div>
<div class="card-labels">
{#if issue && issue.subIssues > 0}
<SubIssuesSelector value={issue} {currentProject} />
<SubIssuesSelector value={issue} {currentProject} size={'small'} />
{/if}
<PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'inline'} justify={'center'} />
<PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'small'} justify={'center'} />
<ComponentEditor
value={issue}
isEditable={true}
kind={'link-bordered'}
size={'inline'}
size={'small'}
justify={'center'}
width={''}
bind:onlyIcon={fullFilled[issueId]}
/>
<DueDatePresenter value={issue} kind={'link-bordered'} />
<EstimationEditor kind={'list'} size={'small'} value={issue} />
<div
class="clear-mins"
use:tooltip={{
component: fullFilled[issueId] ? tags.component.LabelsPresenter : undefined,
props: { object: issue, kind: 'full' }
<DueDatePresenter value={issue} size={'small'} kind={'link-bordered'} />
</div>
<div
class="card-labels labels"
use:tooltip={{
component: fullFilled[issueId] ? tags.component.LabelsPresenter : undefined,
props: { object: issue, kind: 'full' }
}}
>
<Component
is={tags.component.LabelsPresenter}
props={{ object: issue, ckeckFilled: fullFilled[issueId], lookupField: 'labels', kind: 'kanban' }}
on:change={(res) => {
if (res.detail.full) fullFilled[issueId] = true
}}
>
<Component
is={tags.component.LabelsPresenter}
props={{ object: issue, ckeckFilled: fullFilled[issueId], lookupField: 'labels' }}
on:change={(res) => {
if (res.detail.full) fullFilled[issueId] = true
}}
/>
/>
</div>
<div class="card-footer flex-between">
<EstimationEditor kind={'list'} size={'small'} value={issue} />
<div class="flex-row-center gap-3 reverse">
{#if (object.attachments ?? 0) > 0}
<AttachmentsPresenter value={object.attachments} {object} />
{/if}
{#if (object.comments ?? 0) > 0 || (object.$lookup?.attachedTo !== undefined && (object.$lookup.attachedTo.comments ?? 0) > 0)}
{#if (object.comments ?? 0) > 0}
<CommentsPresenter value={object.comments} {object} />
{/if}
{#if object.$lookup?.attachedTo !== undefined && (object.$lookup.attachedTo.comments ?? 0) > 0}
<CommentsPresenter value={object.$lookup?.attachedTo?.comments} object={object.$lookup?.attachedTo} />
{/if}
{/if}
</div>
</div>
</div>
@ -377,33 +434,66 @@
{/if}
<style lang="scss">
.names {
font-size: 0.8125rem;
}
.header {
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--theme-divider-color);
margin: 0 0.75rem 0.5rem;
padding: 0 0.5rem 0 1.25rem;
height: 2.5rem;
min-height: 2.5rem;
border: 1px solid var(--theme-divider-color);
border-radius: 0.25rem;
.label {
color: var(--theme-caption-color);
.counter {
color: rgba(var(--theme-caption-color), 0.8);
}
&:not(.gradient) {
background: rgba(var(--kanban-header-rgb-color), 1);
}
&.gradient {
background: linear-gradient(
90deg,
rgba(var(--kanban-header-rgb-color), 0.15),
rgba(var(--kanban-header-rgb-color), 0.05)
);
}
.counter {
color: var(--theme-dark-color);
}
.tools {
opacity: 0;
}
&:hover .tools {
opacity: 1;
}
}
.tracker-card {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
// padding: 0.5rem 1rem;
min-height: 6.5rem;
}
.states-bar {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin: 0.625rem 1rem;
.card-header {
padding: 0.75rem 1rem 0;
}
.card-content {
margin: 0.5rem 1rem;
}
/* Global styles in components.scss */
.card-labels {
display: flex;
flex-wrap: nowrap;
margin: 0 0.75rem 0 1rem;
min-width: 0;
&.labels {
overflow: hidden;
margin: 0 1rem;
width: calc(100% - 2rem);
border-radius: 0 0.24rem 0.24rem 0;
}
}
.card-footer {
margin-top: 1rem;
padding: 0.75rem 1rem;
background-color: var(--theme-kanban-card-footer);
border-radius: 0 0 0.25rem 0.25rem;
}
}
</style>