diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 373af28747..fcf9fb234d 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''" > <div v-if="drawerMenuShowing" :class="$style.menuDrawer"> - <XDrawerMenu/> + <XNavbar style="height: 100%;" :asDrawer="true" :showWidgetButton="false"/> </div> </Transition> @@ -113,7 +113,7 @@ import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; import { globalEvents } from '@/events.js'; import { store } from '@/store.js'; -import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; +import XNavbar from '@/ui/_common_/navbar.vue'; const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue')); const XUpload = defineAsyncComponent(() => import('./upload.vue')); @@ -230,12 +230,6 @@ if ($i) { left: 0; z-index: 1001; height: 100dvh; - width: 240px; - box-sizing: border-box; - contain: strict; - overflow: auto; - overscroll-behavior: contain; - background: var(--MI_THEME-navBg); } .widgetsDrawerBg { diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue deleted file mode 100644 index 480db8741b..0000000000 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ /dev/null @@ -1,387 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<div :class="$style.root"> - <div :class="$style.top"> - <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div> - <button class="_button" :class="$style.instance" @click="openInstanceMenu"> - <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/> - </button> - <button class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode"> - <i class="ti ti-bolt ti-fw"></i> - </button> - </div> - <div :class="$style.middle"> - <MkA :class="$style.item" :activeClass="$style.active" to="/" exact> - <i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> - </MkA> - <template v-for="item in prefer.r.menu.value"> - <div v-if="item === '-'" :class="$style.divider"></div> - <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> - <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span> - <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator" class="_blink"> - <span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span> - <i v-else class="_indicatorCircle"></i> - </span> - </component> - </template> - <div :class="$style.divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" :class="$style.item" :activeClass="$style.active" to="/admin"> - <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> - </MkA> - <button :class="$style.item" class="_button" @click="more"> - <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> - <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span> - </button> - <MkA :class="$style.item" :activeClass="$style.active" to="/settings"> - <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> - </MkA> - </div> - <div :class="$style.bottom"> - <button class="_button" :class="$style.post" data-cy-open-post-form @click="os.post"> - <i :class="$style.postIcon" class="ti ti-pencil ti-fw"></i><span style="position: relative;">{{ i18n.ts.note }}</span> - </button> - <button class="_button" :class="$style.account" @click="openAccountMenu"> - <MkAvatar :user="$i" :class="$style.avatar"/><MkAcct :class="$style.acct" class="_nowrap" :user="$i"/> - </button> - </div> -</div> -</template> - -<script lang="ts" setup> -import { computed, defineAsyncComponent } from 'vue'; -import { openInstanceMenu } from './common.js'; -import * as os from '@/os.js'; -import { navbarItemDef } from '@/navbar.js'; -import { prefer } from '@/preferences.js'; -import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; -import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; -import { $i } from '@/i.js'; -import { store } from '@/store.js'; - -const otherMenuItemIndicated = computed(() => { - for (const def in navbarItemDef) { - if (prefer.r.menu.value.includes(def)) continue; - if (navbarItemDef[def].indicated) return true; - } - return false; -}); - -function toggleRealtimeMode(ev: MouseEvent) { - os.popupMenu([{ - type: 'label', - text: i18n.ts.realtimeMode, - }, { - text: store.s.realtimeMode ? i18n.ts.turnItOff : i18n.ts.turnItOn, - icon: store.s.realtimeMode ? 'ti ti-bolt-off' : 'ti ti-bolt', - action: () => { - store.set('realtimeMode', !store.s.realtimeMode); - window.location.reload(); - }, - }], ev.currentTarget ?? ev.target); -} - -function openAccountMenu(ev: MouseEvent) { - openAccountMenu_({ - withExtraOperation: true, - }, ev); -} - -function more() { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {}, { - closed: () => dispose(), - }); -} -</script> - -<style lang="scss" module> -.root { - --nav-bg-transparent: color(from var(--MI_THEME-navBg) srgb r g b / 0.5); - - display: flex; - flex-direction: column; -} - -.top { - --top-height: 80px; - - position: sticky; - top: 0; - z-index: 1; - display: flex; - height: var(--top-height); - - /* 疑似progressive blur */ - &::before { - position: absolute; - z-index: -1; - inset: 0; - content: ""; - backdrop-filter: blur(8px); - mask-image: linear-gradient( - to top, - rgb(0 0 0 / 0%) 0%, - rgb(0 0 0 / 4.9%) 7.75%, - rgb(0 0 0 / 10.4%) 11.25%, - rgb(0 0 0 / 45%) 23.55%, - rgb(0 0 0 / 55%) 26.45%, - rgb(0 0 0 / 89.6%) 38.75%, - rgb(0 0 0 / 95.1%) 42.25%, - rgb(0 0 0 / 100%) 50% - ); - } - - &::after { - position: absolute; - z-index: -1; - inset: 0; - bottom: 25%; - content: ""; - backdrop-filter: blur(16px); - mask-image: linear-gradient( - to top, - rgb(0 0 0 / 0%) 0%, - rgb(0 0 0 / 4.9%) 15.5%, - rgb(0 0 0 / 10.4%) 22.5%, - rgb(0 0 0 / 45%) 47.1%, - rgb(0 0 0 / 55%) 52.9%, - rgb(0 0 0 / 89.6%) 77.5%, - rgb(0 0 0 / 95.1%) 91.9%, - rgb(0 0 0 / 100%) 100% - ); - } -} - -.banner { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-size: cover; - background-position: center center; - mask-image: linear-gradient( - to top, - rgb(0 0 0 / 0%) 0%, - rgb(0 0 0 / 4.9%) 15.5%, - rgb(0 0 0 / 10.4%) 22.5%, - rgb(0 0 0 / 45%) 47.1%, - rgb(0 0 0 / 55%) 52.9%, - rgb(0 0 0 / 89.6%) 77.5%, - rgb(0 0 0 / 95.1%) 91.9%, - rgb(0 0 0 / 100%) 100% - ); - pointer-events: none; - opacity: 0.5; -} - -.instance { - position: relative; - width: var(--top-height); -} - -.instanceIcon { - display: inline-block; - width: 38px; - aspect-ratio: 1; - border-radius: 8px; -} - -.realtimeMode { - display: inline-block; - width: var(--top-height); - margin-left: auto; - - &.on { - color: var(--MI_THEME-accent); - } -} - -.bottom { - position: sticky; - bottom: 0; - padding: 20px 0; - - /* 疑似progressive blur */ - &::before { - position: absolute; - z-index: -1; - inset: -30px 0 0 0; - content: ""; - backdrop-filter: blur(8px); - mask-image: linear-gradient( - to bottom, - rgb(0 0 0 / 0%) 0%, - rgb(0 0 0 / 4.9%) 7.75%, - rgb(0 0 0 / 10.4%) 11.25%, - rgb(0 0 0 / 45%) 23.55%, - rgb(0 0 0 / 55%) 26.45%, - rgb(0 0 0 / 89.6%) 38.75%, - rgb(0 0 0 / 95.1%) 42.25%, - rgb(0 0 0 / 100%) 50% - ); - } - - &::after { - position: absolute; - z-index: -1; - inset: 0; - top: 25%; - content: ""; - backdrop-filter: blur(16px); - mask-image: linear-gradient( - to bottom, - rgb(0 0 0 / 0%) 0%, - rgb(0 0 0 / 4.9%) 15.5%, - rgb(0 0 0 / 10.4%) 22.5%, - rgb(0 0 0 / 45%) 47.1%, - rgb(0 0 0 / 55%) 52.9%, - rgb(0 0 0 / 89.6%) 77.5%, - rgb(0 0 0 / 95.1%) 91.9%, - rgb(0 0 0 / 100%) 100% - ); - } -} - -.post { - position: relative; - display: block; - width: 100%; - height: 40px; - color: var(--MI_THEME-fgOnAccent); - font-weight: bold; - text-align: left; - - &::before { - content: ""; - display: block; - width: calc(100% - 38px); - height: 100%; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); - } - - &:hover, &.active { - &::before { - background: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); - } - } -} - -.postIcon { - position: relative; - margin-left: 30px; - margin-right: 8px; - width: 32px; -} - -.account { - position: relative; - display: flex; - align-items: center; - padding-left: 30px; - width: 100%; - text-align: left; - box-sizing: border-box; - margin-top: 16px; -} - -.avatar { - display: block; - flex-shrink: 0; - position: relative; - width: 32px; - aspect-ratio: 1; - margin-right: 8px; -} - -.acct { - display: block; - flex-shrink: 1; - padding-right: 8px; -} - -.middle { - flex: 1; -} - -.divider { - margin: 16px 16px; - border-top: solid 0.5px var(--MI_THEME-divider); -} - -.item { - position: relative; - display: block; - padding-left: 24px; - line-height: 2.85rem; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - width: 100%; - text-align: left; - box-sizing: border-box; - color: var(--MI_THEME-navFg); - - &:hover { - text-decoration: none; - color: light-dark(hsl(from var(--MI_THEME-navFg) h s calc(l - 17)), hsl(from var(--MI_THEME-navFg) h s calc(l + 17))); - } - - &.active { - color: var(--MI_THEME-navActive); - } - - &:hover, &.active { - &::before { - content: ""; - display: block; - width: calc(100% - 24px); - height: 100%; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: var(--MI_THEME-accentedBg); - } - } -} - -.itemIcon { - position: relative; - width: 32px; - margin-right: 8px; -} - -.itemIndicator { - position: absolute; - top: 0; - left: 20px; - color: var(--MI_THEME-navIndicator); - font-size: 8px; - - &:has(.itemIndicateValueIcon) { - animation: none; - left: auto; - right: 20px; - } -} - -.itemText { - position: relative; - font-size: 0.9em; -} -</style> diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index f757d6a7fc..d8a444db8a 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -10,6 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu"> <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/> </button> + <button v-if="!iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode"> + <i class="ti ti-bolt ti-fw"></i> + </button> </div> <div :class="$style.middle"> <MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact> @@ -50,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-if="showWidgetButton" class="_button" :class="[$style.widget]" @click="() => emit('widgetButtonClick')"> <i class="ti ti-apps ti-fw"></i> </button> - <button v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode"> + <button v-if="iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode"> <i class="ti ti-bolt ti-fw"></i> </button> <button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="() => { os.post(); }"> @@ -79,16 +82,18 @@ SPDX-License-Identifier: AGPL-3.0-only </svg> <button class="_button" :class="$style.subButtonClickable" @click="menuEdit"><i :class="$style.subButtonIcon" class="ti ti-settings-2"></i></button> </div> - <div :class="$style.subButtonGapFill"></div> - <div :class="$style.subButtonGapFillDivider"></div> - <div :class="[$style.subButton, $style.toggleButton]"> - <svg viewBox="0 0 16 64" :class="$style.subButtonShape"> - <g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)"> - <path d="M47.488,7.995C47.79,10.11 47.943,12.266 47.943,14.429C47.997,26.989 47.997,84 47.997,84C47.997,84 44.018,118.246 23.997,133.5C-0.374,152.07 -0.003,192 -0.003,192L-0.003,-96C-0.003,-96 0.151,-56.216 23.997,-37.5C40.861,-24.265 46.043,-1.243 47.488,7.995Z" style="fill:var(--MI_THEME-navBg);"/> - </g> - </svg> - <button class="_button" :class="$style.subButtonClickable" @click="toggleIconOnly"><i v-if="iconOnly" class="ti ti-chevron-right" :class="$style.subButtonIcon"></i><i v-else class="ti ti-chevron-left" :class="$style.subButtonIcon"></i></button> - </div> + <template v-if="!props.asDrawer"> + <div :class="$style.subButtonGapFill"></div> + <div :class="$style.subButtonGapFillDivider"></div> + <div :class="[$style.subButton, $style.toggleButton]"> + <svg viewBox="0 0 16 64" :class="$style.subButtonShape"> + <g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)"> + <path d="M47.488,7.995C47.79,10.11 47.943,12.266 47.943,14.429C47.997,26.989 47.997,84 47.997,84C47.997,84 44.018,118.246 23.997,133.5C-0.374,152.07 -0.003,192 -0.003,192L-0.003,-96C-0.003,-96 0.151,-56.216 23.997,-37.5C40.861,-24.265 46.043,-1.243 47.488,7.995Z" style="fill:var(--MI_THEME-navBg);"/> + </g> + </svg> + <button class="_button" :class="$style.subButtonClickable" @click="toggleIconOnly"><i v-if="iconOnly" class="ti ti-chevron-right" :class="$style.subButtonIcon"></i><i v-else class="ti ti-chevron-left" :class="$style.subButtonIcon"></i></button> + </div> + </template> </div> </div> </template> @@ -111,15 +116,16 @@ const router = useRouter(); const props = defineProps<{ showWidgetButton?: boolean; + asDrawer?: boolean; }>(); const emit = defineEmits<{ (ev: 'widgetButtonClick'): void; }>(); -const forceIconOnly = ref(window.innerWidth <= 1279); +const forceIconOnly = ref(!props.asDrawer && window.innerWidth <= 1279); const iconOnly = computed(() => { - return forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon'); + return !props.asDrawer && (forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon')); }); const otherMenuItemIndicated = computed(() => { @@ -208,13 +214,51 @@ function menuEdit() { overscroll-behavior: contain; background: var(--MI_THEME-navBg); contain: strict; - display: flex; - flex-direction: column; direction: rtl; // スクロールバーを左に表示したいため } .top { direction: ltr; + + /* 疑似progressive blur */ + &::before { + position: absolute; + z-index: -1; + inset: 0; + content: ""; + backdrop-filter: blur(8px); + mask-image: linear-gradient( + to top, + rgb(0 0 0 / 0%) 0%, + rgb(0 0 0 / 4.9%) 7.75%, + rgb(0 0 0 / 10.4%) 11.25%, + rgb(0 0 0 / 45%) 23.55%, + rgb(0 0 0 / 55%) 26.45%, + rgb(0 0 0 / 89.6%) 38.75%, + rgb(0 0 0 / 95.1%) 42.25%, + rgb(0 0 0 / 100%) 50% + ); + } + + &::after { + position: absolute; + z-index: -1; + inset: 0; + bottom: 25%; + content: ""; + backdrop-filter: blur(16px); + mask-image: linear-gradient( + to top, + rgb(0 0 0 / 0%) 0%, + rgb(0 0 0 / 4.9%) 15.5%, + rgb(0 0 0 / 10.4%) 22.5%, + rgb(0 0 0 / 45%) 47.1%, + rgb(0 0 0 / 55%) 52.9%, + rgb(0 0 0 / 89.6%) 77.5%, + rgb(0 0 0 / 95.1%) 91.9%, + rgb(0 0 0 / 100%) 100% + ); + } } .middle { @@ -223,6 +267,47 @@ function menuEdit() { .bottom { direction: ltr; + + /* 疑似progressive blur */ + &::before { + position: absolute; + z-index: -1; + inset: -30px 0 0 0; + content: ""; + backdrop-filter: blur(8px); + mask-image: linear-gradient( + to bottom, + rgb(0 0 0 / 0%) 0%, + rgb(0 0 0 / 4.9%) 7.75%, + rgb(0 0 0 / 10.4%) 11.25%, + rgb(0 0 0 / 45%) 23.55%, + rgb(0 0 0 / 55%) 26.45%, + rgb(0 0 0 / 89.6%) 38.75%, + rgb(0 0 0 / 95.1%) 42.25%, + rgb(0 0 0 / 100%) 50% + ); + pointer-events: none; + } + + &::after { + position: absolute; + z-index: -1; + inset: 0; + top: 25%; + content: ""; + backdrop-filter: blur(16px); + mask-image: linear-gradient( + to bottom, + rgb(0 0 0 / 0%) 0%, + rgb(0 0 0 / 4.9%) 15.5%, + rgb(0 0 0 / 10.4%) 22.5%, + rgb(0 0 0 / 45%) 47.1%, + rgb(0 0 0 / 55%) 52.9%, + rgb(0 0 0 / 89.6%) 77.5%, + rgb(0 0 0 / 95.1%) 91.9%, + rgb(0 0 0 / 100%) 100% + ); + } } .subButtons { @@ -307,29 +392,18 @@ function menuEdit() { } .top { + --top-height: 80px; + position: sticky; top: 0; z-index: 1; - padding: 20px 0; - background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--MI-blur, blur(8px)); - backdrop-filter: var(--MI-blur, blur(8px)); + display: flex; + height: var(--top-height); } .instance { position: relative; - display: block; - text-align: center; - width: 100%; - - &:focus-visible { - outline: none; - - > .instanceIcon { - outline: 2px solid var(--MI_THEME-focus); - outline-offset: 2px; - } - } + width: var(--top-height); } .instanceIcon { @@ -339,26 +413,22 @@ function menuEdit() { border-radius: 8px; } - .bottom { - position: sticky; - bottom: 0; - background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--MI-blur, blur(8px)); - backdrop-filter: var(--MI-blur, blur(8px)); - } - .realtimeMode { - display: block; - position: relative; - width: 100%; - height: 52px; - text-align: center; + display: inline-block; + width: var(--top-height); + margin-left: auto; &.on { color: var(--MI_THEME-accent); } } + .bottom { + position: sticky; + bottom: 0; + padding-top: 20px; + } + .post { position: relative; display: block; @@ -548,9 +618,6 @@ function menuEdit() { top: 0; z-index: 1; padding: 20px 0; - background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--MI-blur, blur(8px)); - backdrop-filter: var(--MI-blur, blur(8px)); } .instance { @@ -579,9 +646,6 @@ function menuEdit() { position: sticky; bottom: 0; padding-top: 20px; - background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--MI-blur, blur(8px)); - backdrop-filter: var(--MI-blur, blur(8px)); } .widget { @@ -690,7 +754,7 @@ function menuEdit() { .item { display: block; position: relative; - padding: 18px 0; + padding: 16px 0; width: 100%; text-align: center;