Merge branch 'misskey-develop' into merge/2025-03-24
This commit is contained in:
commit
4ae26e6e18
113 changed files with 1303 additions and 1720 deletions
|
|
@ -2,20 +2,18 @@
|
|||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
import { channel } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkChannelFollowButton from './MkChannelFollowButton.vue';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
return new Promise(resolve => window.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -2,18 +2,16 @@
|
|||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkClickerGame from './MkClickerGame.vue';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
return new Promise(resolve => window.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -4,41 +4,45 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<nav :class="$style.nav">
|
||||
<div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
|
||||
<XNavFolder
|
||||
:class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
|
||||
:parentFolder="folder"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
<template v-for="f in hierarchyFolders">
|
||||
<span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<nav :class="$style.nav">
|
||||
<div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
|
||||
<XNavFolder
|
||||
:folder="f"
|
||||
:class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
|
||||
:parentFolder="folder"
|
||||
:class="[$style.navPathItem]"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
|
||||
</div>
|
||||
<div :class="$style.navMenu">
|
||||
<!-- "Search drive via alt text or file names" -->
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" :placeholder="i18n.ts.driveSearchbarPlaceholder" @enter="fetch">
|
||||
<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<template v-for="f in hierarchyFolders">
|
||||
<span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
<XNavFolder
|
||||
:folder="f"
|
||||
:parentFolder="folder"
|
||||
:class="[$style.navPathItem]"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
|
||||
</div>
|
||||
<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||
</div>
|
||||
</nav>
|
||||
<div :class="$style.navMenu">
|
||||
<!-- "Search drive via alt text or file names" -->
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" :placeholder="i18n.ts.driveSearchbarPlaceholder" @enter="fetch">
|
||||
<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<div
|
||||
ref="main"
|
||||
:class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]"
|
||||
|
|
@ -98,8 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
<div v-if="draghover" :class="$style.dropzone"></div>
|
||||
<input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -118,6 +121,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { uploadFile, uploads } from '@/utility/upload.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { chooseFileFromPc } from '@/utility/select-file.js';
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
|
|
@ -140,7 +144,6 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const loadMoreFiles = useTemplateRef('loadMoreFiles');
|
||||
const fileInput = useTemplateRef('fileInput');
|
||||
|
||||
const folder = ref<Misskey.entities.DriveFolder | null>(null);
|
||||
const files = ref<Misskey.entities.DriveFile[]>([]);
|
||||
|
|
@ -152,7 +155,6 @@ const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
|
|||
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const uploadings = uploads;
|
||||
const connection = useStream().useChannel('drive');
|
||||
const keepOriginal = ref<boolean>(prefer.s.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
|
||||
|
||||
// ドロップされようとしているか
|
||||
const draghover = ref(false);
|
||||
|
|
@ -314,10 +316,6 @@ function onDrop(ev: DragEvent) {
|
|||
//#endregion
|
||||
}
|
||||
|
||||
function selectLocalFile() {
|
||||
fileInput.value?.click();
|
||||
}
|
||||
|
||||
function urlUpload() {
|
||||
os.inputText({
|
||||
title: i18n.ts.uploadFromUrl,
|
||||
|
|
@ -393,15 +391,8 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
|||
});
|
||||
}
|
||||
|
||||
function onChangeFileInput() {
|
||||
if (!fileInput.value?.files) return;
|
||||
for (const file of Array.from(fileInput.value.files)) {
|
||||
upload(file, folder.value);
|
||||
}
|
||||
}
|
||||
|
||||
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
|
||||
uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
|
||||
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null, keepOriginal?: boolean) {
|
||||
uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal).then(res => {
|
||||
addFile(res, true);
|
||||
});
|
||||
}
|
||||
|
|
@ -644,16 +635,20 @@ function getMenu() {
|
|||
const menu: MenuItem[] = [];
|
||||
|
||||
menu.push({
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal,
|
||||
}, { type: 'divider' }, {
|
||||
text: i18n.ts.addFile,
|
||||
type: 'label',
|
||||
}, {
|
||||
text: i18n.ts.upload + ' (' + i18n.ts.compress + ')',
|
||||
icon: 'ti ti-upload',
|
||||
action: () => {
|
||||
chooseFileFromPc(true, { keepOriginal: false });
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.upload,
|
||||
icon: 'ti ti-upload',
|
||||
action: () => { selectLocalFile(); },
|
||||
action: () => {
|
||||
chooseFileFromPc(true, { keepOriginal: true });
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.fromUrl,
|
||||
icon: 'ti ti-link',
|
||||
|
|
@ -765,22 +760,17 @@ onBeforeUnmount(() => {
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
font-size: 0.9em;
|
||||
box-shadow: 0 1px 0 var(--MI_THEME-divider);
|
||||
user-select: none;
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
.navPath {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ function opening() {
|
|||
picker.value?.focus();
|
||||
|
||||
// 何故かちょっと待たないとフォーカスされない
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
picker.value?.focus();
|
||||
}, 10);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ function touchMove(event: TouchEvent) {
|
|||
|
||||
pullDistance.value = 0;
|
||||
isSwiping.value = false;
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
isSwipingForClass.value = false;
|
||||
}, 400);
|
||||
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ const bufferedDataRatio = computed(() => {
|
|||
// MediaControl Events
|
||||
function onMouseOver() {
|
||||
if (controlStateTimer) {
|
||||
clearTimeout(controlStateTimer);
|
||||
window.clearTimeout(controlStateTimer);
|
||||
}
|
||||
isHoverring.value = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import { deviceKind } from '@/utility/device-kind.js';
|
|||
import { focusTrap } from '@/utility/focus-trap.js';
|
||||
import { focusParent } from '@/utility/focus.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { DI } from '@/di.js';
|
||||
|
||||
function getFixedContainer(el: Element | null): Element | null {
|
||||
if (el == null || el.tagName === 'BODY') return null;
|
||||
|
|
@ -94,7 +95,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
provide('modal', true);
|
||||
provide(DI.inModal, true);
|
||||
|
||||
const maxHeight = ref<number>();
|
||||
const fixed = ref(false);
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<MkNoteSub v-if="appearNote.reply" v-show="!renoteCollapsed && !inReplyToCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||
<!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>-->
|
||||
<!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>-->
|
||||
<div v-if="isRenote" :class="$style.renote">
|
||||
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
|
||||
|
|
@ -53,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
|
||||
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
|
||||
<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
|
||||
<MkAvatar :class="[$style.avatar, prefer.s.useStickyIcons ? $style.useSticky : null]" :user="appearNote.user" :link="!mock" :preview="!mock"/>
|
||||
<div :class="[$style.main, { [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click.stop="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined">
|
||||
<MkNoteHeader :note="appearNote" :mini="true" @click.stop/>
|
||||
<MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
|
||||
|
|
@ -922,7 +920,6 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
transition: box-shadow 0.1s ease;
|
||||
font-size: 1.05em;
|
||||
overflow: clip;
|
||||
contain: content;
|
||||
|
|
@ -993,6 +990,8 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
}
|
||||
|
||||
.skipRender {
|
||||
// TODO: これが有効だとTransitionGroupでnoteを追加するときに一瞬がくっとなってしまうのをどうにかしたい
|
||||
// Transitionが完了するのを待ってからskipRenderを付与すれば解決しそうだけどパフォーマンス的な影響が不明
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 0 150px;
|
||||
}
|
||||
|
|
@ -1127,9 +1126,12 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
margin: 0 14px 0 0;
|
||||
width: var(--MI-avatar);
|
||||
height: var(--MI-avatar);
|
||||
position: sticky !important;
|
||||
top: calc(22px + var(--MI-stickyTop, 0px));
|
||||
left: 0;
|
||||
|
||||
&.useSticky {
|
||||
position: sticky !important;
|
||||
top: calc(22px + var(--MI-stickyTop, 0px));
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
|
|
@ -1324,7 +1326,10 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
margin: 0 10px 0 0;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
top: calc(14px + var(--MI-stickyTop, 0px));
|
||||
|
||||
&.useSticky {
|
||||
top: calc(14px + var(--MI-stickyTop, 0px));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,38 +13,34 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<template #default="{ items: notes }">
|
||||
<div :class="[$style.root, { [$style.noGap]: noGap }]">
|
||||
<MkDateSeparatedList
|
||||
ref="notes"
|
||||
v-slot="{ item: note }"
|
||||
:items="notes"
|
||||
:direction="pagination.reversed ? 'up' : 'down'"
|
||||
:reversed="pagination.reversed"
|
||||
:noGap="noGap"
|
||||
:ad="true"
|
||||
:class="$style.notes"
|
||||
>
|
||||
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
</MkDateSeparatedList>
|
||||
</div>
|
||||
<component
|
||||
:is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
:moveClass=" $style.transition_x_move"
|
||||
tag="div"
|
||||
>
|
||||
<template v-for="(note, i) in notes" :key="note.id">
|
||||
<DynamicNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
<div v-if="note._shouldInsertAd_" :class="$style.ad">
|
||||
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
|
||||
</div>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, useTemplateRef } from 'vue';
|
||||
import { useTemplateRef, TransitionGroup } from 'vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import DynamicNote from '@/components/DynamicNote.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const MkNote = defineAsyncComponent(() =>
|
||||
(defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') :
|
||||
(defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') :
|
||||
null
|
||||
);
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = defineProps<{
|
||||
pagination: Paging;
|
||||
|
|
@ -60,24 +56,49 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
&.noGap {
|
||||
border-radius: var(--MI-radius);
|
||||
.transition_x_move,
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
|
||||
}
|
||||
.transition_x_enterFrom,
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.transition_x_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
> .notes {
|
||||
background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent);
|
||||
.root {
|
||||
container-type: inline-size;
|
||||
|
||||
&.noGap {
|
||||
background: var(--MI_THEME-panel);
|
||||
|
||||
.note {
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
.ad {
|
||||
padding: 8px;
|
||||
background-size: auto auto;
|
||||
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.noGap) {
|
||||
> .notes {
|
||||
background: var(--MI_THEME-bg);
|
||||
background: var(--MI_THEME-bg);
|
||||
|
||||
.note {
|
||||
background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent);
|
||||
border-radius: var(--MI-radius);
|
||||
}
|
||||
.note {
|
||||
background: var(--MI_THEME-panel);
|
||||
border-radius: var(--MI-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ad:empty {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -14,34 +14,38 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<template #default="{ items: notifications }">
|
||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
|
||||
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
|
||||
</MkDateSeparatedList>
|
||||
<component
|
||||
:is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.notifications]"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
:moveClass=" $style.transition_x_move"
|
||||
tag="div"
|
||||
>
|
||||
<template v-for="(notification, i) in notifications" :key="notification.id">
|
||||
<DynamicNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true"/>
|
||||
<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true"/>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkPullToRefresh>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onUnmounted, onDeactivated, onMounted, computed, useTemplateRef, onActivated } from 'vue';
|
||||
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { notificationTypes } from '@@/js/const.js';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import XNotification from '@/components/MkNotification.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import DynamicNote from '@/components/DynamicNote.vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const MkNote = defineAsyncComponent(() =>
|
||||
(defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') :
|
||||
(defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') :
|
||||
null
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
excludeTypes?: typeof notificationTypes[number][];
|
||||
}>();
|
||||
|
|
@ -89,28 +93,36 @@ onMounted(() => {
|
|||
connection.on('notificationFlushed', reload);
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
pagingComponent.value?.reload();
|
||||
connection = useStream().useChannel('main');
|
||||
connection.on('notification', onNotification);
|
||||
connection.on('notificationFlushed', reload);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (connection) connection.dispose();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
if (connection) connection.dispose();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
reload,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.list {
|
||||
.transition_x_move,
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
|
||||
}
|
||||
.transition_x_enterFrom,
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.transition_x_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
container-type: inline-size;
|
||||
background: var(--MI_THEME-panel);
|
||||
}
|
||||
|
||||
.item {
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<MkError v-else-if="error" @retry="init()"/>
|
||||
|
||||
<div v-else-if="empty" key="_empty_" class="empty">
|
||||
<div v-else-if="empty" key="_empty_">
|
||||
<slot name="empty">
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
|
|
@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-else class="loading"/>
|
||||
<MkLoading v-else/>
|
||||
</div>
|
||||
<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
|
||||
<div v-show="!pagination.reversed && more" key="_more_">
|
||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-else class="loading"/>
|
||||
<MkLoading v-else/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
|
@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, useTemplateRef, watch, type Ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
|
||||
import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible } from '@@/js/scroll.js';
|
||||
import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible } from '@@/js/scroll.js';
|
||||
import type { ComputedRef } from 'vue';
|
||||
import type { MisskeyEntity } from '@/types/date-separated-list.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
|
@ -268,7 +268,7 @@ const fetchMore = async (): Promise<void> => {
|
|||
|
||||
return nextTick(() => {
|
||||
if (scrollableElement.value) {
|
||||
scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
|
||||
scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
|
||||
} else {
|
||||
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
|
||||
}
|
||||
|
|
@ -368,7 +368,7 @@ watch(visibility, () => {
|
|||
BACKGROUND_PAUSE_WAIT_SEC * 1000);
|
||||
} else { // 'visible'
|
||||
if (timerForSetPause) {
|
||||
clearTimeout(timerForSetPause);
|
||||
window.clearTimeout(timerForSetPause);
|
||||
timerForSetPause = null;
|
||||
} else {
|
||||
isPausingUpdate = false;
|
||||
|
|
@ -464,11 +464,11 @@ onBeforeMount(() => {
|
|||
init().then(() => {
|
||||
if (props.pagination.reversed) {
|
||||
nextTick(() => {
|
||||
setTimeout(toBottom, 800);
|
||||
window.setTimeout(toBottom, 800);
|
||||
|
||||
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
|
||||
// more = trueを遅らせる
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
moreFetching.value = false;
|
||||
}, 2000);
|
||||
});
|
||||
|
|
@ -478,11 +478,11 @@ onBeforeMount(() => {
|
|||
|
||||
onBeforeUnmount(() => {
|
||||
if (timerForSetPause) {
|
||||
clearTimeout(timerForSetPause);
|
||||
window.clearTimeout(timerForSetPause);
|
||||
timerForSetPause = null;
|
||||
}
|
||||
if (preventAppearFetchMoreTimer.value) {
|
||||
clearTimeout(preventAppearFetchMoreTimer.value);
|
||||
window.clearTimeout(preventAppearFetchMoreTimer.value);
|
||||
preventAppearFetchMoreTimer.value = null;
|
||||
}
|
||||
scrollObserver.value?.disconnect();
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ import { DI } from '@/di.js';
|
|||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const modal = inject('modal');
|
||||
const modal = inject(DI.inModal, false);
|
||||
|
||||
const props = withDefaults(defineProps<PostFormProps & {
|
||||
fixed?: boolean;
|
||||
|
|
@ -1431,7 +1431,7 @@ defineExpose({
|
|||
padding: 0 24px;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
font-size: 110%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="{ [$style.slotClip]: isPullStart }">
|
||||
<slot/>
|
||||
</div>
|
||||
|
||||
<slot/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -82,11 +81,11 @@ function moveBySystem(to: number): Promise<void> {
|
|||
return;
|
||||
}
|
||||
const startTime = Date.now();
|
||||
let intervalId = setInterval(() => {
|
||||
let intervalId = window.setInterval(() => {
|
||||
const time = Date.now() - startTime;
|
||||
if (time > RELEASE_TRANSITION_DURATION) {
|
||||
pullDistance.value = to;
|
||||
clearInterval(intervalId);
|
||||
window.clearInterval(intervalId);
|
||||
r();
|
||||
return;
|
||||
}
|
||||
|
|
@ -261,8 +260,4 @@ defineExpose({
|
|||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.slotClip {
|
||||
overflow-y: clip;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,22 +4,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<TransitionGroup
|
||||
:enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''"
|
||||
:leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''"
|
||||
:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
|
||||
<component
|
||||
:is="prefer.s.animation ? TransitionGroup : 'div'"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
:moveClass="$style.transition_x_move"
|
||||
tag="div" :class="$style.root"
|
||||
>
|
||||
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
|
||||
<slot v-if="hasMoreReactions" name="more"/>
|
||||
</TransitionGroup>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { inject, watch, ref } from 'vue';
|
||||
import { TransitionGroup } from 'vue';
|
||||
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { DI } from '@/di.js';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:leaveActiveClass="prefer.s.animation ? $style.transition_tooltip_leaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.transition_tooltip_enterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.transition_tooltip_leaveTo : ''"
|
||||
appear @afterLeave="emit('closed')"
|
||||
appear :css="prefer.s.animation"
|
||||
@afterLeave="emit('closed')"
|
||||
>
|
||||
<div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
|
||||
<slot>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -53,7 +52,7 @@ import { prefer } from '@/preferences.js';
|
|||
type Ad = (typeof instance)['ads'][number];
|
||||
|
||||
const props = defineProps<{
|
||||
prefer: string[];
|
||||
preferForms: string[];
|
||||
specify?: Ad;
|
||||
}>();
|
||||
|
||||
|
|
@ -72,7 +71,7 @@ const choseAd = (): Ad | null => {
|
|||
ratio: 0,
|
||||
} : ad);
|
||||
|
||||
let ads = allAds.filter(ad => props.prefer.includes(ad.place));
|
||||
let ads = allAds.filter(ad => props.preferForms.includes(ad.place));
|
||||
|
||||
if (ads.length === 0) {
|
||||
ads = allAds.filter(ad => ad.place === 'square');
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const props = defineProps<{
|
|||
menuReaction?: boolean;
|
||||
}>();
|
||||
|
||||
const react = inject(DI.mfmEmojiReactCallback);
|
||||
const react = inject(DI.mfmEmojiReactCallback, null);
|
||||
|
||||
const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : prefer.s.emojiStyle === 'tossface' ? char2tossfaceFilePath : char2fluentEmojiFilePath;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@
|
|||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
|
||||
import { waitFor } from '@storybook/test';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkPageHeader from './MkPageHeader.vue';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
export const Empty = {
|
||||
render(args) {
|
||||
return {
|
||||
|
|
@ -29,7 +28,7 @@ export const Empty = {
|
|||
};
|
||||
},
|
||||
async play() {
|
||||
const wait = new Promise((resolve) => setTimeout(resolve, 800));
|
||||
const wait = new Promise((resolve) => window.setTimeout(resolve, 800));
|
||||
await waitFor(async () => await wait);
|
||||
},
|
||||
args: {
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ async function enter(el: Element) {
|
|||
entering = false;
|
||||
});
|
||||
|
||||
setTimeout(renderTab, 170);
|
||||
window.setTimeout(renderTab, 170);
|
||||
}
|
||||
|
||||
function afterEnter(el: Element) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="show" ref="el" :class="[$style.root]" :style="{ background: bg }">
|
||||
<div v-if="show" ref="el" :class="[$style.root]">
|
||||
<div :class="[$style.upper, { [$style.slim]: narrow, [$style.thin]: thin_ }]">
|
||||
<div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu">
|
||||
<MkAvatar :class="$style.avatar" :user="$i"/>
|
||||
|
|
@ -51,7 +51,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { scrollToTop } from '@@/js/scroll.js';
|
||||
import XTabs from './MkPageHeader.tabs.vue';
|
||||
import type { Tab } from './MkPageHeader.tabs.vue';
|
||||
|
|
@ -82,7 +81,6 @@ const emit = defineEmits<{
|
|||
const displayBackButton = props.displayBackButton && history.state.key !== 'index' && history.length > 1 && inject('shouldBackButton', true);
|
||||
|
||||
const viewId = inject(DI.viewId);
|
||||
const viewTransitionName = computed(() => `${viewId}---pageHeader`);
|
||||
const injectedPageMetadata = inject(DI.pageMetadata);
|
||||
const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value);
|
||||
|
||||
|
|
@ -90,7 +88,6 @@ const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props
|
|||
const thin_ = props.thin || inject('shouldHeaderThin', false);
|
||||
|
||||
const el = useTemplateRef('el');
|
||||
const bg = ref<string | undefined>(undefined);
|
||||
const narrow = ref(false);
|
||||
const hasTabs = computed(() => props.tabs.length > 0);
|
||||
const hasActions = computed(() => props.actions && props.actions.length > 0);
|
||||
|
|
@ -122,19 +119,9 @@ function goBack(): void {
|
|||
window.history.back();
|
||||
}
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = 'var(--MI_THEME-bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(window.document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
tinyBg.setAlpha(0.85);
|
||||
bg.value = tinyBg.toRgbString();
|
||||
};
|
||||
|
||||
let ro: ResizeObserver | null;
|
||||
|
||||
onMounted(() => {
|
||||
calcBg();
|
||||
globalEvents.on('themeChanging', calcBg);
|
||||
|
||||
if (el.value && el.value.parentElement) {
|
||||
narrow.value = el.value.parentElement.offsetWidth < 500;
|
||||
ro = new ResizeObserver((entries, observer) => {
|
||||
|
|
@ -147,18 +134,17 @@ onMounted(() => {
|
|||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
globalEvents.off('themeChanging', calcBg);
|
||||
if (ro) ro.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
width: 100%;
|
||||
view-transition-name: v-bind(viewTransitionName);
|
||||
}
|
||||
|
||||
.upper,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']">
|
||||
<div ref="rootEl" :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']">
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="actions" :tabs="tabs"/></template>
|
||||
<div :class="$style.body">
|
||||
|
|
@ -16,6 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTemplateRef } from 'vue';
|
||||
import { scrollInContainer } from '@@/js/scroll.js';
|
||||
import type { PageHeaderItem } from '@/types/page-header.js';
|
||||
import type { Tab } from './MkPageHeader.tabs.vue';
|
||||
|
||||
|
|
@ -31,6 +33,13 @@ const props = withDefaults(defineProps<{
|
|||
});
|
||||
|
||||
const tab = defineModel<string>('tab');
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
|
||||
defineExpose({
|
||||
scrollToTop: () => {
|
||||
if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'smooth' });
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ provide(DI.routerCurrentDepth, currentDepth + 1);
|
|||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
onMounted(() => {
|
||||
rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入
|
||||
if (prefer.s.animation) {
|
||||
rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入
|
||||
}
|
||||
});
|
||||
|
||||
// view-transition-newなどの<pt-name-selector>にはcss varが使えず、v-bindできないため直接スタイルを生成
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, toRefs, watch } from 'vue';
|
||||
import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
|
||||
import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
|
||||
import type { GridContext, GridEvent } from '@/components/grid/grid-event.js';
|
||||
import type { GridColumn } from '@/components/grid/column.js';
|
||||
import type { GridRow, GridRowSetting } from '@/components/grid/row.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { GridEventEmitter } from '@/components/grid/grid.js';
|
||||
import MkDataRow from '@/components/grid/MkDataRow.vue';
|
||||
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
|
||||
|
|
@ -68,13 +74,6 @@ import { createColumn } from '@/components/grid/column.js';
|
|||
import { createRow, defaultGridRowSetting, resetRow } from '@/components/grid/row.js';
|
||||
import { handleKeyEvent } from '@/utility/key-event.js';
|
||||
|
||||
import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
|
||||
import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
|
||||
import type { GridContext, GridEvent } from '@/components/grid/grid-event.js';
|
||||
import type { GridColumn } from '@/components/grid/column.js';
|
||||
import type { GridRow, GridRowSetting } from '@/components/grid/row.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
type RowHolder = {
|
||||
row: GridRow,
|
||||
cells: GridCell[],
|
||||
|
|
@ -130,7 +129,7 @@ const bus = new GridEventEmitter();
|
|||
*
|
||||
* @see {@link onResize}
|
||||
*/
|
||||
const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries)));
|
||||
const resizeObserver = new ResizeObserver((entries) => window.setTimeout(() => onResize(entries)));
|
||||
|
||||
const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue