Browse Source

feat(Dashboard): add new designs

Signed-off-by: Dorra Jaouad <dorra.jaoued7@gmail.com>
pull/15697/head
Dorra Jaouad 2 months ago
parent
commit
470dc6673a
  1. 6
      REUSE.toml
  2. BIN
      img/dashboard/meetings.png
  3. BIN
      img/dashboard/mentions.png
  4. BIN
      img/dashboard/reminders.png
  5. BIN
      img/dashboard/welcome.png
  6. 117
      src/components/Dashboard/DashboardSection.vue
  7. 4
      src/components/Dashboard/EventCard.vue
  8. 97
      src/components/Dashboard/Section.vue
  9. 183
      src/components/Dashboard/TalkDashboard.vue

6
REUSE.toml

@ -71,6 +71,12 @@ precedence = "aggregate"
SPDX-FileCopyrightText = "2018-2025 Google LLC"
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = "img/dashboard/**.png"
precedence = "aggregate"
SPDX-FileCopyrightText = "Nextcloud GmbH <https://nextcloud.com/trademarks/>"
SPDX-License-Identifier = "LicenseRef-NextcloudTrademarks"
[[annotations]]
path = "img/emojis/**.gif"
precedence = "aggregate"

BIN
img/dashboard/meetings.png

After

Width: 600  |  Height: 600  |  Size: 280 KiB

BIN
img/dashboard/mentions.png

After

Width: 600  |  Height: 600  |  Size: 142 KiB

BIN
img/dashboard/reminders.png

After

Width: 600  |  Height: 600  |  Size: 106 KiB

BIN
img/dashboard/welcome.png

After

Width: 600  |  Height: 600  |  Size: 112 KiB

117
src/components/Dashboard/DashboardSection.vue

