diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-03-03 15:35:40 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-03 15:35:40 +0900 |
| commit | 5bd68aa3e0ce65a9183e193ff113b2486021f679 (patch) | |
| tree | 7cce90cfd87cbdfc932226273a6a89aa7871b33e /packages/frontend/src/components | |
| parent | Merge pull request #10112 from misskey-dev/develop (diff) | |
| parent | fix CHANGELOG.md (diff) | |
| download | misskey-5bd68aa3e0ce65a9183e193ff113b2486021f679.tar.gz misskey-5bd68aa3e0ce65a9183e193ff113b2486021f679.tar.bz2 misskey-5bd68aa3e0ce65a9183e193ff113b2486021f679.zip | |
Merge pull request #10177 from misskey-dev/develop
Release: 13.9.0
Diffstat (limited to 'packages/frontend/src/components')
| -rw-r--r-- | packages/frontend/src/components/MkCropperDialog.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkFolder.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkMediaList.vue | 23 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkMenu.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNote.vue | 119 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNoteDetailed.vue | 25 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNoteHeader.vue | 1 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkPostForm.vue | 10 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkTab.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkToast.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkUserPopup.vue | 222 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkUserPreview.vue | 199 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkWindow.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/components/global/MkPageHeader.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/global/MkStickyContainer.vue | 42 |
15 files changed, 399 insertions, 262 deletions
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index d1b5cc5118..043a614e46 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -18,7 +18,7 @@ </div> </Transition> <div class="container"> - <img ref="imgEl" :src="imgUrl" style="display: none;" crossorigin="anonymous" @load="onImageLoad"> + <img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad"> </div> </div> </template> @@ -49,7 +49,7 @@ const props = defineProps<{ aspectRatio: number; }>(); -const imgUrl = getProxiedImageUrl(props.file.url); +const imgUrl = getProxiedImageUrl(props.file.url, undefined, true); let dialogEl = $shallowRef<InstanceType<typeof MkModalWindow>>(); let imgEl = $shallowRef<HTMLImageElement>(); let cropper: Cropper | null = null; diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index b97e36cd5f..a54a1c2305 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -43,8 +43,8 @@ import { nextTick, onMounted } from 'vue'; const props = withDefaults(defineProps<{ - defaultOpen: boolean; - maxHeight: number | null; + defaultOpen?: boolean; + maxHeight?: number | null; }>(), { defaultOpen: false, maxHeight: null, diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index fafa0bd232..c768a086cd 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -13,7 +13,7 @@ </template> <script lang="ts" setup> -import { onMounted, ref } from 'vue'; +import { onMounted, ref, useCssModule } from 'vue'; import * as misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipe from 'photoswipe'; @@ -29,8 +29,11 @@ const props = defineProps<{ raw?: boolean; }>(); +const $style = useCssModule(); + const gallery = ref(null); const pswpZIndex = os.claimZIndex('middle'); +document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString()); const count = $computed(() => props.mediaList.filter(media => previewable(media)).length); onMounted(() => { @@ -54,17 +57,18 @@ onMounted(() => { return item; }), gallery: gallery.value, + mainClass: $style.pswp, children: '.image', thumbSelector: '.image', loop: false, padding: window.innerWidth > 500 ? { top: 32, - bottom: 32, + bottom: 90, left: 32, right: 32, } : { top: 0, - bottom: 0, + bottom: 78, left: 0, right: 0, }, @@ -82,6 +86,7 @@ onMounted(() => { const id = element.dataset.id; const file = props.mediaList.find(media => media.id === id); + if (!file) return; itemData.src = file.url; itemData.w = Number(file.properties.width); @@ -198,16 +203,14 @@ const previewable = (file: misskey.entities.DriveFile): boolean => { overflow: hidden; // clipにするとバグる border-radius: 8px; } -</style> -<style lang="scss"> .pswp { - // なぜか機能しない - //z-index: v-bind(pswpZIndex); - z-index: 2000000; - --pswp-bg: var(--modalBg); + --pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important; + --pswp-bg: var(--modalBg) !important; } +</style> +<style lang="scss"> .pswp__bg { background: var(--modalBg); backdrop-filter: var(--modalBgFilter); @@ -219,7 +222,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => { align-items: center; position: absolute; - bottom: 30px; + bottom: 20px; left: 50%; transform: translateX(-50%); diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 09d530c4ea..9e3022896c 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -36,7 +36,7 @@ <button v-else-if="item.type === 'parent'" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <span>{{ item.text }}</span> - <span :class="$style.caret"><i class="ti ti-caret-right ti-fw"></i></span> + <span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span> </button> <button v-else :tabindex="i" class="_button" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 1040dac12e..bb1269562d 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -4,7 +4,7 @@ v-show="!isDeleted" ref="el" v-hotkey="keymap" - :class="$style.root" + :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]" :tabindex="!isDeleted ? '-1' : undefined" > <MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> @@ -32,6 +32,7 @@ <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span> + <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span> </div> </div> <div v-if="renoteCollapsed" :class="$style.collapsedRenoteTarget"> @@ -76,14 +77,14 @@ </div> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </div> + <MkReactionsViewer :note="appearNote" :max-number="16"> + <template #more> + <button class="_button" :class="$style.reactionDetailsButton" @click="showReactions"> + {{ i18n.ts.more }} + </button> + </template> + </MkReactionsViewer> <footer :class="$style.footer"> - <MkReactionsViewer :note="appearNote" :max-number="16"> - <template #more> - <button class="_button" :class="$style.reactionDetailsButton" @click="showReactions"> - {{ i18n.ts.more }} - </button> - </template> - </MkReactionsViewer> <button :class="$style.footerButton" class="_button" @click="reply()"> <i class="ti ti-arrow-back-up"></i> <p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p> @@ -156,6 +157,7 @@ import { useTooltip } from '@/scripts/use-tooltip'; import { claimAchievement } from '@/scripts/achievements'; import { getNoteSummary } from '@/scripts/get-note-summary'; import { MenuItem } from '@/types/menu'; +import MkRippleEffect from '@/components/MkRippleEffect.vue'; const props = defineProps<{ note: misskey.entities.Note; @@ -255,9 +257,19 @@ function renote(viaKeyboard = false) { text: i18n.ts.inChannelRenote, icon: 'ti ti-repeat', action: () => { - os.apiWithDialog('notes/create', { + const el = renoteButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + + os.api('notes/create', { renoteId: appearNote.id, channelId: appearNote.channelId, + }).then(() => { + os.toast(i18n.ts.renoted); }); }, }, { @@ -276,8 +288,18 @@ function renote(viaKeyboard = false) { text: i18n.ts.renote, icon: 'ti ti-repeat', action: () => { - os.apiWithDialog('notes/create', { + const el = renoteButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + + os.api('notes/create', { renoteId: appearNote.id, + }).then(() => { + os.toast(i18n.ts.renoted); }); }, }, { @@ -443,6 +465,34 @@ function showReactions(): void { &:hover > .article > .main > .footer > .footerButton { opacity: 1; } + + &.showActionsOnlyHover { + .footer { + visibility: hidden; + position: absolute; + top: 12px; + right: 12px; + padding: 0 4px; + margin-bottom: 0 !important; + background: var(--popup); + border-radius: 8px; + box-shadow: 0px 4px 32px var(--shadow); + } + + .footerButton { + font-size: 90%; + + &:not(:last-child) { + margin-right: 0; + } + } + } + + &.showActionsOnlyHover:hover { + .footer { + visibility: visible; + } + } } .tip { @@ -541,14 +591,15 @@ function showReactions(): void { } .article { + position: relative; display: flex; - padding: 28px 32px 18px; + padding: 28px 32px; } .avatar { flex-shrink: 0; display: block !important; - margin: 0 14px 8px 0; + margin: 0 14px 0 0; width: 58px; height: 58px; position: sticky !important; @@ -571,9 +622,9 @@ function showReactions(): void { .showLess { width: 100%; - margin-top: 1em; + margin-top: 14px; position: sticky; - bottom: 1em; + bottom: calc(var(--stickyBottom, 0px) + 14px); } .showLessLabel { @@ -653,6 +704,10 @@ function showReactions(): void { font-size: 80%; } +.footer { + margin-bottom: -14px; +} + .footerButton { margin: 0; padding: 8px; @@ -683,7 +738,7 @@ function showReactions(): void { } .article { - padding: 24px 26px 14px; + padding: 24px 26px; } .avatar { @@ -702,7 +757,11 @@ function showReactions(): void { } .article { - padding: 20px 22px 12px; + padding: 20px 22px; + } + + .footer { + margin-bottom: -8px; } } @@ -721,13 +780,13 @@ function showReactions(): void { } .article { - padding: 14px 16px 9px; + padding: 14px 16px; } } @container (max-width: 450px) { .avatar { - margin: 0 10px 8px 0; + margin: 0 10px 0 0; width: 46px; height: 46px; top: calc(14px + var(--stickyTop, 0px)); @@ -735,17 +794,21 @@ function showReactions(): void { } @container (max-width: 400px) { - .footerButton { - &:not(:last-child) { - margin-right: 18px; + .root:not(.showActionsOnlyHover) { + .footerButton { + &:not(:last-child) { + margin-right: 18px; + } } } } @container (max-width: 350px) { - .footerButton { - &:not(:last-child) { - margin-right: 12px; + .root:not(.showActionsOnlyHover) { + .footerButton { + &:not(:last-child) { + margin-right: 12px; + } } } } @@ -756,9 +819,11 @@ function showReactions(): void { height: 44px; } - .footerButton { - &:not(:last-child) { - margin-right: 8px; + .root:not(.showActionsOnlyHover) { + .footerButton { + &:not(:last-child) { + margin-right: 8px; + } } } } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 2eebe999a5..f5f4a2afc1 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -161,6 +161,7 @@ import { deepClone } from '@/scripts/clone'; import { useTooltip } from '@/scripts/use-tooltip'; import { claimAchievement } from '@/scripts/achievements'; import { MenuItem } from '@/types/menu'; +import MkRippleEffect from '@/components/MkRippleEffect.vue'; const props = defineProps<{ note: misskey.entities.Note; @@ -250,9 +251,19 @@ function renote(viaKeyboard = false) { text: i18n.ts.inChannelRenote, icon: 'ti ti-repeat', action: () => { - os.apiWithDialog('notes/create', { + const el = renoteButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + + os.api('notes/create', { renoteId: appearNote.id, channelId: appearNote.channelId, + }).then(() => { + os.toast(i18n.ts.renoted); }); }, }, { @@ -271,8 +282,18 @@ function renote(viaKeyboard = false) { text: i18n.ts.renote, icon: 'ti ti-repeat', action: () => { - os.apiWithDialog('notes/create', { + const el = renoteButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + + os.api('notes/create', { renoteId: appearNote.id, + }).then(() => { + os.toast(i18n.ts.renoted); }); }, }, { diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index ffd9a20ef7..15d7ea2e14 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -18,6 +18,7 @@ <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span> + <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span> </div> </header> </template> diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index f73eab5b86..2b3e2c8646 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -437,8 +437,8 @@ function clear() { } function onKeydown(ev: KeyboardEvent) { - if ((ev.which === 10 || ev.which === 13) && (ev.ctrlKey || ev.metaKey) && canPost) post(); - if (ev.which === 27) emit('esc'); + if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost) post(); + if (ev.key === 'Escape') emit('esc'); } function onCompositionUpdate(ev: CompositionEvent) { @@ -489,9 +489,9 @@ function onDragover(ev) { switch (ev.dataTransfer.effectAllowed) { case 'all': case 'uninitialized': - case 'copy': - case 'copyLink': - case 'copyMove': + case 'copy': + case 'copyLink': + case 'copyMove': ev.dataTransfer.dropEffect = 'copy'; break; case 'linkMove': diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue index 81cbde0ac7..6f819bbbd7 100644 --- a/packages/frontend/src/components/MkTab.vue +++ b/packages/frontend/src/components/MkTab.vue @@ -34,7 +34,7 @@ export default defineComponent({ > button { flex: 1; padding: 10px 8px; - border-radius: var(--radius); + border-radius: 999px; &:disabled { opacity: 1 !important; diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue index 1aa48f88e6..6d59702569 100644 --- a/packages/frontend/src/components/MkToast.vue +++ b/packages/frontend/src/components/MkToast.vue @@ -53,7 +53,7 @@ onMounted(() => { position: fixed; left: 0; right: 0; - top: 0; + top: 50px; margin: 0 auto; margin-top: 16px; min-width: 300px; diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue new file mode 100644 index 0000000000..93e914f6dd --- /dev/null +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -0,0 +1,222 @@ +<template> +<Transition + :enter-active-class="$store.state.animation ? $style.transition_popup_enterActive : ''" + :leave-active-class="$store.state.animation ? $style.transition_popup_leaveActive : ''" + :enter-from-class="$store.state.animation ? $style.transition_popup_enterFrom : ''" + :leave-to-class="$store.state.animation ? $style.transition_popup_leaveTo : ''" + appear @after-leave="emit('closed')" +> + <div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }"> + <div v-if="user != null"> + <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"> + <span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ $ts.followsYou }}</span> + </div> + <svg viewBox="0 0 128 128" :class="$style.avatarBack"> + <g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)"> + <path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--popup);"/> + </g> + </svg> + <MkAvatar :class="$style.avatar" :user="user" indicator/> + <div :class="$style.title"> + <MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> + <div :class="$style.username"><MkAcct :user="user"/></div> + </div> + <div :class="$style.description"> + <Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/> + <div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div> + </div> + <div :class="$style.status"> + <div :class="$style.statusItem"> + <div :class="$style.statusItemLabel">{{ $ts.notes }}</div> + <div>{{ number(user.notesCount) }}</div> + </div> + <div :class="$style.statusItem"> + <div :class="$style.statusItemLabel">{{ $ts.following }}</div> + <div>{{ number(user.followingCount) }}</div> + </div> + <div :class="$style.statusItem"> + <div :class="$style.statusItemLabel">{{ $ts.followers }}</div> + <div>{{ number(user.followersCount) }}</div> + </div> + </div> + <button class="_button" :class="$style.menu" @click="showMenu"><i class="ti ti-dots"></i></button> + <MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/> + </div> + <div v-else> + <MkLoading/> + </div> + </div> +</Transition> +</template> + +<script lang="ts" setup> +import { onMounted } from 'vue'; +import * as Acct from 'misskey-js/built/acct'; +import * as misskey from 'misskey-js'; +import MkFollowButton from '@/components/MkFollowButton.vue'; +import { userPage } from '@/filters/user'; +import * as os from '@/os'; +import { getUserMenu } from '@/scripts/get-user-menu'; +import number from '@/filters/number'; +import { i18n } from '@/i18n'; + +const props = defineProps<{ + showing: boolean; + q: string; + source: HTMLElement; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; + (ev: 'mouseover'): void; + (ev: 'mouseleave'): void; +}>(); + +const zIndex = os.claimZIndex('middle'); +let user = $ref<misskey.entities.UserDetailed | null>(null); +let top = $ref(0); +let left = $ref(0); + +function showMenu(ev: MouseEvent) { + os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target); +} + +onMounted(() => { + if (typeof props.q === 'object') { + user = props.q; + } else { + const query = props.q.startsWith('@') ? + Acct.parse(props.q.substr(1)) : + { userId: props.q }; + + os.api('users/show', query).then(res => { + if (!props.showing) return; + user = res; + }); + } + + const rect = props.source.getBoundingClientRect(); + const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset; + const y = rect.top + props.source.offsetHeight + window.pageYOffset; + + top = y; + left = x; +}); +</script> + +<style lang="scss" module> +.transition_popup_enterActive, +.transition_popup_leaveActive { + transition: opacity 0.15s, transform 0.15s !important; +} +.transition_popup_enterFrom, +.transition_popup_leaveTo { + opacity: 0; + transform: scale(0.9); +} + +.root { + position: absolute; + width: 300px; + overflow: clip; + transform-origin: center top; +} + +.banner { + height: 78px; + background-color: rgba(0, 0, 0, 0.1); + background-size: cover; + background-position: center; +} + +.followed { + position: absolute; + top: 12px; + left: 12px; + padding: 4px 8px; + color: #fff; + background: rgba(0, 0, 0, 0.7); + font-size: 0.7em; + border-radius: 6px; +} + +.avatarBack { + width: 100px; + position: absolute; + top: 28px; + left: 0; + right: 0; + margin: 0 auto; +} + +.avatar { + display: block; + position: absolute; + top: 38px; + left: 0; + right: 0; + margin: 0 auto; + z-index: 2; + width: 58px; + height: 58px; +} + +.title { + position: relative; + z-index: 3; + display: block; + padding: 8px 26px 16px 26px; + margin-top: 16px; + text-align: center; +} + +.name { + display: inline-block; + font-weight: bold; + word-break: break-all; +} + +.username { + display: block; + font-size: 0.8em; + opacity: 0.7; +} + +.description { + padding: 16px 26px; + font-size: 0.8em; + text-align: center; + border-top: solid 1px var(--divider); + border-bottom: solid 1px var(--divider); +} + +.status { + padding: 16px 26px 16px 26px; +} + +.statusItem { + display: inline-block; + width: 33%; + text-align: center; +} + +.statusItemLabel { + font-size: 0.7em; + color: var(--fgTransparentWeak); +} + +.menu { + position: absolute; + top: 8px; + right: 44px; + padding: 6px; + background: var(--panel); + border-radius: 999px; +} + +.follow { + position: absolute !important; + top: 8px; + right: 8px; +} +</style> diff --git a/packages/frontend/src/components/MkUserPreview.vue b/packages/frontend/src/components/MkUserPreview.vue deleted file mode 100644 index 1086a2c651..0000000000 --- a/packages/frontend/src/components/MkUserPreview.vue +++ /dev/null @@ -1,199 +0,0 @@ -<template> -<Transition :name="$store.state.animation ? 'popup' : ''" appear @after-leave="emit('closed')"> - <div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }"> - <div v-if="user != null" class="info"> - <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"> - <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span> - </div> - <MkAvatar class="avatar" :user="user" indicator/> - <div class="title"> - <MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> - <p class="username"><MkAcct :user="user"/></p> - </div> - <div class="description"> - <Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/> - </div> - <div class="status"> - <div> - <p>{{ $ts.notes }}</p><span>{{ user.notesCount }}</span> - </div> - <div> - <p>{{ $ts.following }}</p><span>{{ user.followingCount }}</span> - </div> - <div> - <p>{{ $ts.followers }}</p><span>{{ user.followersCount }}</span> - </div> - </div> - <button class="menu _button" @click="showMenu"><i class="ti ti-dots"></i></button> - <MkFollowButton v-if="$i && user.id != $i.id" class="koudoku-button" :user="user" mini/> - </div> - <div v-else> - <MkLoading/> - </div> - </div> -</Transition> -</template> - -<script lang="ts" setup> -import { onMounted } from 'vue'; -import * as Acct from 'misskey-js/built/acct'; -import * as misskey from 'misskey-js'; -import MkFollowButton from '@/components/MkFollowButton.vue'; -import { userPage } from '@/filters/user'; -import * as os from '@/os'; -import { getUserMenu } from '@/scripts/get-user-menu'; - -const props = defineProps<{ - showing: boolean; - q: string; - source: HTMLElement; -}>(); - -const emit = defineEmits<{ - (ev: 'closed'): void; - (ev: 'mouseover'): void; - (ev: 'mouseleave'): void; -}>(); - -const zIndex = os.claimZIndex('middle'); -let user = $ref<misskey.entities.UserDetailed | null>(null); -let top = $ref(0); -let left = $ref(0); - -function showMenu(ev: MouseEvent) { - os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target); -} - -onMounted(() => { - if (typeof props.q === 'object') { - user = props.q; - } else { - const query = props.q.startsWith('@') ? - Acct.parse(props.q.substr(1)) : - { userId: props.q }; - - os.api('users/show', query).then(res => { - if (!props.showing) return; - user = res; - }); - } - - const rect = props.source.getBoundingClientRect(); - const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset; - const y = rect.top + props.source.offsetHeight + window.pageYOffset; - - top = y; - left = x; -}); -</script> - -<style lang="scss" scoped> -.popup-enter-active, .popup-leave-active { - transition: opacity 0.15s, transform 0.15s !important; -} -.popup-enter-from, .popup-leave-to { - opacity: 0; - transform: scale(0.9); -} - -.fxxzrfni { - position: absolute; - width: 300px; - overflow: hidden; - transform-origin: center top; - - > .info { - > .banner { - height: 84px; - background-color: rgba(0, 0, 0, 0.1); - background-size: cover; - background-position: center; - > .followed { - position: absolute; - top: 12px; - left: 12px; - padding: 4px 8px; - color: #fff; - background: rgba(0, 0, 0, 0.7); - font-size: 0.7em; - border-radius: 6px; - } - } - - > .avatar { - display: block; - position: absolute; - top: 62px; - left: 13px; - z-index: 2; - width: 58px; - height: 58px; - border: solid 3px var(--face); - border-radius: 8px; - } - - > .title { - display: block; - padding: 8px 0 8px 82px; - - > .name { - display: inline-block; - margin: 0; - font-weight: bold; - line-height: 16px; - word-break: break-all; - } - - > .username { - display: block; - margin: 0; - line-height: 16px; - font-size: 0.8em; - color: var(--fg); - opacity: 0.7; - } - } - - > .description { - padding: 0 16px; - font-size: 0.8em; - color: var(--fg); - } - - > .status { - padding: 8px 16px; - - > div { - display: inline-block; - width: 33%; - - > p { - margin: 0; - font-size: 0.7em; - color: var(--fg); - } - - > span { - font-size: 1em; - color: var(--accent); - } - } - } - - > .menu { - position: absolute; - top: 8px; - right: 44px; - padding: 6px; - background: var(--panel); - border-radius: 999px; - } - - > .koudoku-button { - position: absolute; - top: 8px; - right: 8px; - } - } -} -</style> diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 7e53507f2e..e7ad2b9a43 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -23,7 +23,7 @@ <button v-if="closeButton" v-tooltip="i18n.ts.close" class="_button" :class="$style.headerButton" @click="close()"><i class="ti ti-x"></i></button> </span> </div> - <div :class="$style.content"> + <div v-container :class="$style.content"> <slot></slot> </div> </div> @@ -465,7 +465,7 @@ defineExpose({ -webkit-backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px)); //border-bottom: solid 1px var(--divider); - font-size: 95%; + font-size: 90%; font-weight: bold; } diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 589ca92d75..4d968db6a3 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -96,7 +96,7 @@ function onTabClick(): void { } const calcBg = () => { - const rawBg = metadata?.bg ?? 'var(--bg)'; + const rawBg = 'var(--bg)'; const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); tinyBg.setAlpha(0.85); bg.value = tinyBg.toRgbString(); diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue index a3fee91a36..44c02088da 100644 --- a/packages/frontend/src/components/global/MkStickyContainer.vue +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -6,20 +6,19 @@ <div ref="bodyEl" :data-sticky-container-header-height="headerHeight"> <slot></slot> </div> + <div ref="footerEl"> + <slot name="footer"></slot> + </div> </div> </template> -<script lang="ts"> -// なんか動かない -//const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP'); -const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP'; -</script> - <script lang="ts" setup> import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue'; +import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const'; const rootEl = $shallowRef<HTMLElement>(); const headerEl = $shallowRef<HTMLElement>(); +const footerEl = $shallowRef<HTMLElement>(); const bodyEl = $shallowRef<HTMLElement>(); let headerHeight = $ref<string | undefined>(); @@ -27,9 +26,23 @@ let childStickyTop = $ref(0); const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0)); provide(CURRENT_STICKY_TOP, $$(childStickyTop)); +let footerHeight = $ref<string | undefined>(); +let childStickyBottom = $ref(0); +const parentStickyBottom = inject<Ref<number>>(CURRENT_STICKY_BOTTOM, ref(0)); +provide(CURRENT_STICKY_BOTTOM, $$(childStickyBottom)); + const calc = () => { - childStickyTop = parentStickyTop.value + headerEl.offsetHeight; - headerHeight = headerEl.offsetHeight.toString(); + // コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる + if (headerEl != null) { + childStickyTop = parentStickyTop.value + headerEl.offsetHeight; + headerHeight = headerEl.offsetHeight.toString(); + } + + // コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる + if (footerEl != null) { + childStickyBottom = parentStickyBottom.value + footerEl.offsetHeight; + footerHeight = footerEl.offsetHeight.toString(); + } }; const observer = new ResizeObserver(() => { @@ -41,7 +54,7 @@ const observer = new ResizeObserver(() => { onMounted(() => { calc(); - watch(parentStickyTop, calc); + watch([parentStickyTop, parentStickyBottom], calc); watch($$(childStickyTop), () => { bodyEl.style.setProperty('--stickyTop', `${childStickyTop}px`); @@ -49,11 +62,22 @@ onMounted(() => { immediate: true, }); + watch($$(childStickyBottom), () => { + bodyEl.style.setProperty('--stickyBottom', `${childStickyBottom}px`); + }, { + immediate: true, + }); + headerEl.style.position = 'sticky'; headerEl.style.top = 'var(--stickyTop, 0)'; headerEl.style.zIndex = '1000'; + footerEl.style.position = 'sticky'; + footerEl.style.bottom = 'var(--stickyBottom, 0)'; + footerEl.style.zIndex = '1000'; + observer.observe(headerEl); + observer.observe(footerEl); }); onUnmounted(() => { |