From 1ed9438972ad67ca57e52b0bf8a07f2f044066f9 Mon Sep 17 00:00:00 2001 From: freeplay <freeplay@duck.com> Date: Thu, 13 Jul 2023 19:26:44 -0400 Subject: [PATCH] style: user card design --- packages/client/src/components/MkUserInfo.vue | 320 +++++++++++++----- 1 file changed, 244 insertions(+), 76 deletions(-) diff --git a/packages/client/src/components/MkUserInfo.vue b/packages/client/src/components/MkUserInfo.vue index 9c2763e53..9ff5f8b6c 100644 --- a/packages/client/src/components/MkUserInfo.vue +++ b/packages/client/src/components/MkUserInfo.vue @@ -1,151 +1,319 @@ <template> - <div class="_panel vjnjpkug"> + <article class="_panel user-card"> <div class="banner" :style=" user.bannerUrl ? `background-image: url(${user.bannerUrl})` : '' " + :class="{ detailed }" ></div> - <MkAvatar - class="avatar" - :user="user" - :disable-preview="true" - :show-indicator="true" - /> - <div class="title"> + <h3 class="title"> + <MkAvatar + class="avatar" + :user="user" + :disable-preview="true" + :show-indicator="true" + /> <MkA class="name" :to="userPage(user)" - ><MkUserName :user="user" :nowrap="false" + ><MkUserName :user="user" :nowrap="true" /></MkA> <p class="username"><MkAcct :user="user" /></p> - </div> - <div class="description"> - <div v-if="user.description" class="mfm"> - <Mfm - :text="user.description" - :author="user" - :i="$i" - :custom-emojis="user.emojis" - /> - </div> + </h3> + <div + class="description" + :class="{ collapsed: isLong && collapsed, truncate: !detailed }" + > + <Mfm + v-if="user.description" + class="mfm" + :text="user.description" + :author="user" + :i="$i" + :custom-emojis="user.emojis" + /> <span v-else style="opacity: 0.7">{{ i18n.ts.noAccountDescription }}</span> </div> + <XShowMoreButton + v-if="isLong" + v-model="collapsed" + ></XShowMoreButton> + <div v-if="user.fields.length > 0 && detailed" class="fields"> + <dl + v-for="(field, i) in user.fields" + :key="i" + class="field" + > + <dt class="name"> + <Mfm + :text="field.name" + :plain="true" + :custom-emojis="user.emojis" + :colored="false" + /> + </dt> + <dd class="value"> + <Mfm + :text="field.value" + :author="user" + :i="$i" + :custom-emojis="user.emojis" + :colored="false" + /> + </dd> + </dl> + </div> <div class="status"> - <div> - <p>{{ i18n.ts.notes }}</p> + <p> <MkNumber :value="user.notesCount" /> - </div> - <div> - <p>{{ i18n.ts.following }}</p> + {{ i18n.ts.notes }} + </p> + <p> <MkNumber :value="user.followingCount" /> - </div> - <div> - <p>{{ i18n.ts.followers }}</p> + {{ i18n.ts.following }} + </p> + <p> <MkNumber :value="user.followersCount" /> - </div> + {{ i18n.ts.followers }} + </p> </div> - <div class="koudoku-button"> - <MkFollowButton v-if="$i && user.id != $i.id" :user="user" /> + <div class="buttons"> + <slot> + <MkFollowButton v-if="$i && user.id != $i.id" :user="user" /> + </slot> </div> - </div> + </article> </template> <script lang="ts" setup> import * as misskey from "calckey-js"; import MkFollowButton from "@/components/MkFollowButton.vue"; +import XShowMoreButton from "@/components/MkShowMoreButton.vue"; import MkNumber from "@/components/MkNumber.vue"; import { userPage } from "@/filters/user"; import { i18n } from "@/i18n"; -defineProps<{ +const props = defineProps<{ user: misskey.entities.UserDetailed; + detailed?: boolean; }>(); + +let isLong = $ref( + props.detailed && + ( + props.user.description.split("\n").length > 9 || + props.user.description.length > 400 + ) +); +let collapsed = $ref(isLong); </script> <style lang="scss" scoped> -.vjnjpkug { +.user-card { position: relative; + display: flex; + flex-direction: column; - > .banner { - height: 84px; + .banner { + height: 94px; background-color: rgba(0, 0, 0, 0.1); background-size: cover; background-position: center; + margin-bottom: calc(0px - var(--radius)); + &::before { + content: ""; + position: absolute; + inset: 0; + z-index: 2; + height: inherit; + background: linear-gradient( + -125deg, + rgba(0, 0, 0, 0.7), + transparent, + transparent + ); + } + &.detailed::after { + content: ""; + background-image: var(--blur, inherit); + position: fixed; + inset: 0; + background-size: cover; + background-position: center; + pointer-events: none; + opacity: 0.1; + filter: var(--blur, blur(10px)); + z-index: 5; + } } - > .avatar { + + .title { + position: relative; display: block; - position: absolute; - top: 62px; - left: 13px; - z-index: 2; - width: 58px; - height: 58px; - border: solid 4px var(--panel); - } - - > .title { - display: block; - padding: 10px 0 10px 88px; - - > .name { + padding: 10px 10px 10px 84px; + margin: 0; + font-size: 1em; + border-radius: var(--radius); + background: var(--panel); + line-height: 1; + z-index: 4; + + .avatar { + display: block; + position: absolute; + bottom: 6px; + left: 10px; + z-index: 2; + width: 58px; + height: 58px; + background: var(--panel); + border: solid 4px var(--panel); + } + .name { display: inline-block; margin: 0; font-weight: bold; line-height: 16px; - word-break: break-all; + overflow: hidden; + text-overflow: ellipsis; + height: 1.5em; + margin-block: -0.5em; + padding-block: 0.5em 0.25em; + max-width: 100%; } - > .username { + .username { display: block; margin: 0; line-height: 16px; font-size: 0.8em; color: var(--fg); + font-weight: 500; opacity: 0.7; + line-height: 1; } } - > .description { - padding: 16px; + .description { + overflow: hidden; + padding-inline: 16px; + margin-bottom: 10px; font-size: 0.8em; - border-top: solid 0.5px var(--divider); - - > .mfm { + &.truncate { display: -webkit-box; - -webkit-line-clamp: 3; + -webkit-line-clamp: 6; -webkit-box-orient: vertical; - overflow: hidden; + } + &.collapsed { + position: relative; + max-height: calc(9em + 50px); + mask: linear-gradient(black calc(100% - 64px), transparent); + -webkit-mask: linear-gradient( + black calc(100% - 64px), + transparent + ); + } + &.collapsed, &.truncate { + :deep(br) { + display: none; // collapse white spaces + } } } - - > .status { - padding: 10px 16px; - border-top: solid 0.5px var(--divider); - - > div { + :deep(.fade) { + position: relative; + display: block; + width: 100%; + margin-top: -4.5em; + z-index: 2; + > span { display: inline-block; - width: 33%; - - > p { - margin: 0; - font-size: 0.7em; - color: var(--fg); - } - + background: var(--panel); + padding: 0.4em 1em; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); + } + &:hover { > span { - font-size: 1em; - color: var(--accent); + background: var(--panelHighlight); } } } + :deep(.showLess) { + width: 100%; + position: sticky; + bottom: var(--stickyBottom); - > .koudoku-button { + > span { + display: inline-block; + background: var(--panel); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 0 7px 7px var(--bg); + } + } + > .fields { + padding-inline: 16px; + font-size: 0.8em; + padding-block: 1em; + border-top: 1px solid var(--divider); + + > .field { + display: flex; + padding: 0; + margin: 0; + align-items: center; + + &:not(:last-child) { + margin-bottom: 8px; + } + + :deep(span) { + white-space: nowrap !important; + } + + > .name { + width: 30%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-weight: bold; + text-align: center; + padding-right: 10px; + } + + > .value { + width: 70%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin: 0; + } + } + } + .status { + display: flex; + gap: 1em; + padding-inline: 16px; + font-size: 0.8em; + margin-top: auto; + border-top: 1px solid var(--divider); + > p > :deep(span) { + font-weight: 700; + color: var(--accent); + } + } + + .buttons { position: absolute; top: 8px; right: 8px; margin-bottom: 1rem; + z-index: 3; + color: white; } } </style>