@ -0,0 +1,117 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script lang="ts" setup>
import { useIsMobile, useIsSmallMobile } from '@nextcloud/vue/composables/useIsMobile'
const { wide = false, title = '', subtitle = '', description = '' } = defineProps<{
wide?: boolean
title?: string
subtitle?: string
description?: string
}>()
const isSmallMobile = useIsSmallMobile()
const isMobile = useIsMobile()
</script>
<template>
<div class="dashboard-section"
:class="{
'dashboard-section--wide': wide && !isSmallMobile,
'dashboard-section--list': $slots.list,
}">
<div v-if="!isSmallMobile"
class="dashboard-section__bar"
:class="{
'dashboard-section__bar--narrow': $slots.list || isMobile,
gradient: !$slots.image || isMobile,
'image-container': $slots.image,
}">
<slot v-if="!($slots.list || isMobile)" name="image" />
</div>
<div class="dashboard-section__content">
<h3 class="dashboard-section__title">
{{ title }}
</h3>
<span class="dashboard-section__subtitle">{{ subtitle }}</span>
<span class="dashboard-section__description">{{ description }}</span>
<slot name="list" />
<div v-if="$slots.action" class="dashboard-section__action">
<slot name="action" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.dashboard-section {
display: flex;
border-radius: var(--border-radius-large);
overflow: hidden;
border: 2px solid var(--color-border);
height: 100%;
&--wide {
flex-direction: row;
.dashboard-section__content {
justify-content: center;
}
}
&__content {
position: relative;
display: flex;
flex-direction: column;
flex: auto;
min-height: 0;
padding: 0 calc(var(--default-grid-baseline) * 3) calc(var(--default-grid-baseline) * 2) calc(var(--default-grid-baseline) * 5);
}
&__bar {
flex: 0 0 200px;
&.gradient {
background: linear-gradient(78deg, var(--color-primary) 60%, var(--color-main-background) 120%);
}
&--narrow {
flex: 0 0 10px;
}
// Style for slotted images
:deep(img) {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
&.image-container {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
}
&__title {
font-size: 1.25rem;
font-weight: bold;
}
&__subtitle {
font-weight: bold;
}
&__action {
padding-block: calc(var(--default-grid-baseline) * 2);
}
}
h3 {
margin-block: calc(var(--default-grid-baseline) * 2);
}
</style>

4
src/components/Dashboard/EventCard.vue

@ -246,7 +246,7 @@ function handleJoin({ call }: { call: boolean }) {
flex-direction: column;
flex: 0 0 100%;
max-width: 300px;
border: 3px solid var(--color-border);
border: 2px solid var(--color-border);
padding: calc(var(--default-grid-baseline) * 2);
border-radius: var(--border-radius-large);
background-color: var(--color-main-background);
@ -255,7 +255,7 @@ function handleJoin({ call }: { call: boolean }) {
background-color: var(--color-primary-light);
&:not(.event-card--in-call) {
border-color: var(--color-primary-light) !important;
border-color: var(--color-primary-element-light-hover) !important;
}
}

97
src/components/Dashboard/Section.vue

@ -1,97 +0,0 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script lang="ts" setup>
import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile'
const { wide = false, title = '', subtitle = '', description = '' } = defineProps<{
wide?: boolean
title?: string
subtitle?: string
description?: string
}>()
const isMobile = useIsMobile()
</script>
<template>
<div class="dashboard-section"
:class="{
'dashboard-section--wide': wide,
'dashboard-section--list': $slots.list,
}">
<div v-if="!isMobile"
class="dashboard-section__bar"
:class="{ 'dashboard-section__bar--narrow': $slots.list }" />
<div class="dashboard-section__content-wrapper">
<div class="dashboard-section__content">
<h3 class="dashboard-section__title">
{{ title }}
</h3>
<span class="dashboard-section__subtitle">{{ subtitle }}</span>
<span class="dashboard-section__description">{{ description }}</span>
<slot name="list" />
<div v-if="$slots.action" class="dashboard-section__action">
<slot name="action" />
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.dashboard-section {
display: flex;
border-radius: var(--border-radius-large);
overflow: hidden;
box-shadow: 0 2px 10px 0 rgba(0,0,0, 0.2);
height: 100%;
flex-direction: column;
&--wide {
flex-direction: row;
}
&__content-wrapper {
display: flex;
flex: auto;
height: 96%; // bar is 4%
padding: var(--default-grid-baseline);
}
&__content {
position: relative;
display: flex;
flex-direction: column;
flex-grow: inherit;
padding: 0 calc(var(--default-grid-baseline) * 3) calc(var(--default-grid-baseline) * 2) calc(var(--default-grid-baseline) * 4);
}
&__bar {
background: linear-gradient(100deg, var(--color-primary) 0%, var(--color-main-background) 130%);
flex: 0 0 50%;
&--narrow {
flex: 0 0 4%;
}
}
&__title {
font-size: 1.25rem;
font-weight: bold;
}
&__subtitle {
font-weight: bold;
}
&__action {
margin-top: auto;
}
}
h3 {
margin-block: calc(var(--default-grid-baseline) * 2);
}
</style>

183
src/components/Dashboard/TalkDashboard.vue

@ -6,8 +6,8 @@
import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { isRTL, t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile'
import { generateUrl, imagePath } from '@nextcloud/router'
import { useIsMobile, useIsSmallMobile } from '@nextcloud/vue/composables/useIsMobile'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
@ -25,8 +25,8 @@ import IconVideoOutline from 'vue-material-design-icons/VideoOutline.vue'
import ConversationsListVirtual from '../LeftSidebar/ConversationsList/ConversationsListVirtual.vue'
import SearchMessageItem from '../RightSidebar/SearchMessages/SearchMessageItem.vue'
import LoadingPlaceholder from '../UIShared/LoadingPlaceholder.vue'
import DashboardSection from './DashboardSection.vue'
import EventCard from './EventCard.vue'
import Section from './Section.vue'
import { CONVERSATION } from '../../constants.ts'
import { getTalkConfig, hasTalkFeature } from '../../services/CapabilitiesManager.ts'
import { EventBus } from '../../services/EventBus.ts'
@ -43,6 +43,7 @@ const canModerateSipDialOut = hasTalkFeature('local', 'sip-support-dialout')
const canStartConversations = getTalkConfig('local', 'conversations', 'can-create')
const isDirectionRTL = isRTL()
const isMobile = useIsMobile()
const isSmallMobile = useIsSmallMobile()
const store = useStore()
const router = useRouter()
@ -178,49 +179,53 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
<template>
<div class="talk-dashboard-wrapper"
:class="{ 'talk-dashboard-wrapper--mobile': isMobile }">
<h2 class="talk-dashboard__header">
{{ t('spreed', 'Hello, {displayName}', { displayName: actorStore.displayName }, { escape: false }) }}
</h2>
<div class="talk-dashboard__actions">
<NcPopover v-if="canStartConversations"
popup-role="dialog">
<template #trigger>
<NcButton variant="primary">
<template #icon>
<IconVideoOutline />
</template>
{{ t('spreed', 'Start meeting now') }}
</NcButton>
</template>
<div role="dialog"
aria-labelledby="instant_meeting_dialog"
class="instant-meeting__dialog"
aria-modal="true">
<strong>{{ t('spreed', 'Give your meeting a title') }}</strong>
<NcInputField id="room-name"
v-model="conversationName"
:placeholder="t('spreed', 'Meeting')" />
<NcButton variant="primary"
@click="startMeeting">
{{ t('spreed', 'Create and copy link') }}
</NcButton>
</div>
</NcPopover>
<NcButton v-if="canStartConversations"
@click="EventBus.emit('new-conversation-dialog:show')">
<template #icon>
<IconPlus :size="20" />
</template>
{{ t('spreed', 'Create a new conversation') }}
</NcButton>
:class="{
'talk-dashboard-wrapper--mobile': isMobile,
'talk-dashboard-wrapper--small-mobile': isSmallMobile,
}">
<div class="talk-dashboard__menu">
<h2 class="talk-dashboard__header">
{{ t('spreed', 'Hello, {displayName}', { displayName: actorStore.displayName }, { escape: false }) }}
</h2>
<div class="talk-dashboard__actions">
<NcPopover v-if="canStartConversations"
popup-role="dialog">
<template #trigger>
<NcButton variant="primary">
<template #icon>
<IconVideoOutline />
</template>
{{ t('spreed', 'Start meeting now') }}
</NcButton>
</template>
<div role="dialog"
aria-labelledby="instant_meeting_dialog"
class="instant-meeting__dialog"
aria-modal="true">
<strong>{{ t('spreed', 'Give your meeting a title') }}</strong>
<NcInputField id="room-name"
v-model="conversationName"
:placeholder="t('spreed', 'Meeting')" />
<NcButton variant="primary"
@click="startMeeting">
{{ t('spreed', 'Create and copy link') }}
</NcButton>
</div>
</NcPopover>
<NcButton v-if="canStartConversations"
@click="EventBus.emit('new-conversation-dialog:show')">
<template #icon>
<IconPlus :size="20" />
</template>
{{ t('spreed', 'Create a new conversation') }}
</NcButton>
<NcButton @click="EventBus.emit('open-conversations-list:show')">
<template #icon>
<IconList :size="20" />
</template>
{{ t('spreed', 'Join open conversations') }}
</NcButton>
<NcButton @click="EventBus.emit('open-conversations-list:show')">
<template #icon>
<IconList :size="20" />
</template>
{{ t('spreed', 'Join open conversations') }}
</NcButton>
<NcButton v-if="canModerateSipDialOut"
@click="EventBus.emit('call-phone-dialog:show')">
@ -315,7 +320,7 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
</DashboardSection>
<DashboardSection v-else
:title="t('spreed', 'Unread mentions')"
:description="t('spreed', 'Messages where you were mentioned will show up here\. You can mention people by typing @ followed by their name')">
:description="t('spreed', 'Messages where you were mentioned will show up here. You can mention people by typing @ followed by their name')">
<template #image>
<img :src="imagePath('spreed', 'dashboard/mentions.png')">
</template>
@ -366,25 +371,38 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
@import '../../assets/variables';
.talk-dashboard-wrapper {
--title-height: calc(var(--default-clickable-area) + var(--default-grid-baseline) * 3); // '.title' height
--section-width: 300px;
--section-height: 300px;
--content-height: calc(100% - var(--title-height));
padding: 0 calc(var(--default-grid-baseline) * 3);
max-width: calc(100vw - 300px - var(--body-container-margin) * 2); // 300px for the left sidebar and body container margins
padding: calc(var(--default-grid-baseline) * 2) calc(var(--default-grid-baseline) * 3);
width: calc(100vw - 300px - var(--body-container-margin) * 2); // 300px for the left sidebar and body container margins
margin: 0 auto;
display: flex;
flex-direction: column;
height: 100%;
max-height: 800px;
max-width: 1200px;
&--mobile {
max-width: 100%;
width: 100%;
}
&--small-mobile {
width: 100%;
height: auto;
.talk-dashboard__chats {
grid-template-columns: 1fr;
gap: calc(var(--default-grid-baseline) * 6);
}
}
}
.talk-dashboard__menu {
margin-bottom: calc(var(--default-grid-baseline) * 5);
}
.talk-dashboard__header {
font-size: 21px; // NcDialog header font size
font-weight: bold;
height: 51px; // top bar height
line-height: 51px;
margin: 0 auto;
margin: 0 auto calc(var(--default-grid-baseline) * 2);
padding-inline-start: calc(var(--default-clickable-area) + var(--default-grid-baseline)); // navigation button
}
@ -393,6 +411,15 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
gap: calc(var(--default-grid-baseline) * 3);
padding-block: var(--default-grid-baseline);
flex-wrap: wrap;
justify-content: space-evenly;
:deep(.button-vue),
:deep(.v-popper--theme-dropdown) {
height: var(--header-menu-item-height);
border-radius: var(--border-radius-large);
flex: 1;
width: 100%;
}
:deep(.button-vue) {
padding-inline: calc(var(--default-grid-baseline) * 2) calc(var(--default-grid-baseline) * 4);
@ -400,7 +427,7 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
}
.event-section {
margin-block: calc(var(--default-grid-baseline) * 8);
margin-block-end: calc(var(--default-grid-baseline) * 6);
&--empty {
height: 225px;
@ -460,15 +487,22 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
inset-inline-start: calc(var(--default-grid-baseline) * 2);
}
.talk-dashboard__chats {
.talk-dashboard__items {
display: flex;
flex-direction: column;
justify-content: space-around;
min-width: 0;
flex-grow: 3;
}
.talk-dashboard__chats {
display: grid;
gap: calc(var(--default-grid-baseline) * 8);
justify-content: space-between;
flex-direction: row;
height: 300px;
grid-template-columns: 1fr 1fr;
flex-grow: 1;
&> div {
width: calc(50% - calc(var(--default-grid-baseline) * 4));
max-height: 360px;
}
}
@ -479,33 +513,26 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
&__loading-placeholder {
overflow: hidden;
height: var(--content-height);
}
}
.talk-dashboard__conversations-list {
margin: var(--default-grid-baseline) 0;
height: var(--content-height);
height: 225px;
line-height: 20px;
}
.title {
font-weight: bold;
font-size: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
height: var(--default-clickable-area);
margin-block: calc(var(--default-grid-baseline) * 2) var(--default-grid-baseline);
margin-inline: var(--default-grid-baseline);
font-size: 1.25rem;
font-weight: bold;
margin-block: 0 calc(var(--default-grid-baseline) * 2);
}
.instant-meeting__dialog {
padding: 8px;
padding: calc(var(--default-grid-baseline) * 2);
display: flex;
flex-direction: column;
gap: 4px;
gap: var(--default-grid-baseline) ;
align-items: center;
}
@ -514,7 +541,11 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
.talk-dashboard__actions {
:deep(.button-vue),
:deep(.v-popper--theme-dropdown) {
width: 100%;
flex: initial;
}
:deep(.button-vue) {
padding-inline-end: calc(var(--default-grid-baseline) * 2);
}
}
}

Loading…
Cancel
Save