refactor(client): refactor ui components
This commit is contained in:
		
							parent
							
								
									8223a069fe
								
							
						
					
					
						commit
						fa36b88af4
					
				
					 7 changed files with 814 additions and 689 deletions
				
			
		
							
								
								
									
										205
									
								
								packages/client/src/ui/_common_/sidebar-for-mobile.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								packages/client/src/ui/_common_/sidebar-for-mobile.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,205 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="kmwsukvl">
 | 
			
		||||
	<div>
 | 
			
		||||
		<button v-click-anime class="item _button account" @click="openAccountMenu">
 | 
			
		||||
			<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
 | 
			
		||||
		</button>
 | 
			
		||||
		<MkA v-click-anime class="item index" active-class="active" to="/" exact>
 | 
			
		||||
			<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
 | 
			
		||||
		</MkA>
 | 
			
		||||
		<template v-for="item in menu">
 | 
			
		||||
			<div v-if="item === '-'" class="divider"></div>
 | 
			
		||||
			<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
 | 
			
		||||
				<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
 | 
			
		||||
				<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
 | 
			
		||||
			</component>
 | 
			
		||||
		</template>
 | 
			
		||||
		<div class="divider"></div>
 | 
			
		||||
		<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
 | 
			
		||||
			<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
 | 
			
		||||
		</MkA>
 | 
			
		||||
		<button v-click-anime class="item _button" @click="more">
 | 
			
		||||
			<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
 | 
			
		||||
			<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
 | 
			
		||||
		</button>
 | 
			
		||||
		<MkA v-click-anime class="item" active-class="active" to="/settings">
 | 
			
		||||
			<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
 | 
			
		||||
		</MkA>
 | 
			
		||||
		<button class="item _button post" data-cy-open-post-form @click="post">
 | 
			
		||||
			<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
 | 
			
		||||
		</button>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent, ref, toRef, watch } from 'vue';
 | 
			
		||||
import { host } from '@/config';
 | 
			
		||||
import { search } from '@/scripts/search';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { menuDef } from '@/menu';
 | 
			
		||||
import { openAccountMenu } from '@/account';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	setup(props, context) {
 | 
			
		||||
		const menu = toRef(defaultStore.state, 'menu');
 | 
			
		||||
		const otherMenuItemIndicated = computed(() => {
 | 
			
		||||
			for (const def in menuDef) {
 | 
			
		||||
				if (menu.value.includes(def)) continue;
 | 
			
		||||
				if (menuDef[def].indicated) return true;
 | 
			
		||||
			}
 | 
			
		||||
			return false;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			host: host,
 | 
			
		||||
			accounts: [],
 | 
			
		||||
			connection: null,
 | 
			
		||||
			menu,
 | 
			
		||||
			menuDef: menuDef,
 | 
			
		||||
			otherMenuItemIndicated,
 | 
			
		||||
			post: os.post,
 | 
			
		||||
			search,
 | 
			
		||||
			openAccountMenu,
 | 
			
		||||
			more: () => {
 | 
			
		||||
				os.popup(import('@/components/launch-pad.vue'), {}, {
 | 
			
		||||
				}, 'closed');
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.kmwsukvl {
 | 
			
		||||
	$ui-font-size: 1em; // TODO: どこかに集約したい
 | 
			
		||||
	$avatar-size: 32px;
 | 
			
		||||
	$avatar-margin: 8px;
 | 
			
		||||
 | 
			
		||||
	> div {
 | 
			
		||||
 | 
			
		||||
		> .divider {
 | 
			
		||||
			margin: 16px 16px;
 | 
			
		||||
			border-top: solid 0.5px var(--divider);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .item {
 | 
			
		||||
			position: relative;
 | 
			
		||||
			display: block;
 | 
			
		||||
			padding-left: 24px;
 | 
			
		||||
			font-size: $ui-font-size;
 | 
			
		||||
			line-height: 2.85rem;
 | 
			
		||||
			text-overflow: ellipsis;
 | 
			
		||||
			overflow: hidden;
 | 
			
		||||
			white-space: nowrap;
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			text-align: left;
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
			color: var(--navFg);
 | 
			
		||||
 | 
			
		||||
			> i {
 | 
			
		||||
				position: relative;
 | 
			
		||||
				width: 32px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> i,
 | 
			
		||||
			> .avatar {
 | 
			
		||||
				margin-right: $avatar-margin;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .avatar {
 | 
			
		||||
				width: $avatar-size;
 | 
			
		||||
				height: $avatar-size;
 | 
			
		||||
				vertical-align: middle;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .indicator {
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 0;
 | 
			
		||||
				left: 20px;
 | 
			
		||||
				color: var(--navIndicator);
 | 
			
		||||
				font-size: 8px;
 | 
			
		||||
				animation: blink 1s infinite;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .text {
 | 
			
		||||
				position: relative;
 | 
			
		||||
				font-size: 0.9em;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:hover {
 | 
			
		||||
				text-decoration: none;
 | 
			
		||||
				color: var(--navHoverFg);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.active {
 | 
			
		||||
				color: var(--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(--accentedBg);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:first-child, &:last-child {
 | 
			
		||||
				position: sticky;
 | 
			
		||||
				z-index: 1;
 | 
			
		||||
				padding-top: 8px;
 | 
			
		||||
				padding-bottom: 8px;
 | 
			
		||||
				background: var(--X14);
 | 
			
		||||
				-webkit-backdrop-filter: var(--blur, blur(8px));
 | 
			
		||||
				backdrop-filter: var(--blur, blur(8px));
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:first-child {
 | 
			
		||||
				top: 0;
 | 
			
		||||
 | 
			
		||||
				&:hover, &.active {
 | 
			
		||||
					&:before {
 | 
			
		||||
						content: none;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:last-child {
 | 
			
		||||
				bottom: 0;
 | 
			
		||||
				color: var(--fgOnAccent);
 | 
			
		||||
 | 
			
		||||
				&:before {
 | 
			
		||||
					content: "";
 | 
			
		||||
					display: block;
 | 
			
		||||
					width: calc(100% - 20px);
 | 
			
		||||
					height: calc(100% - 20px);
 | 
			
		||||
					margin: auto;
 | 
			
		||||
					position: absolute;
 | 
			
		||||
					top: 0;
 | 
			
		||||
					left: 0;
 | 
			
		||||
					right: 0;
 | 
			
		||||
					bottom: 0;
 | 
			
		||||
					border-radius: 999px;
 | 
			
		||||
					background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
 | 
			
		||||
				}
 | 
			
		||||
				
 | 
			
		||||
				&:hover, &.active {
 | 
			
		||||
					&:before {
 | 
			
		||||
						background: var(--accentLighten);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,385 +1,300 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="mvcprjjd">
 | 
			
		||||
	<transition name="nav-back">
 | 
			
		||||
		<div v-if="showing"
 | 
			
		||||
			class="nav-back _modalBg"
 | 
			
		||||
			@click="showing = false"
 | 
			
		||||
			@touchstart.passive="showing = false"
 | 
			
		||||
		></div>
 | 
			
		||||
	</transition>
 | 
			
		||||
 | 
			
		||||
	<transition name="nav">
 | 
			
		||||
		<nav v-show="showing" class="nav" :class="{ iconOnly, hidden }">
 | 
			
		||||
			<div>
 | 
			
		||||
				<button v-click-anime class="item _button account" @click="openAccountMenu">
 | 
			
		||||
					<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
 | 
			
		||||
				</button>
 | 
			
		||||
				<MkA v-click-anime class="item index" active-class="active" to="/" exact>
 | 
			
		||||
					<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
 | 
			
		||||
				</MkA>
 | 
			
		||||
				<template v-for="item in menu">
 | 
			
		||||
					<div v-if="item === '-'" class="divider"></div>
 | 
			
		||||
					<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
 | 
			
		||||
						<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
 | 
			
		||||
						<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
 | 
			
		||||
					</component>
 | 
			
		||||
				</template>
 | 
			
		||||
				<div class="divider"></div>
 | 
			
		||||
				<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
 | 
			
		||||
					<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
 | 
			
		||||
				</MkA>
 | 
			
		||||
				<button v-click-anime class="item _button" @click="more">
 | 
			
		||||
					<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
 | 
			
		||||
					<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
 | 
			
		||||
				</button>
 | 
			
		||||
				<MkA v-click-anime class="item" active-class="active" to="/settings">
 | 
			
		||||
					<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
 | 
			
		||||
				</MkA>
 | 
			
		||||
				<button class="item _button post" data-cy-open-post-form @click="post">
 | 
			
		||||
					<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</nav>
 | 
			
		||||
	</transition>
 | 
			
		||||
<div class="mvcprjjd" :class="{ iconOnly }">
 | 
			
		||||
	<div>
 | 
			
		||||
		<button v-click-anime class="item _button account" @click="openAccountMenu">
 | 
			
		||||
			<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
 | 
			
		||||
		</button>
 | 
			
		||||
		<MkA v-click-anime class="item index" active-class="active" to="/" exact>
 | 
			
		||||
			<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
 | 
			
		||||
		</MkA>
 | 
			
		||||
		<template v-for="item in menu">
 | 
			
		||||
			<div v-if="item === '-'" class="divider"></div>
 | 
			
		||||
			<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
 | 
			
		||||
				<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
 | 
			
		||||
				<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
 | 
			
		||||
			</component>
 | 
			
		||||
		</template>
 | 
			
		||||
		<div class="divider"></div>
 | 
			
		||||
		<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
 | 
			
		||||
			<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
 | 
			
		||||
		</MkA>
 | 
			
		||||
		<button v-click-anime class="item _button" @click="more">
 | 
			
		||||
			<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
 | 
			
		||||
			<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
 | 
			
		||||
		</button>
 | 
			
		||||
		<MkA v-click-anime class="item" active-class="active" to="/settings">
 | 
			
		||||
			<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
 | 
			
		||||
		</MkA>
 | 
			
		||||
		<button class="item _button post" data-cy-open-post-form @click="post">
 | 
			
		||||
			<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
 | 
			
		||||
		</button>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { computed, defineComponent, ref, watch } from 'vue';
 | 
			
		||||
import { host } from '@/config';
 | 
			
		||||
import { search } from '@/scripts/search';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { menuDef } from '@/menu';
 | 
			
		||||
import { openAccountMenu } from '@/account';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
		defaultHidden: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	setup(props, context) {
 | 
			
		||||
		const iconOnly = ref(false);
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			host: host,
 | 
			
		||||
			showing: false,
 | 
			
		||||
			accounts: [],
 | 
			
		||||
			connection: null,
 | 
			
		||||
			menuDef: menuDef,
 | 
			
		||||
			iconOnly: false,
 | 
			
		||||
			hidden: this.defaultHidden,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		menu(): string[] {
 | 
			
		||||
			return this.$store.state.menu;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		otherNavItemIndicated(): boolean {
 | 
			
		||||
			for (const def in this.menuDef) {
 | 
			
		||||
				if (this.menu.includes(def)) continue;
 | 
			
		||||
				if (this.menuDef[def].indicated) return true;
 | 
			
		||||
		const menu = computed(() => defaultStore.state.menu);
 | 
			
		||||
		const otherMenuItemIndicated = computed(() => {
 | 
			
		||||
			for (const def in menuDef) {
 | 
			
		||||
				if (menu.value.includes(def)) continue;
 | 
			
		||||
				if (menuDef[def].indicated) return true;
 | 
			
		||||
			}
 | 
			
		||||
			return false;
 | 
			
		||||
		},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const calcViewState = () => {
 | 
			
		||||
			iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		calcViewState();
 | 
			
		||||
 | 
			
		||||
		window.addEventListener('resize', calcViewState);
 | 
			
		||||
 | 
			
		||||
		watch(defaultStore.reactiveState.menuDisplay, () => {
 | 
			
		||||
			calcViewState();
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			host: host,
 | 
			
		||||
			accounts: [],
 | 
			
		||||
			connection: null,
 | 
			
		||||
			menu,
 | 
			
		||||
			menuDef: menuDef,
 | 
			
		||||
			otherMenuItemIndicated,
 | 
			
		||||
			iconOnly,
 | 
			
		||||
			post: os.post,
 | 
			
		||||
			search,
 | 
			
		||||
			openAccountMenu,
 | 
			
		||||
			more: () => {
 | 
			
		||||
				os.popup(import('@/components/launch-pad.vue'), {}, {
 | 
			
		||||
				}, 'closed');
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		$route(to, from) {
 | 
			
		||||
			this.showing = false;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		'$store.reactiveState.menuDisplay.value'() {
 | 
			
		||||
			this.calcViewState();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		iconOnly() {
 | 
			
		||||
			this.$nextTick(() => {
 | 
			
		||||
				this.$emit('change-view-mode');
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		hidden() {
 | 
			
		||||
			this.$nextTick(() => {
 | 
			
		||||
				this.$emit('change-view-mode');
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		window.addEventListener('resize', this.calcViewState);
 | 
			
		||||
		this.calcViewState();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		calcViewState() {
 | 
			
		||||
			this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.menuDisplay === 'sideIcon');
 | 
			
		||||
			if (!this.defaultHidden) {
 | 
			
		||||
				this.hidden = (window.innerWidth <= 650);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		show() {
 | 
			
		||||
			this.showing = true;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		post() {
 | 
			
		||||
			os.post();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		search() {
 | 
			
		||||
			search();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		more(ev) {
 | 
			
		||||
			os.popup(import('@/components/launch-pad.vue'), {}, {
 | 
			
		||||
			}, 'closed');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		openAccountMenu,
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.nav-enter-active,
 | 
			
		||||
.nav-leave-active {
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
	transform: translateX(0);
 | 
			
		||||
	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 | 
			
		||||
}
 | 
			
		||||
.nav-enter-from,
 | 
			
		||||
.nav-leave-active {
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
	transform: translateX(-240px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-back-enter-active,
 | 
			
		||||
.nav-back-leave-active {
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 | 
			
		||||
}
 | 
			
		||||
.nav-back-enter-from,
 | 
			
		||||
.nav-back-leave-active {
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mvcprjjd {
 | 
			
		||||
	$ui-font-size: 1em; // TODO: どこかに集約したい
 | 
			
		||||
	$nav-width: 250px;
 | 
			
		||||
	$nav-icon-only-width: 86px;
 | 
			
		||||
	$avatar-size: 32px;
 | 
			
		||||
	$avatar-margin: 8px;
 | 
			
		||||
 | 
			
		||||
	> .nav-back {
 | 
			
		||||
	flex: 0 0 $nav-width;
 | 
			
		||||
	width: $nav-width;
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
 | 
			
		||||
	> div {
 | 
			
		||||
		position: fixed;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		z-index: 1001;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .nav {
 | 
			
		||||
		$avatar-size: 32px;
 | 
			
		||||
		$avatar-margin: 8px;
 | 
			
		||||
 | 
			
		||||
		flex: 0 0 $nav-width;
 | 
			
		||||
		width: $nav-width;
 | 
			
		||||
		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 | 
			
		||||
		height: calc(var(--vh, 1vh) * 100);
 | 
			
		||||
		box-sizing: border-box;
 | 
			
		||||
		overflow: auto;
 | 
			
		||||
		overflow-x: clip;
 | 
			
		||||
		background: var(--navBg);
 | 
			
		||||
 | 
			
		||||
		&.iconOnly {
 | 
			
		||||
			flex: 0 0 $nav-icon-only-width;
 | 
			
		||||
			width: $nav-icon-only-width;
 | 
			
		||||
 | 
			
		||||
			&:not(.hidden) {
 | 
			
		||||
				> div {
 | 
			
		||||
					width: $nav-icon-only-width;
 | 
			
		||||
 | 
			
		||||
					> .divider {
 | 
			
		||||
						margin: 8px auto;
 | 
			
		||||
						width: calc(100% - 32px);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .item {
 | 
			
		||||
						padding-left: 0;
 | 
			
		||||
						padding: 18px 0;
 | 
			
		||||
						width: 100%;
 | 
			
		||||
						text-align: center;
 | 
			
		||||
						font-size: $ui-font-size * 1.1;
 | 
			
		||||
						line-height: initial;
 | 
			
		||||
 | 
			
		||||
						> i,
 | 
			
		||||
						> .avatar {
 | 
			
		||||
							display: block;
 | 
			
		||||
							margin: 0 auto;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						> i {
 | 
			
		||||
							opacity: 0.7;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						> .text {
 | 
			
		||||
							display: none;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						&:hover, &.active {
 | 
			
		||||
							> i, > .text {
 | 
			
		||||
								opacity: 1;
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						&:first-child {
 | 
			
		||||
							margin-bottom: 8px;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						&:last-child {
 | 
			
		||||
							margin-top: 8px;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		> .divider {
 | 
			
		||||
			margin: 16px 16px;
 | 
			
		||||
			border-top: solid 0.5px var(--divider);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&.hidden {
 | 
			
		||||
			position: fixed;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			left: 0;
 | 
			
		||||
			z-index: 1001;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&:not(.hidden) {
 | 
			
		||||
			display: block !important;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> div {
 | 
			
		||||
			position: fixed;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			left: 0;
 | 
			
		||||
			z-index: 1001;
 | 
			
		||||
			width: $nav-width;
 | 
			
		||||
			// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 | 
			
		||||
			height: calc(var(--vh, 1vh) * 100);
 | 
			
		||||
		> .item {
 | 
			
		||||
			position: relative;
 | 
			
		||||
			display: block;
 | 
			
		||||
			padding-left: 24px;
 | 
			
		||||
			font-size: $ui-font-size;
 | 
			
		||||
			line-height: 2.85rem;
 | 
			
		||||
			text-overflow: ellipsis;
 | 
			
		||||
			overflow: hidden;
 | 
			
		||||
			white-space: nowrap;
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			text-align: left;
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
			overflow: auto;
 | 
			
		||||
			overflow-x: clip;
 | 
			
		||||
			background: var(--navBg);
 | 
			
		||||
			color: var(--navFg);
 | 
			
		||||
 | 
			
		||||
			> .divider {
 | 
			
		||||
				margin: 16px 16px;
 | 
			
		||||
				border-top: solid 0.5px var(--divider);
 | 
			
		||||
			> i {
 | 
			
		||||
				position: relative;
 | 
			
		||||
				width: 32px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .item {
 | 
			
		||||
			> i,
 | 
			
		||||
			> .avatar {
 | 
			
		||||
				margin-right: $avatar-margin;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .avatar {
 | 
			
		||||
				width: $avatar-size;
 | 
			
		||||
				height: $avatar-size;
 | 
			
		||||
				vertical-align: middle;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .indicator {
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 0;
 | 
			
		||||
				left: 20px;
 | 
			
		||||
				color: var(--navIndicator);
 | 
			
		||||
				font-size: 8px;
 | 
			
		||||
				animation: blink 1s infinite;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .text {
 | 
			
		||||
				position: relative;
 | 
			
		||||
				display: block;
 | 
			
		||||
				padding-left: 24px;
 | 
			
		||||
				font-size: $ui-font-size;
 | 
			
		||||
				line-height: 2.85rem;
 | 
			
		||||
				text-overflow: ellipsis;
 | 
			
		||||
				overflow: hidden;
 | 
			
		||||
				white-space: nowrap;
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				text-align: left;
 | 
			
		||||
				box-sizing: border-box;
 | 
			
		||||
				color: var(--navFg);
 | 
			
		||||
				font-size: 0.9em;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
				> i {
 | 
			
		||||
					position: relative;
 | 
			
		||||
					width: 32px;
 | 
			
		||||
				}
 | 
			
		||||
			&:hover {
 | 
			
		||||
				text-decoration: none;
 | 
			
		||||
				color: var(--navHoverFg);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
				> i,
 | 
			
		||||
				> .avatar {
 | 
			
		||||
					margin-right: $avatar-margin;
 | 
			
		||||
				}
 | 
			
		||||
			&.active {
 | 
			
		||||
				color: var(--navActive);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
				> .avatar {
 | 
			
		||||
					width: $avatar-size;
 | 
			
		||||
					height: $avatar-size;
 | 
			
		||||
					vertical-align: middle;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .indicator {
 | 
			
		||||
			&:hover, &.active {
 | 
			
		||||
				&:before {
 | 
			
		||||
					content: "";
 | 
			
		||||
					display: block;
 | 
			
		||||
					width: calc(100% - 24px);
 | 
			
		||||
					height: 100%;
 | 
			
		||||
					margin: auto;
 | 
			
		||||
					position: absolute;
 | 
			
		||||
					top: 0;
 | 
			
		||||
					left: 20px;
 | 
			
		||||
					color: var(--navIndicator);
 | 
			
		||||
					font-size: 8px;
 | 
			
		||||
					animation: blink 1s infinite;
 | 
			
		||||
					left: 0;
 | 
			
		||||
					right: 0;
 | 
			
		||||
					bottom: 0;
 | 
			
		||||
					border-radius: 999px;
 | 
			
		||||
					background: var(--accentedBg);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
				> .text {
 | 
			
		||||
					position: relative;
 | 
			
		||||
					font-size: 0.9em;
 | 
			
		||||
				}
 | 
			
		||||
			&:first-child, &:last-child {
 | 
			
		||||
				position: sticky;
 | 
			
		||||
				z-index: 1;
 | 
			
		||||
				padding-top: 8px;
 | 
			
		||||
				padding-bottom: 8px;
 | 
			
		||||
				background: var(--X14);
 | 
			
		||||
				-webkit-backdrop-filter: var(--blur, blur(8px));
 | 
			
		||||
				backdrop-filter: var(--blur, blur(8px));
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
				&:hover {
 | 
			
		||||
					text-decoration: none;
 | 
			
		||||
					color: var(--navHoverFg);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&.active {
 | 
			
		||||
					color: var(--navActive);
 | 
			
		||||
				}
 | 
			
		||||
			&:first-child {
 | 
			
		||||
				top: 0;
 | 
			
		||||
 | 
			
		||||
				&: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(--accentedBg);
 | 
			
		||||
						content: none;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
				&:first-child, &:last-child {
 | 
			
		||||
					position: sticky;
 | 
			
		||||
					z-index: 1;
 | 
			
		||||
					padding-top: 8px;
 | 
			
		||||
					padding-bottom: 8px;
 | 
			
		||||
					background: var(--X14);
 | 
			
		||||
					-webkit-backdrop-filter: var(--blur, blur(8px));
 | 
			
		||||
					backdrop-filter: var(--blur, blur(8px));
 | 
			
		||||
			&:last-child {
 | 
			
		||||
				bottom: 0;
 | 
			
		||||
				color: var(--fgOnAccent);
 | 
			
		||||
 | 
			
		||||
				&:before {
 | 
			
		||||
					content: "";
 | 
			
		||||
					display: block;
 | 
			
		||||
					width: calc(100% - 20px);
 | 
			
		||||
					height: calc(100% - 20px);
 | 
			
		||||
					margin: auto;
 | 
			
		||||
					position: absolute;
 | 
			
		||||
					top: 0;
 | 
			
		||||
					left: 0;
 | 
			
		||||
					right: 0;
 | 
			
		||||
					bottom: 0;
 | 
			
		||||
					border-radius: 999px;
 | 
			
		||||
					background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
 | 
			
		||||
				}
 | 
			
		||||
				
 | 
			
		||||
				&:hover, &.active {
 | 
			
		||||
					&:before {
 | 
			
		||||
						background: var(--accentLighten);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.iconOnly {
 | 
			
		||||
		flex: 0 0 $nav-icon-only-width;
 | 
			
		||||
		width: $nav-icon-only-width;
 | 
			
		||||
 | 
			
		||||
		> div {
 | 
			
		||||
			width: $nav-icon-only-width;
 | 
			
		||||
 | 
			
		||||
			> .divider {
 | 
			
		||||
				margin: 8px auto;
 | 
			
		||||
				width: calc(100% - 32px);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .item {
 | 
			
		||||
				padding-left: 0;
 | 
			
		||||
				padding: 18px 0;
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				text-align: center;
 | 
			
		||||
				font-size: $ui-font-size * 1.1;
 | 
			
		||||
				line-height: initial;
 | 
			
		||||
 | 
			
		||||
				> i,
 | 
			
		||||
				> .avatar {
 | 
			
		||||
					display: block;
 | 
			
		||||
					margin: 0 auto;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> i {
 | 
			
		||||
					opacity: 0.7;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .text {
 | 
			
		||||
					display: none;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:hover, &.active {
 | 
			
		||||
					> i, > .text {
 | 
			
		||||
						opacity: 1;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:first-child {
 | 
			
		||||
					top: 0;
 | 
			
		||||
 | 
			
		||||
					&:hover, &.active {
 | 
			
		||||
						&:before {
 | 
			
		||||
							content: none;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					margin-bottom: 8px;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:last-child {
 | 
			
		||||
					bottom: 0;
 | 
			
		||||
					color: var(--fgOnAccent);
 | 
			
		||||
					margin-top: 8px;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
					&:before {
 | 
			
		||||
						content: "";
 | 
			
		||||
						display: block;
 | 
			
		||||
						width: calc(100% - 20px);
 | 
			
		||||
						height: calc(100% - 20px);
 | 
			
		||||
						margin: auto;
 | 
			
		||||
						position: absolute;
 | 
			
		||||
						top: 0;
 | 
			
		||||
						left: 0;
 | 
			
		||||
						right: 0;
 | 
			
		||||
						bottom: 0;
 | 
			
		||||
						border-radius: 999px;
 | 
			
		||||
						background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
 | 
			
		||||
					}
 | 
			
		||||
					
 | 
			
		||||
					&:hover, &.active {
 | 
			
		||||
						&:before {
 | 
			
		||||
							background: var(--accentLighten);
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				&:before {
 | 
			
		||||
					width: 100%;
 | 
			
		||||
					border-radius: 0;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&.post {
 | 
			
		||||
					height: $nav-icon-only-width;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&.post:before {
 | 
			
		||||
					width: calc(100% - 32px);
 | 
			
		||||
					height: calc(100% - 32px);
 | 
			
		||||
					border-radius: 100%;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +1,14 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="mk-app" :class="{ wallpaper, isMobile }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
 | 
			
		||||
<div class="gbhvwtnk" :class="{ wallpaper }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
 | 
			
		||||
	<XHeaderMenu v-if="showMenuOnTop" v-get-size="(w, h) => globalHeaderHeight = h"/>
 | 
			
		||||
 | 
			
		||||
	<div class="columns" :class="{ fullView, withGlobalHeader: showMenuOnTop }">
 | 
			
		||||
		<template v-if="!isMobile">
 | 
			
		||||
			<div v-if="!showMenuOnTop" class="sidebar">
 | 
			
		||||
				<XSidebar/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-else ref="widgetsLeft" class="widgets left">
 | 
			
		||||
				<XWidgets :place="'left'" @mounted="attachSticky('widgetsLeft')"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</template>
 | 
			
		||||
		<div v-if="!showMenuOnTop" class="sidebar">
 | 
			
		||||
			<XSidebar/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-else ref="widgetsLeft" class="widgets left">
 | 
			
		||||
			<XWidgets :place="'left'" @mounted="attachSticky('widgetsLeft')"/>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<main class="main" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
 | 
			
		||||
			<div class="content">
 | 
			
		||||
| 
						 | 
				
			
			@ -32,16 +30,6 @@
 | 
			
		|||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div v-if="isMobile" class="buttons">
 | 
			
		||||
		<button ref="navButton" class="button nav _button" @click="showDrawerNav"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
 | 
			
		||||
		<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
 | 
			
		||||
		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
 | 
			
		||||
		<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
 | 
			
		||||
		<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<XDrawerSidebar v-if="isMobile" ref="drawerNav" class="sidebar"/>
 | 
			
		||||
 | 
			
		||||
	<transition name="tray-back">
 | 
			
		||||
		<div v-if="widgetsShowing"
 | 
			
		||||
			class="tray-back _modalBg"
 | 
			
		||||
| 
						 | 
				
			
			@ -65,20 +53,17 @@ import { defineComponent, defineAsyncComponent, markRaw } from 'vue';
 | 
			
		|||
import { instanceName } from '@/config';
 | 
			
		||||
import { StickySidebar } from '@/scripts/sticky-sidebar';
 | 
			
		||||
import XSidebar from './classic.sidebar.vue';
 | 
			
		||||
import XDrawerSidebar from '@/ui/_common_/sidebar.vue';
 | 
			
		||||
import XCommon from './_common_/common.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { menuDef } from '@/menu';
 | 
			
		||||
import * as symbols from '@/symbols';
 | 
			
		||||
 | 
			
		||||
const DESKTOP_THRESHOLD = 1100;
 | 
			
		||||
const MOBILE_THRESHOLD = 600;
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XCommon,
 | 
			
		||||
		XSidebar,
 | 
			
		||||
		XDrawerSidebar,
 | 
			
		||||
		XHeaderMenu: defineAsyncComponent(() => import('./classic.header.vue')),
 | 
			
		||||
		XWidgets: defineAsyncComponent(() => import('./classic.widgets.vue')),
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +80,6 @@ export default defineComponent({
 | 
			
		|||
			pageInfo: null,
 | 
			
		||||
			menuDef: menuDef,
 | 
			
		||||
			globalHeaderHeight: 0,
 | 
			
		||||
			isMobile: window.innerWidth <= MOBILE_THRESHOLD,
 | 
			
		||||
			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
 | 
			
		||||
			widgetsShowing: false,
 | 
			
		||||
			fullView: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -104,16 +88,8 @@ export default defineComponent({
 | 
			
		|||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		navIndicated(): boolean {
 | 
			
		||||
			for (const def in this.menuDef) {
 | 
			
		||||
				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
 | 
			
		||||
				if (this.menuDef[def].indicated) return true;
 | 
			
		||||
			}
 | 
			
		||||
			return false;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		showMenuOnTop(): boolean {
 | 
			
		||||
			return !this.isMobile && this.$store.state.menuDisplay === 'top';
 | 
			
		||||
			return this.$store.state.menuDisplay === 'top';
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -136,7 +112,6 @@ export default defineComponent({
 | 
			
		|||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		window.addEventListener('resize', () => {
 | 
			
		||||
			this.isMobile = (window.innerWidth <= MOBILE_THRESHOLD);
 | 
			
		||||
			this.isDesktop = (window.innerWidth >= DESKTOP_THRESHOLD);
 | 
			
		||||
		}, { passive: true });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -179,22 +154,10 @@ export default defineComponent({
 | 
			
		|||
			}, { passive: true });
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		post() {
 | 
			
		||||
			os.post();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		top() {
 | 
			
		||||
			window.scroll({ top: 0, behavior: 'smooth' });
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		back() {
 | 
			
		||||
			history.back();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		showDrawerNav() {
 | 
			
		||||
			this.$refs.drawerNav.show();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onTransition() {
 | 
			
		||||
			if (window._scroll) window._scroll();
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -258,10 +221,9 @@ export default defineComponent({
 | 
			
		|||
	opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mk-app {
 | 
			
		||||
.gbhvwtnk {
 | 
			
		||||
	$ui-font-size: 1em;
 | 
			
		||||
	$widgets-hide-threshold: 1200px;
 | 
			
		||||
	$nav-icon-only-width: 78px; // TODO: どこかに集約したい
 | 
			
		||||
 | 
			
		||||
	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 | 
			
		||||
	min-height: calc(var(--vh, 1vh) * 100);
 | 
			
		||||
| 
						 | 
				
			
			@ -272,21 +234,6 @@ export default defineComponent({
 | 
			
		|||
		//backdrop-filter: var(--blur, blur(4px));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.isMobile {
 | 
			
		||||
		> .columns {
 | 
			
		||||
			display: block;
 | 
			
		||||
			margin: 0;
 | 
			
		||||
 | 
			
		||||
			> .main {
 | 
			
		||||
				margin: 0;
 | 
			
		||||
				padding-bottom: 92px;
 | 
			
		||||
				border: none;
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				border-radius: 0;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .columns {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		justify-content: center;
 | 
			
		||||
| 
						 | 
				
			
			@ -372,76 +319,6 @@ export default defineComponent({
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .buttons {
 | 
			
		||||
		position: fixed;
 | 
			
		||||
		z-index: 1000;
 | 
			
		||||
		bottom: 0;
 | 
			
		||||
		padding: 16px;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		box-sizing: border-box;
 | 
			
		||||
		-webkit-backdrop-filter: var(--blur, blur(32px));
 | 
			
		||||
		backdrop-filter: var(--blur, blur(32px));
 | 
			
		||||
		background-color: var(--header);
 | 
			
		||||
		border-top: solid 0.5px var(--divider);
 | 
			
		||||
 | 
			
		||||
		> .button {
 | 
			
		||||
			position: relative;
 | 
			
		||||
			flex: 1;
 | 
			
		||||
			padding: 0;
 | 
			
		||||
			margin: auto;
 | 
			
		||||
			height: 64px;
 | 
			
		||||
			border-radius: 8px;
 | 
			
		||||
			background: var(--panel);
 | 
			
		||||
			color: var(--fg);
 | 
			
		||||
 | 
			
		||||
			&:not(:last-child) {
 | 
			
		||||
				margin-right: 12px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@media (max-width: 400px) {
 | 
			
		||||
				height: 60px;
 | 
			
		||||
 | 
			
		||||
				&:not(:last-child) {
 | 
			
		||||
					margin-right: 8px;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:hover {
 | 
			
		||||
				background: var(--X2);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .indicator {
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 0;
 | 
			
		||||
				left: 0;
 | 
			
		||||
				color: var(--indicator);
 | 
			
		||||
				font-size: 16px;
 | 
			
		||||
				animation: blink 1s infinite;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:first-child {
 | 
			
		||||
				margin-left: 0;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:last-child {
 | 
			
		||||
				margin-right: 0;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> * {
 | 
			
		||||
				font-size: 22px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:disabled {
 | 
			
		||||
				cursor: default;
 | 
			
		||||
 | 
			
		||||
				> * {
 | 
			
		||||
					opacity: 0.5;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .tray-back {
 | 
			
		||||
		z-index: 1001;
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="mk-deck" :class="`${deckStore.reactiveState.columnAlign.value}`" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
 | 
			
		||||
<div class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
 | 
			
		||||
	@contextmenu.self.prevent="onContextmenu"
 | 
			
		||||
>
 | 
			
		||||
	<XSidebar ref="nav"/>
 | 
			
		||||
	<XSidebar v-if="!isMobile"/>
 | 
			
		||||
 | 
			
		||||
	<template v-for="ids in layout">
 | 
			
		||||
		<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
 | 
			
		||||
| 
						 | 
				
			
			@ -22,94 +22,76 @@
 | 
			
		|||
		/>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<button v-if="$i" class="nav _button" @click="showNav()"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
 | 
			
		||||
	<button v-if="$i" class="post _buttonPrimary" @click="post()"><i class="fas fa-pencil-alt"></i></button>
 | 
			
		||||
	<div v-if="isMobile" class="buttons">
 | 
			
		||||
		<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
 | 
			
		||||
		<button class="button home _button" @click="$router.push('/')"><i class="fas fa-home"></i></button>
 | 
			
		||||
		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
 | 
			
		||||
		<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<transition name="menu-back">
 | 
			
		||||
		<div v-if="drawerMenuShowing"
 | 
			
		||||
			class="menu-back _modalBg"
 | 
			
		||||
			@click="drawerMenuShowing = false"
 | 
			
		||||
			@touchstart.passive="drawerMenuShowing = false"
 | 
			
		||||
		></div>
 | 
			
		||||
	</transition>
 | 
			
		||||
 | 
			
		||||
	<transition name="menu">
 | 
			
		||||
		<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
 | 
			
		||||
	</transition>
 | 
			
		||||
 | 
			
		||||
	<XCommon/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { computed, defineComponent, provide, ref, watch } from 'vue';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import { host } from '@/config';
 | 
			
		||||
import DeckColumnCore from '@/ui/deck/column-core.vue';
 | 
			
		||||
import XSidebar from '@/ui/_common_/sidebar.vue';
 | 
			
		||||
import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
 | 
			
		||||
import { getScrollContainer } from '@/scripts/scroll';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { menuDef } from '@/menu';
 | 
			
		||||
import XCommon from './_common_/common.vue';
 | 
			
		||||
import { deckStore, addColumn, loadDeck } from './deck/deck-store';
 | 
			
		||||
import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { $i } from '@/account';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XCommon,
 | 
			
		||||
		XSidebar,
 | 
			
		||||
		XDrawerMenu,
 | 
			
		||||
		DeckColumnCore,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	provide() {
 | 
			
		||||
		return {
 | 
			
		||||
			shouldSpacerMin: true,
 | 
			
		||||
			...deckStore.state.navWindow ? {
 | 
			
		||||
				navHook: (url) => {
 | 
			
		||||
					os.pageWindow(url);
 | 
			
		||||
				}
 | 
			
		||||
			} : {}
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	setup() {
 | 
			
		||||
		const isMobile = ref(window.innerWidth <= 500);
 | 
			
		||||
		window.addEventListener('resize', () => {
 | 
			
		||||
			isMobile.value = window.innerWidth <= 500;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			deckStore,
 | 
			
		||||
			host: host,
 | 
			
		||||
			menuDef: menuDef,
 | 
			
		||||
			wallpaper: localStorage.getItem('wallpaper') != null,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
		const drawerMenuShowing = ref(false);
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		columns() {
 | 
			
		||||
			return deckStore.reactiveState.columns.value;
 | 
			
		||||
		},
 | 
			
		||||
		layout() {
 | 
			
		||||
			return deckStore.reactiveState.layout.value;
 | 
			
		||||
		},
 | 
			
		||||
		navIndicated(): boolean {
 | 
			
		||||
			if (!this.$i) return false;
 | 
			
		||||
			for (const def in this.menuDef) {
 | 
			
		||||
				if (this.menuDef[def].indicated) return true;
 | 
			
		||||
		const route = useRoute();
 | 
			
		||||
		watch(route, () => {
 | 
			
		||||
			drawerMenuShowing.value = false;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const columns = deckStore.reactiveState.columns;
 | 
			
		||||
		const layout = deckStore.reactiveState.layout.value;
 | 
			
		||||
		const menuIndicated = computed(() => {
 | 
			
		||||
			if ($i == null) return false;
 | 
			
		||||
			for (const def in menuDef) {
 | 
			
		||||
				if (menuDef[def].indicated) return true;
 | 
			
		||||
			}
 | 
			
		||||
			return false;
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		document.documentElement.style.overflowY = 'hidden';
 | 
			
		||||
		document.documentElement.style.scrollBehavior = 'auto';
 | 
			
		||||
		window.addEventListener('wheel', this.onWheel);
 | 
			
		||||
		loadDeck();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		onWheel(e) {
 | 
			
		||||
			if (getScrollContainer(e.target) == null) {
 | 
			
		||||
				document.documentElement.scrollLeft += e.deltaY > 0 ? 96 : -96;
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		showNav() {
 | 
			
		||||
			this.$refs.nav.show();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		post() {
 | 
			
		||||
			os.post();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async addColumn(ev) {
 | 
			
		||||
		const addColumn = async (ev) => {
 | 
			
		||||
			const columns = [
 | 
			
		||||
				'main',
 | 
			
		||||
				'widgets',
 | 
			
		||||
| 
						 | 
				
			
			@ -122,33 +104,83 @@ export default defineComponent({
 | 
			
		|||
			];
 | 
			
		||||
 | 
			
		||||
			const { canceled, result: column } = await os.select({
 | 
			
		||||
				title: this.$ts._deck.addColumn,
 | 
			
		||||
				title: i18n.locale._deck.addColumn,
 | 
			
		||||
				items: columns.map(column => ({
 | 
			
		||||
					value: column, text: this.$t('_deck._columns.' + column)
 | 
			
		||||
					value: column, text: i18n.t('_deck._columns.' + column)
 | 
			
		||||
				}))
 | 
			
		||||
			});
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
 | 
			
		||||
			addColumn({
 | 
			
		||||
			addColumnToStore({
 | 
			
		||||
				type: column,
 | 
			
		||||
				id: uuid(),
 | 
			
		||||
				name: this.$t('_deck._columns.' + column),
 | 
			
		||||
				name: i18n.t('_deck._columns.' + column),
 | 
			
		||||
				width: 330,
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		onContextmenu(e) {
 | 
			
		||||
		const onContextmenu = (ev) => {
 | 
			
		||||
			os.contextMenu([{
 | 
			
		||||
				text: this.$ts._deck.addColumn,
 | 
			
		||||
				text: i18n.locale._deck.addColumn,
 | 
			
		||||
				icon: null,
 | 
			
		||||
				action: this.addColumn
 | 
			
		||||
			}], e);
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
				action: addColumn
 | 
			
		||||
			}], ev);
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		provide('shouldSpacerMin', true);
 | 
			
		||||
		if (deckStore.state.navWindow) {
 | 
			
		||||
			provide('navHook', (url) => {
 | 
			
		||||
				os.pageWindow(url);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		document.documentElement.style.overflowY = 'hidden';
 | 
			
		||||
		document.documentElement.style.scrollBehavior = 'auto';
 | 
			
		||||
		window.addEventListener('wheel', (ev) => {
 | 
			
		||||
			if (getScrollContainer(ev.target) == null) {
 | 
			
		||||
				document.documentElement.scrollLeft += ev.deltaY > 0 ? 96 : -96;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
		loadDeck();
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			isMobile,
 | 
			
		||||
			deckStore,
 | 
			
		||||
			drawerMenuShowing,
 | 
			
		||||
			columns,
 | 
			
		||||
			layout,
 | 
			
		||||
			menuIndicated,
 | 
			
		||||
			onContextmenu,
 | 
			
		||||
			wallpaper: localStorage.getItem('wallpaper') != null,
 | 
			
		||||
			post: os.post,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.menu-enter-active,
 | 
			
		||||
.menu-leave-active {
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
	transform: translateX(0);
 | 
			
		||||
	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 | 
			
		||||
}
 | 
			
		||||
.menu-enter-from,
 | 
			
		||||
.menu-leave-active {
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
	transform: translateX(-240px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.menu-back-enter-active,
 | 
			
		||||
.menu-back-leave-active {
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 | 
			
		||||
}
 | 
			
		||||
.menu-back-enter-from,
 | 
			
		||||
.menu-back-leave-active {
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mk-deck {
 | 
			
		||||
	$nav-hide-threshold: 650px; // TODO: どこかに集約したい
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -172,6 +204,10 @@ export default defineComponent({
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.isMobile {
 | 
			
		||||
		padding-bottom: 100px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .column {
 | 
			
		||||
		flex-shrink: 0;
 | 
			
		||||
		margin-right: var(--deckMargin);
 | 
			
		||||
| 
						 | 
				
			
			@ -186,43 +222,88 @@ export default defineComponent({
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .post,
 | 
			
		||||
	> .nav {
 | 
			
		||||
	> .buttons {
 | 
			
		||||
		position: fixed;
 | 
			
		||||
		z-index: 1000;
 | 
			
		||||
		bottom: 32px;
 | 
			
		||||
		width: 64px;
 | 
			
		||||
		height: 64px;
 | 
			
		||||
		border-radius: 100%;
 | 
			
		||||
		box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
 | 
			
		||||
		font-size: 22px;
 | 
			
		||||
		bottom: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		padding: 16px;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		box-sizing: border-box;
 | 
			
		||||
 | 
			
		||||
		@media (min-width: ($nav-hide-threshold + 1px)) {
 | 
			
		||||
			display: none;
 | 
			
		||||
		> .button {
 | 
			
		||||
			position: relative;
 | 
			
		||||
			flex: 1;
 | 
			
		||||
			padding: 0;
 | 
			
		||||
			margin: auto;
 | 
			
		||||
			height: 64px;
 | 
			
		||||
			border-radius: 8px;
 | 
			
		||||
			background: var(--panel);
 | 
			
		||||
			color: var(--fg);
 | 
			
		||||
 | 
			
		||||
			&:not(:last-child) {
 | 
			
		||||
				margin-right: 12px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@media (max-width: 400px) {
 | 
			
		||||
				height: 60px;
 | 
			
		||||
 | 
			
		||||
				&:not(:last-child) {
 | 
			
		||||
					margin-right: 8px;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:hover {
 | 
			
		||||
				background: var(--X2);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .indicator {
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 0;
 | 
			
		||||
				left: 0;
 | 
			
		||||
				color: var(--indicator);
 | 
			
		||||
				font-size: 16px;
 | 
			
		||||
				animation: blink 1s infinite;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:first-child {
 | 
			
		||||
				margin-left: 0;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:last-child {
 | 
			
		||||
				margin-right: 0;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> * {
 | 
			
		||||
				font-size: 22px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:disabled {
 | 
			
		||||
				cursor: default;
 | 
			
		||||
 | 
			
		||||
				> * {
 | 
			
		||||
					opacity: 0.5;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .post {
 | 
			
		||||
		right: 32px;
 | 
			
		||||
	> .menu-back {
 | 
			
		||||
		z-index: 1001;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .nav {
 | 
			
		||||
		left: 32px;
 | 
			
		||||
		background: var(--panel);
 | 
			
		||||
		color: var(--fg);
 | 
			
		||||
 | 
			
		||||
		&:hover {
 | 
			
		||||
			background: var(--X2);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .indicator {
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			left: 0;
 | 
			
		||||
			color: var(--indicator);
 | 
			
		||||
			font-size: 16px;
 | 
			
		||||
			animation: blink 1s infinite;
 | 
			
		||||
		}
 | 
			
		||||
	> .menu {
 | 
			
		||||
		position: fixed;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		z-index: 1001;
 | 
			
		||||
		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 | 
			
		||||
		height: calc(var(--vh, 1vh) * 100);
 | 
			
		||||
		width: 240px;
 | 
			
		||||
		box-sizing: border-box;
 | 
			
		||||
		overflow: auto;
 | 
			
		||||
		background: var(--bg);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="mk-app" :class="{ wallpaper }">
 | 
			
		||||
	<XSidebar ref="nav" class="sidebar"/>
 | 
			
		||||
<div class="dkgtipfy" :class="{ wallpaper }">
 | 
			
		||||
	<XSidebar v-if="!isMobile" class="sidebar"/>
 | 
			
		||||
 | 
			
		||||
	<div ref="contents" class="contents" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
 | 
			
		||||
		<main ref="main">
 | 
			
		||||
	<div class="contents" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
 | 
			
		||||
		<main>
 | 
			
		||||
			<div class="content">
 | 
			
		||||
				<MkStickyContainer>
 | 
			
		||||
					<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
 | 
			
		||||
| 
						 | 
				
			
			@ -20,32 +20,44 @@
 | 
			
		|||
		</main>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<XSide v-if="isDesktop" ref="side" class="side"/>
 | 
			
		||||
	<XSideView v-if="isDesktop" ref="side" class="side"/>
 | 
			
		||||
 | 
			
		||||
	<div v-if="isDesktop" ref="widgets" class="widgets">
 | 
			
		||||
	<div v-if="isDesktop" ref="widgetsEl" class="widgets">
 | 
			
		||||
		<XWidgets @mounted="attachSticky"/>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="buttons" :class="{ navHidden }">
 | 
			
		||||
		<button ref="navButton" class="button nav _button" @click="showNav"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
 | 
			
		||||
	<button class="widgetButton _button" :class="{ show: true }" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
 | 
			
		||||
 | 
			
		||||
	<div v-if="isMobile" class="buttons">
 | 
			
		||||
		<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
 | 
			
		||||
		<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
 | 
			
		||||
		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
 | 
			
		||||
		<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
 | 
			
		||||
		<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
 | 
			
		||||
		<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<button class="widgetButton _button" :class="{ navHidden }" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
 | 
			
		||||
	<transition name="menuDrawer-back">
 | 
			
		||||
		<div v-if="drawerMenuShowing"
 | 
			
		||||
			class="menuDrawer-back _modalBg"
 | 
			
		||||
			@click="drawerMenuShowing = false"
 | 
			
		||||
			@touchstart.passive="drawerMenuShowing = false"
 | 
			
		||||
		></div>
 | 
			
		||||
	</transition>
 | 
			
		||||
 | 
			
		||||
	<transition name="tray-back">
 | 
			
		||||
	<transition name="menuDrawer">
 | 
			
		||||
		<XDrawerMenu v-if="drawerMenuShowing" class="menuDrawer"/>
 | 
			
		||||
	</transition>
 | 
			
		||||
 | 
			
		||||
	<transition name="widgetsDrawer-back">
 | 
			
		||||
		<div v-if="widgetsShowing"
 | 
			
		||||
			class="tray-back _modalBg"
 | 
			
		||||
			class="widgetsDrawer-back _modalBg"
 | 
			
		||||
			@click="widgetsShowing = false"
 | 
			
		||||
			@touchstart.passive="widgetsShowing = false"
 | 
			
		||||
		></div>
 | 
			
		||||
	</transition>
 | 
			
		||||
 | 
			
		||||
	<transition name="tray">
 | 
			
		||||
		<XWidgets v-if="widgetsShowing" class="tray"/>
 | 
			
		||||
	<transition name="widgetsDrawer">
 | 
			
		||||
		<XWidgets v-if="widgetsShowing" class="widgetsDrawer"/>
 | 
			
		||||
	</transition>
 | 
			
		||||
 | 
			
		||||
	<XCommon/>
 | 
			
		||||
| 
						 | 
				
			
			@ -53,60 +65,69 @@
 | 
			
		|||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, defineAsyncComponent } from 'vue';
 | 
			
		||||
import { defineComponent, defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue';
 | 
			
		||||
import { instanceName } from '@/config';
 | 
			
		||||
import { StickySidebar } from '@/scripts/sticky-sidebar';
 | 
			
		||||
import XSidebar from '@/ui/_common_/sidebar.vue';
 | 
			
		||||
import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
 | 
			
		||||
import XCommon from './_common_/common.vue';
 | 
			
		||||
import XSide from './classic.side.vue';
 | 
			
		||||
import XSideView from './classic.side.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { menuDef } from '@/menu';
 | 
			
		||||
import * as symbols from '@/symbols';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import * as EventEmitter from 'eventemitter3';
 | 
			
		||||
import { menuDef } from '@/menu';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
const DESKTOP_THRESHOLD = 1100;
 | 
			
		||||
const MOBILE_THRESHOLD = 500;
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XCommon,
 | 
			
		||||
		XSidebar,
 | 
			
		||||
		XDrawerMenu,
 | 
			
		||||
		XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')),
 | 
			
		||||
		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
 | 
			
		||||
		XSideView, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	provide() {
 | 
			
		||||
		return {
 | 
			
		||||
			sideViewHook: this.isDesktop ? (url) => {
 | 
			
		||||
				this.$refs.side.navigate(url);
 | 
			
		||||
			} : null
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	setup() {
 | 
			
		||||
		const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
 | 
			
		||||
		const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD);
 | 
			
		||||
		window.addEventListener('resize', () => {
 | 
			
		||||
			isMobile.value = window.innerWidth <= MOBILE_THRESHOLD;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			pageInfo: null,
 | 
			
		||||
			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
 | 
			
		||||
			menuDef: menuDef,
 | 
			
		||||
			navHidden: false,
 | 
			
		||||
			widgetsShowing: false,
 | 
			
		||||
			wallpaper: localStorage.getItem('wallpaper') != null,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
		const pageInfo = ref();
 | 
			
		||||
		const widgetsEl = ref<HTMLElement>();
 | 
			
		||||
		const widgetsShowing = ref(false);
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		navIndicated(): boolean {
 | 
			
		||||
			for (const def in this.menuDef) {
 | 
			
		||||
		const sideViewController = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
		provide('sideViewHook', isDesktop.value ? (url) => {
 | 
			
		||||
			sideViewController.emit('navigate', url);
 | 
			
		||||
		} : null);
 | 
			
		||||
 | 
			
		||||
		const menuIndicated = computed(() => {
 | 
			
		||||
			for (const def in menuDef) {
 | 
			
		||||
				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
 | 
			
		||||
				if (this.menuDef[def].indicated) return true;
 | 
			
		||||
				if (menuDef[def].indicated) return true;
 | 
			
		||||
			}
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const drawerMenuShowing = ref(false);
 | 
			
		||||
 | 
			
		||||
		const route = useRoute();
 | 
			
		||||
		watch(route, () => {
 | 
			
		||||
			drawerMenuShowing.value = false;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		document.documentElement.style.overflowY = 'scroll';
 | 
			
		||||
 | 
			
		||||
		if (this.$store.state.widgets.length === 0) {
 | 
			
		||||
			this.$store.set('widgets', [{
 | 
			
		||||
		if (defaultStore.state.widgets.length === 0) {
 | 
			
		||||
			defaultStore.set('widgets', [{
 | 
			
		||||
				name: 'calendar',
 | 
			
		||||
				id: 'a', place: 'right', data: {}
 | 
			
		||||
			}, {
 | 
			
		||||
| 
						 | 
				
			
			@ -117,123 +138,129 @@ export default defineComponent({
 | 
			
		|||
				id: 'c', place: 'right', data: {}
 | 
			
		||||
			}]);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.adjustUI();
 | 
			
		||||
 | 
			
		||||
		const ro = new ResizeObserver((entries, observer) => {
 | 
			
		||||
			this.adjustUI();
 | 
			
		||||
		onMounted(() => {
 | 
			
		||||
			if (!isDesktop.value) {
 | 
			
		||||
				window.addEventListener('resize', () => {
 | 
			
		||||
					if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
 | 
			
		||||
				}, { passive: true });
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		ro.observe(this.$refs.contents);
 | 
			
		||||
 | 
			
		||||
		window.addEventListener('resize', this.adjustUI, { passive: true });
 | 
			
		||||
 | 
			
		||||
		if (!this.isDesktop) {
 | 
			
		||||
			window.addEventListener('resize', () => {
 | 
			
		||||
				if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
 | 
			
		||||
			}, { passive: true });
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		changePage(page) {
 | 
			
		||||
		const changePage = (page) => {
 | 
			
		||||
			if (page == null) return;
 | 
			
		||||
			if (page[symbols.PAGE_INFO]) {
 | 
			
		||||
				this.pageInfo = page[symbols.PAGE_INFO];
 | 
			
		||||
				document.title = `${this.pageInfo.title} | ${instanceName}`;
 | 
			
		||||
				pageInfo.value = page[symbols.PAGE_INFO];
 | 
			
		||||
				document.title = `${pageInfo.value.title} | ${instanceName}`;
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		adjustUI() {
 | 
			
		||||
			const navWidth = this.$refs.nav.$el.offsetWidth;
 | 
			
		||||
			this.navHidden = navWidth === 0;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		showNav() {
 | 
			
		||||
			this.$refs.nav.show();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		attachSticky(el) {
 | 
			
		||||
			const sticky = new StickySidebar(this.$refs.widgets);
 | 
			
		||||
			window.addEventListener('scroll', () => {
 | 
			
		||||
				sticky.calc(window.scrollY);
 | 
			
		||||
			}, { passive: true });
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		post() {
 | 
			
		||||
			os.post();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		top() {
 | 
			
		||||
			window.scroll({ top: 0, behavior: 'smooth' });
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		back() {
 | 
			
		||||
			history.back();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onTransition() {
 | 
			
		||||
			if (window._scroll) window._scroll();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onContextmenu(e) {
 | 
			
		||||
		const onContextmenu = (ev) => {
 | 
			
		||||
			const isLink = (el: HTMLElement) => {
 | 
			
		||||
				if (el.tagName === 'A') return true;
 | 
			
		||||
				if (el.parentElement) {
 | 
			
		||||
					return isLink(el.parentElement);
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
			if (isLink(e.target)) return;
 | 
			
		||||
			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
 | 
			
		||||
			if (isLink(ev.target)) return;
 | 
			
		||||
			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
 | 
			
		||||
			if (window.getSelection().toString() !== '') return;
 | 
			
		||||
			const path = this.$route.path;
 | 
			
		||||
			const path = route.path;
 | 
			
		||||
			os.contextMenu([{
 | 
			
		||||
				type: 'label',
 | 
			
		||||
				text: path,
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-columns',
 | 
			
		||||
				text: this.$ts.openInSideView,
 | 
			
		||||
				text: i18n.locale.openInSideView,
 | 
			
		||||
				action: () => {
 | 
			
		||||
					this.$refs.side.navigate(path);
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-window-maximize',
 | 
			
		||||
				text: this.$ts.openInWindow,
 | 
			
		||||
				text: i18n.locale.openInWindow,
 | 
			
		||||
				action: () => {
 | 
			
		||||
					os.pageWindow(path);
 | 
			
		||||
				}
 | 
			
		||||
			}], e);
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
			}], ev);
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const attachSticky = (el) => {
 | 
			
		||||
			const sticky = new StickySidebar(widgetsEl.value);
 | 
			
		||||
			window.addEventListener('scroll', () => {
 | 
			
		||||
				sticky.calc(window.scrollY);
 | 
			
		||||
			}, { passive: true });
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			pageInfo,
 | 
			
		||||
			isDesktop,
 | 
			
		||||
			isMobile,
 | 
			
		||||
			widgetsEl,
 | 
			
		||||
			widgetsShowing,
 | 
			
		||||
			drawerMenuShowing,
 | 
			
		||||
			menuIndicated,
 | 
			
		||||
			wallpaper: localStorage.getItem('wallpaper') != null,
 | 
			
		||||
			changePage,
 | 
			
		||||
			top: () => {
 | 
			
		||||
				window.scroll({ top: 0, behavior: 'smooth' });
 | 
			
		||||
			},
 | 
			
		||||
			onTransition: () => {
 | 
			
		||||
				if (window._scroll) window._scroll();
 | 
			
		||||
			},
 | 
			
		||||
			post: os.post,
 | 
			
		||||
			onContextmenu,
 | 
			
		||||
			attachSticky,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.tray-enter-active,
 | 
			
		||||
.tray-leave-active {
 | 
			
		||||
.widgetsDrawer-enter-active,
 | 
			
		||||
.widgetsDrawer-leave-active {
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
	transform: translateX(0);
 | 
			
		||||
	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 | 
			
		||||
}
 | 
			
		||||
.tray-enter-from,
 | 
			
		||||
.tray-leave-active {
 | 
			
		||||
.widgetsDrawer-enter-from,
 | 
			
		||||
.widgetsDrawer-leave-active {
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
	transform: translateX(240px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tray-back-enter-active,
 | 
			
		||||
.tray-back-leave-active {
 | 
			
		||||
.widgetsDrawer-back-enter-active,
 | 
			
		||||
.widgetsDrawer-back-leave-active {
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 | 
			
		||||
}
 | 
			
		||||
.tray-back-enter-from,
 | 
			
		||||
.tray-back-leave-active {
 | 
			
		||||
.widgetsDrawer-back-enter-from,
 | 
			
		||||
.widgetsDrawer-back-leave-active {
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mk-app {
 | 
			
		||||
.menuDrawer-enter-active,
 | 
			
		||||
.menuDrawer-leave-active {
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
	transform: translateX(0);
 | 
			
		||||
	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 | 
			
		||||
}
 | 
			
		||||
.menuDrawer-enter-from,
 | 
			
		||||
.menuDrawer-leave-active {
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
	transform: translateX(-240px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.menuDrawer-back-enter-active,
 | 
			
		||||
.menuDrawer-back-leave-active {
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 | 
			
		||||
}
 | 
			
		||||
.menuDrawer-back-enter-from,
 | 
			
		||||
.menuDrawer-back-leave-active {
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dkgtipfy {
 | 
			
		||||
	$ui-font-size: 1em; // TODO: どこかに集約したい
 | 
			
		||||
	$widgets-hide-threshold: 1090px;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -285,6 +312,7 @@ export default defineComponent({
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
	> .widgetButton {
 | 
			
		||||
		display: block;
 | 
			
		||||
		position: fixed;
 | 
			
		||||
| 
						 | 
				
			
			@ -305,12 +333,34 @@ export default defineComponent({
 | 
			
		|||
		@media (min-width: ($widgets-hide-threshold + 1px)) {
 | 
			
		||||
			display: none;
 | 
			
		||||
		}
 | 
			
		||||
	}*/
 | 
			
		||||
 | 
			
		||||
	> .widgetButton {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .widgetsDrawer-back {
 | 
			
		||||
		z-index: 1001;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .widgetsDrawer {
 | 
			
		||||
		position: fixed;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		right: 0;
 | 
			
		||||
		z-index: 1001;
 | 
			
		||||
		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 | 
			
		||||
		height: calc(var(--vh, 1vh) * 100);
 | 
			
		||||
		padding: var(--margin);
 | 
			
		||||
		box-sizing: border-box;
 | 
			
		||||
		overflow: auto;
 | 
			
		||||
		background: var(--bg);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .buttons {
 | 
			
		||||
		position: fixed;
 | 
			
		||||
		z-index: 1000;
 | 
			
		||||
		bottom: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		padding: 16px;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			@ -319,10 +369,6 @@ export default defineComponent({
 | 
			
		|||
		backdrop-filter: var(--blur, blur(32px));
 | 
			
		||||
		background-color: var(--header);
 | 
			
		||||
 | 
			
		||||
		&:not(.navHidden) {
 | 
			
		||||
			display: none;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .button {
 | 
			
		||||
			position: relative;
 | 
			
		||||
			flex: 1;
 | 
			
		||||
| 
						 | 
				
			
			@ -380,22 +426,23 @@ export default defineComponent({
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .tray-back {
 | 
			
		||||
	> .menuDrawer-back {
 | 
			
		||||
		z-index: 1001;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .tray {
 | 
			
		||||
	> .menuDrawer {
 | 
			
		||||
		position: fixed;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		right: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		z-index: 1001;
 | 
			
		||||
		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 | 
			
		||||
		height: calc(var(--vh, 1vh) * 100);
 | 
			
		||||
		padding: var(--margin);
 | 
			
		||||
		width: 240px;
 | 
			
		||||
		box-sizing: border-box;
 | 
			
		||||
		overflow: auto;
 | 
			
		||||
		background: var(--bg);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue