summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorAcid Chicken (硫酸鶏) <root@acid-chicken.com>2023-04-05 00:41:49 +0900
committerGitHub <noreply@github.com>2023-04-05 00:41:49 +0900
commit7bd0001e763a12c2b2aeb5cf4417f802cd4fbb4c (patch)
tree62ca232417372612f78761f26669b56a80d35733 /packages/frontend/src
parentMerge branch 'develop' into fix/visibility-widening (diff)
parentenhance(backend): improve cache (diff)
downloadmisskey-7bd0001e763a12c2b2aeb5cf4417f802cd4fbb4c.tar.gz
misskey-7bd0001e763a12c2b2aeb5cf4417f802cd4fbb4c.tar.bz2
misskey-7bd0001e763a12c2b2aeb5cf4417f802cd4fbb4c.zip
Merge branch 'develop' into fix/visibility-widening
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkAnalogClock.stories.impl.ts28
-rw-r--r--packages/frontend/src/components/MkButton.stories.impl.ts30
-rw-r--r--packages/frontend/src/components/MkCaptcha.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkContextMenu.vue2
-rw-r--r--packages/frontend/src/components/MkMenu.vue20
-rw-r--r--packages/frontend/src/components/MkNotification.vue32
-rw-r--r--packages/frontend/src/components/MkNotifications.vue35
-rw-r--r--packages/frontend/src/components/MkOmit.vue2
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue2
-rw-r--r--packages/frontend/src/components/MkYouTubePlayer.vue (renamed from packages/frontend/src/components/MkYoutubePlayer.vue)0
-rw-r--r--packages/frontend/src/components/global/MkA.stories.impl.ts47
-rw-r--r--packages/frontend/src/components/global/MkAcct.stories.impl.ts43
-rw-r--r--packages/frontend/src/components/global/MkAcct.vue1
-rw-r--r--packages/frontend/src/components/global/MkAd.stories.impl.ts120
-rw-r--r--packages/frontend/src/components/global/MkAd.vue2
-rw-r--r--packages/frontend/src/components/global/MkAvatar.stories.impl.ts66
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue1
-rw-r--r--packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts45
-rw-r--r--packages/frontend/src/components/global/MkEllipsis.stories.impl.ts32
-rw-r--r--packages/frontend/src/components/global/MkEllipsis.vue16
-rw-r--r--packages/frontend/src/components/global/MkEmoji.stories.impl.ts31
-rw-r--r--packages/frontend/src/components/global/MkError.stories.meta.ts5
-rw-r--r--packages/frontend/src/components/global/MkLoading.stories.impl.ts60
-rw-r--r--packages/frontend/src/components/global/MkLoading.vue8
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts74
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.stories.impl.ts98
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts3
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.tabs.vue18
-rw-r--r--packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts3
-rw-r--r--packages/frontend/src/components/global/MkTime.stories.impl.ts312
-rw-r--r--packages/frontend/src/components/global/MkTime.vue6
-rw-r--r--packages/frontend/src/components/global/MkUrl.stories.impl.ts77
-rw-r--r--packages/frontend/src/components/global/MkUserName.stories.impl.ts57
-rw-r--r--packages/frontend/src/components/global/RouterView.stories.impl.ts3
-rw-r--r--packages/frontend/src/index.mdx12
-rw-r--r--packages/frontend/src/pages/notifications.vue9
-rw-r--r--packages/frontend/src/pages/user/activity.following.vue5
-rw-r--r--packages/frontend/src/pages/user/activity.heatmap.vue3
-rw-r--r--packages/frontend/src/pages/user/activity.notes.vue5
-rw-r--r--packages/frontend/src/pages/user/activity.pv.vue5
-rw-r--r--packages/frontend/src/scripts/achievements.ts3
-rw-r--r--packages/frontend/src/scripts/test-utils.ts6
-rw-r--r--packages/frontend/src/ui/_common_/common.vue4
43 files changed, 1229 insertions, 104 deletions
diff --git a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts
new file mode 100644
index 0000000000..05190aa268
--- /dev/null
+++ b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts
@@ -0,0 +1,28 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import MkAnalogClock from './MkAnalogClock.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkAnalogClock,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkAnalogClock v-bind="props" />',
+ };
+ },
+ parameters: {
+ layout: 'fullscreen',
+ },
+} satisfies StoryObj<typeof MkAnalogClock>;
diff --git a/packages/frontend/src/components/MkButton.stories.impl.ts b/packages/frontend/src/components/MkButton.stories.impl.ts
new file mode 100644
index 0000000000..e1c1c54d10
--- /dev/null
+++ b/packages/frontend/src/components/MkButton.stories.impl.ts
@@ -0,0 +1,30 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+/* eslint-disable import/no-duplicates */
+import { StoryObj } from '@storybook/vue3';
+import MkButton from './MkButton.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkButton,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkButton v-bind="props">Text</MkButton>',
+ };
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkButton>;
diff --git a/packages/frontend/src/components/MkCaptcha.stories.impl.ts b/packages/frontend/src/components/MkCaptcha.stories.impl.ts
new file mode 100644
index 0000000000..6ac437a277
--- /dev/null
+++ b/packages/frontend/src/components/MkCaptcha.stories.impl.ts
@@ -0,0 +1,2 @@
+import MkCaptcha from './MkCaptcha.vue';
+void MkCaptcha;
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index 5bdf477241..b81c806b0c 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -17,8 +17,8 @@ import { onMounted, onBeforeUnmount } from 'vue';
import MkMenu from './MkMenu.vue';
import { MenuItem } from './types/menu.vue';
import contains from '@/scripts/contains';
-import * as os from '@/os';
import { defaultStore } from '@/store';
+import * as os from '@/os';
const props = defineProps<{
items: MenuItem[];
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 9e3022896c..e513a65a32 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -1,5 +1,5 @@
<template>
-<div>
+<div role="menu">
<div
ref="itemsEl" v-hotkey="keymap"
class="_popup _shadow"
@@ -8,37 +8,37 @@
@contextmenu.self="e => e.preventDefault()"
>
<template v-for="(item, i) in items2">
- <div v-if="item === null" :class="$style.divider"></div>
- <span v-else-if="item.type === 'label'" :class="[$style.label, $style.item]">
+ <div v-if="item === null" role="separator" :class="$style.divider"></div>
+ <span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
<span>{{ item.text }}</span>
</span>
- <span v-else-if="item.type === 'pending'" :tabindex="i" :class="[$style.pending, $style.item]">
+ <span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
<span><MkEllipsis/></span>
</span>
- <MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+ <MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<span>{{ item.text }}</span>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</MkA>
- <a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+ <a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<span>{{ item.text }}</span>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</a>
- <button v-else-if="item.type === 'user'" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+ <button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</button>
- <span v-else-if="item.type === 'switch'" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+ <span v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
</span>
- <button v-else-if="item.type === 'parent'" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
+ <button v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<span>{{ item.text }}</span>
<span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span>
</button>
- <button v-else :tabindex="i" class="_button" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+ <button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<span>{{ item.text }}</span>
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index b60967de02..efae687e66 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -83,7 +83,7 @@
</template>
<script lang="ts" setup>
-import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
+import { ref, shallowRef } from 'vue';
import * as misskey from 'misskey-js';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
@@ -94,7 +94,6 @@ import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user';
import { i18n } from '@/i18n';
import * as os from '@/os';
-import { stream } from '@/stream';
import { useTooltip } from '@/scripts/use-tooltip';
import { $i } from '@/account';
@@ -110,35 +109,6 @@ const props = withDefaults(defineProps<{
const elRef = shallowRef<HTMLElement>(null);
const reactionRef = ref(null);
-let readObserver: IntersectionObserver | undefined;
-let connection;
-
-onMounted(() => {
- if (!props.notification.isRead) {
- readObserver = new IntersectionObserver((entries, observer) => {
- if (!entries.some(entry => entry.isIntersecting)) return;
- stream.send('readNotification', {
- id: props.notification.id,
- });
- observer.disconnect();
- });
-
- readObserver.observe(elRef.value);
-
- connection = stream.useChannel('main');
- connection.on('readAllNotifications', () => readObserver.disconnect());
-
- watch(props.notification.isRead, () => {
- readObserver.disconnect();
- });
- }
-});
-
-onUnmounted(() => {
- if (readObserver) readObserver.disconnect();
- if (connection) connection.dispose();
-});
-
const followRequestDone = ref(false);
const acceptFollowRequest = () => {
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 874f1f90ea..1aea95fe0e 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -29,7 +29,6 @@ import { notificationTypes } from '@/const';
const props = defineProps<{
includeTypes?: typeof notificationTypes[number][];
- unreadOnly?: boolean;
}>();
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
@@ -40,23 +39,17 @@ const pagination: Paging = {
params: computed(() => ({
includeTypes: props.includeTypes ?? undefined,
excludeTypes: props.includeTypes ? undefined : $i.mutingNotificationTypes,
- unreadOnly: props.unreadOnly,
})),
};
const onNotification = (notification) => {
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') {
- stream.send('readNotification', {
- id: notification.id,
- });
+ stream.send('readNotification');
}
if (!isMuted) {
- pagingComponent.value.prepend({
- ...notification,
- isRead: document.visibilityState === 'visible',
- });
+ pagingComponent.value.prepend(notification);
}
};
@@ -65,30 +58,6 @@ let connection;
onMounted(() => {
connection = stream.useChannel('main');
connection.on('notification', onNotification);
- connection.on('readAllNotifications', () => {
- if (pagingComponent.value) {
- for (const item of pagingComponent.value.queue) {
- item.isRead = true;
- }
- for (const item of pagingComponent.value.items) {
- item.isRead = true;
- }
- }
- });
- connection.on('readNotifications', notificationIds => {
- if (pagingComponent.value) {
- for (let i = 0; i < pagingComponent.value.queue.length; i++) {
- if (notificationIds.includes(pagingComponent.value.queue[i].id)) {
- pagingComponent.value.queue[i].isRead = true;
- }
- }
- for (let i = 0; i < (pagingComponent.value.items || []).length; i++) {
- if (notificationIds.includes(pagingComponent.value.items[i].id)) {
- pagingComponent.value.items[i].isRead = true;
- }
- }
- }
- });
});
onUnmounted(() => {
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index 9232ebb7c9..0f148022bf 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -12,7 +12,7 @@ import { onMounted } from 'vue';
import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{
- maxHeight: number;
+ maxHeight?: number;
}>(), {
maxHeight: 200,
});
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 635ac3e8bd..9c5622b1c5 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -150,7 +150,7 @@ function adjustTweetHeight(message: any) {
}
const openPlayer = (): void => {
- os.popup(defineAsyncComponent(() => import('@/components/MkYoutubePlayer.vue')), {
+ os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
url: requestUrl.href,
});
};
diff --git a/packages/frontend/src/components/MkYoutubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index 4d765fe2f7..4d765fe2f7 100644
--- a/packages/frontend/src/components/MkYoutubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
diff --git a/packages/frontend/src/components/global/MkA.stories.impl.ts b/packages/frontend/src/components/global/MkA.stories.impl.ts
new file mode 100644
index 0000000000..72d069e853
--- /dev/null
+++ b/packages/frontend/src/components/global/MkA.stories.impl.ts
@@ -0,0 +1,47 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { expect } from '@storybook/jest';
+import { userEvent, within } from '@storybook/testing-library';
+import { StoryObj } from '@storybook/vue3';
+import MkA from './MkA.vue';
+import { tick } from '@/scripts/test-utils';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkA,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkA v-bind="props">Text</MkA>',
+ };
+ },
+ async play({ canvasElement }) {
+ const canvas = within(canvasElement);
+ const a = canvas.getByRole<HTMLAnchorElement>('link');
+ await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
+ await userEvent.click(a, { button: 2 });
+ await tick();
+ const menu = canvas.getByRole('menu');
+ await expect(menu).toBeInTheDocument();
+ await userEvent.click(a, { button: 0 });
+ a.blur();
+ await tick();
+ await expect(menu).not.toBeInTheDocument();
+ },
+ args: {
+ to: '#test',
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkA>;
diff --git a/packages/frontend/src/components/global/MkAcct.stories.impl.ts b/packages/frontend/src/components/global/MkAcct.stories.impl.ts
new file mode 100644
index 0000000000..7dfa1a14f2
--- /dev/null
+++ b/packages/frontend/src/components/global/MkAcct.stories.impl.ts
@@ -0,0 +1,43 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import { userDetailed } from '../../../.storybook/fakes';
+import MkAcct from './MkAcct.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkAcct,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkAcct v-bind="props" />',
+ };
+ },
+ args: {
+ user: {
+ ...userDetailed,
+ host: null,
+ },
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkAcct>;
+export const Detail = {
+ ...Default,
+ args: {
+ ...Default.args,
+ user: userDetailed,
+ detail: true,
+ },
+} satisfies StoryObj<typeof MkAcct>;
diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue
index e06ab64e86..2b9f892fc6 100644
--- a/packages/frontend/src/components/global/MkAcct.vue
+++ b/packages/frontend/src/components/global/MkAcct.vue
@@ -18,4 +18,3 @@ defineProps<{
const host = toUnicode(hostRaw);
</script>
-
diff --git a/packages/frontend/src/components/global/MkAd.stories.impl.ts b/packages/frontend/src/components/global/MkAd.stories.impl.ts
new file mode 100644
index 0000000000..7d8a42a03c
--- /dev/null
+++ b/packages/frontend/src/components/global/MkAd.stories.impl.ts
@@ -0,0 +1,120 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { expect } from '@storybook/jest';
+import { userEvent, within } from '@storybook/testing-library';
+import { StoryObj } from '@storybook/vue3';
+import { i18n } from '@/i18n';
+import MkAd from './MkAd.vue';
+const common = {
+ render(args) {
+ return {
+ components: {
+ MkAd,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkAd v-bind="props" />',
+ };
+ },
+ async play({ canvasElement, args }) {
+ const canvas = within(canvasElement);
+ const a = canvas.getByRole<HTMLAnchorElement>('link');
+ await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
+ const img = within(a).getByRole('img');
+ await expect(img).toBeInTheDocument();
+ let buttons = canvas.getAllByRole<HTMLButtonElement>('button');
+ await expect(buttons).toHaveLength(1);
+ const i = buttons[0];
+ await expect(i).toBeInTheDocument();
+ await userEvent.click(i);
+ await expect(a).not.toBeInTheDocument();
+ await expect(i).not.toBeInTheDocument();
+ buttons = canvas.getAllByRole<HTMLButtonElement>('button');
+ await expect(buttons).toHaveLength(args.__hasReduce ? 2 : 1);
+ const reduce = args.__hasReduce ? buttons[0] : null;
+ const back = buttons[args.__hasReduce ? 1 : 0];
+ if (reduce) {
+ await expect(reduce).toBeInTheDocument();
+ await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd);
+ }
+ await expect(back).toBeInTheDocument();
+ await expect(back).toHaveTextContent(i18n.ts._ad.back);
+ await userEvent.click(back);
+ if (reduce) {
+ await expect(reduce).not.toBeInTheDocument();
+ }
+ await expect(back).not.toBeInTheDocument();
+ const aAgain = canvas.getByRole<HTMLAnchorElement>('link');
+ await expect(aAgain).toBeInTheDocument();
+ const imgAgain = within(aAgain).getByRole('img');
+ await expect(imgAgain).toBeInTheDocument();
+ },
+ args: {
+ prefer: [],
+ specify: {
+ id: 'someadid',
+ radio: 1,
+ url: '#test',
+ },
+ __hasReduce: true,
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkAd>;
+export const Square = {
+ ...common,
+ args: {
+ ...common.args,
+ specify: {
+ ...common.args.specify,
+ place: 'square',
+ imageUrl:
+ 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
+ },
+ },
+} satisfies StoryObj<typeof MkAd>;
+export const Horizontal = {
+ ...common,
+ args: {
+ ...common.args,
+ specify: {
+ ...common.args.specify,
+ place: 'horizontal',
+ imageUrl:
+ 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
+ },
+ },
+} satisfies StoryObj<typeof MkAd>;
+export const HorizontalBig = {
+ ...common,
+ args: {
+ ...common.args,
+ specify: {
+ ...common.args.specify,
+ place: 'horizontal-big',
+ imageUrl:
+ 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
+ },
+ },
+} satisfies StoryObj<typeof MkAd>;
+export const ZeroRatio = {
+ ...Square,
+ args: {
+ ...Square.args,
+ specify: {
+ ...Square.args.specify,
+ ratio: 0,
+ },
+ __hasReduce: false,
+ },
+} satisfies StoryObj<typeof MkAd>;
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index b8f749bd1c..5799f99d5f 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -20,13 +20,13 @@
<script lang="ts" setup>
import { ref } from 'vue';
+import { i18n } from '@/i18n';
import { instance } from '@/instance';
import { host } from '@/config';
import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store';
import * as os from '@/os';
import { $i } from '@/account';
-import { i18n } from '@/i18n';
type Ad = (typeof instance)['ads'][number];
diff --git a/packages/frontend/src/components/global/MkAvatar.stories.impl.ts b/packages/frontend/src/components/global/MkAvatar.stories.impl.ts
new file mode 100644
index 0000000000..6c46f75b5f
--- /dev/null
+++ b/packages/frontend/src/components/global/MkAvatar.stories.impl.ts
@@ -0,0 +1,66 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import { userDetailed } from '../../../.storybook/fakes';
+import MkAvatar from './MkAvatar.vue';
+const common = {
+ render(args) {
+ return {
+ components: {
+ MkAvatar,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkAvatar v-bind="props" />',
+ };
+ },
+ args: {
+ user: userDetailed,
+ },
+ decorators: [
+ (Story, context) => ({
+ // eslint-disable-next-line quotes
+ template: `<div :style="{ display: 'grid', width: '${context.args.size}px', height: '${context.args.size}px' }"><story/></div>`,
+ }),
+ ],
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkAvatar>;
+export const ProfilePage = {
+ ...common,
+ args: {
+ ...common.args,
+ size: 120,
+ indicator: true,
+ },
+} satisfies StoryObj<typeof MkAvatar>;
+export const ProfilePageCat = {
+ ...ProfilePage,
+ args: {
+ ...ProfilePage.args,
+ user: {
+ ...userDetailed,
+ isCat: true,
+ },
+ },
+ parameters: {
+ ...ProfilePage.parameters,
+ chromatic: {
+ /* Your story couldn’t be captured because it exceeds our 25,000,000px limit. Its dimensions are 5,504,893x5,504,892px. Possible ways to resolve:
+ * * Separate pages into components
+ * * Minimize the number of very large elements in a story
+ */
+ disableSnapshot: true,
+ },
+ },
+} satisfies StoryObj<typeof MkAvatar>;
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 9a21941c8d..0cc30a887f 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -148,6 +148,7 @@ watch(() => props.user.avatarBlurhash, () => {
width: 100%;
height: 100%;
padding: 50%;
+ pointer-events: none;
&.mask {
-webkit-mask:
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts b/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts
new file mode 100644
index 0000000000..36ab85b579
--- /dev/null
+++ b/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts
@@ -0,0 +1,45 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import MkCustomEmoji from './MkCustomEmoji.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkCustomEmoji,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkCustomEmoji v-bind="props" />',
+ };
+ },
+ args: {
+ name: 'mi',
+ url: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkCustomEmoji>;
+export const Normal = {
+ ...Default,
+ args: {
+ ...Default.args,
+ normal: true,
+ },
+} satisfies StoryObj<typeof MkCustomEmoji>;
+export const Missing = {
+ ...Default,
+ args: {
+ name: Default.args.name,
+ },
+} satisfies StoryObj<typeof MkCustomEmoji>;
diff --git a/packages/frontend/src/components/global/MkEllipsis.stories.impl.ts b/packages/frontend/src/components/global/MkEllipsis.stories.impl.ts
new file mode 100644
index 0000000000..65405a9bc8
--- /dev/null
+++ b/packages/frontend/src/components/global/MkEllipsis.stories.impl.ts
@@ -0,0 +1,32 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import isChromatic from 'chromatic/isChromatic';
+import MkEllipsis from './MkEllipsis.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkEllipsis,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkEllipsis v-bind="props" />',
+ };
+ },
+ args: {
+ static: isChromatic(),
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkEllipsis>;
diff --git a/packages/frontend/src/components/global/MkEllipsis.vue b/packages/frontend/src/components/global/MkEllipsis.vue
index b3cf69c075..c8f6cd3394 100644
--- a/packages/frontend/src/components/global/MkEllipsis.vue
+++ b/packages/frontend/src/components/global/MkEllipsis.vue
@@ -1,9 +1,19 @@
<template>
-<span :class="$style.root">
+<span :class="[$style.root, { [$style.static]: static }]">
<span :class="$style.dot">.</span><span :class="$style.dot">.</span><span :class="$style.dot">.</span>
</span>
</template>
+<script lang="ts" setup>
+import { } from 'vue';
+
+const props = withDefaults(defineProps<{
+ static?: boolean;
+}>(), {
+ static: false,
+});
+</script>
+
<style lang="scss" module>
@keyframes ellipsis {
0%, 80%, 100% {
@@ -15,7 +25,9 @@
}
.root {
-
+ &.static > .dot {
+ animation-play-state: paused;
+ }
}
.dot {
diff --git a/packages/frontend/src/components/global/MkEmoji.stories.impl.ts b/packages/frontend/src/components/global/MkEmoji.stories.impl.ts
new file mode 100644
index 0000000000..f9900375f7
--- /dev/null
+++ b/packages/frontend/src/components/global/MkEmoji.stories.impl.ts
@@ -0,0 +1,31 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import MkEmoji from './MkEmoji.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkEmoji,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkEmoji v-bind="props" />',
+ };
+ },
+ args: {
+ emoji: '❤',
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkEmoji>;
diff --git a/packages/frontend/src/components/global/MkError.stories.meta.ts b/packages/frontend/src/components/global/MkError.stories.meta.ts
new file mode 100644
index 0000000000..51d763ada7
--- /dev/null
+++ b/packages/frontend/src/components/global/MkError.stories.meta.ts
@@ -0,0 +1,5 @@
+export const argTypes = {
+ retry: {
+ action: 'retry',
+ },
+};
diff --git a/packages/frontend/src/components/global/MkLoading.stories.impl.ts b/packages/frontend/src/components/global/MkLoading.stories.impl.ts
new file mode 100644
index 0000000000..9dcc0cdea1
--- /dev/null
+++ b/packages/frontend/src/components/global/MkLoading.stories.impl.ts
@@ -0,0 +1,60 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import isChromatic from 'chromatic/isChromatic';
+import MkLoading from './MkLoading.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkLoading,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkLoading v-bind="props" />',
+ };
+ },
+ args: {
+ static: isChromatic(),
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkLoading>;
+export const Inline = {
+ ...Default,
+ args: {
+ ...Default.args,
+ inline: true,
+ },
+} satisfies StoryObj<typeof MkLoading>;
+export const Colored = {
+ ...Default,
+ args: {
+ ...Default.args,
+ colored: true,
+ },
+} satisfies StoryObj<typeof MkLoading>;
+export const Mini = {
+ ...Default,
+ args: {
+ ...Default.args,
+ mini: true,
+ },
+} satisfies StoryObj<typeof MkLoading>;
+export const Em = {
+ ...Default,
+ args: {
+ ...Default.args,
+ em: true,
+ },
+} satisfies StoryObj<typeof MkLoading>;
diff --git a/packages/frontend/src/components/global/MkLoading.vue b/packages/frontend/src/components/global/MkLoading.vue
index 64e12e3b44..4311f9fe8a 100644
--- a/packages/frontend/src/components/global/MkLoading.vue
+++ b/packages/frontend/src/components/global/MkLoading.vue
@@ -6,7 +6,7 @@
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
</g>
</svg>
- <svg :class="[$style.spinner, $style.fg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
+ <svg :class="[$style.spinner, $style.fg, { [$style.static]: static }]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.125,0,0,1.125,12,12)">
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
</g>
@@ -19,11 +19,13 @@
import { } from 'vue';
const props = withDefaults(defineProps<{
+ static?: boolean;
inline?: boolean;
colored?: boolean;
mini?: boolean;
em?: boolean;
}>(), {
+ static: false,
inline: false,
colored: true,
mini: false,
@@ -97,5 +99,9 @@ const props = withDefaults(defineProps<{
.fg {
animation: spinner 0.5s linear infinite;
+
+ &.static {
+ animation-play-state: paused;
+ }
}
</style>
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
new file mode 100644
index 0000000000..f6811b6747
--- /dev/null
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
@@ -0,0 +1,74 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.vue';
+import { within } from '@storybook/testing-library';
+import { expect } from '@storybook/jest';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkMisskeyFlavoredMarkdown,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkMisskeyFlavoredMarkdown v-bind="props" />',
+ };
+ },
+ async play({ canvasElement, args }) {
+ const canvas = within(canvasElement);
+ if (args.plain) {
+ const aiHelloMiskist = canvas.getByText('@ai *Hello*, #Miskist!');
+ await expect(aiHelloMiskist).toBeInTheDocument();
+ } else {
+ const ai = canvas.getByText('@ai');
+ await expect(ai).toBeInTheDocument();
+ await expect(ai.closest('a')).toHaveAttribute('href', '/@ai');
+ const hello = canvas.getByText('Hello');
+ await expect(hello).toBeInTheDocument();
+ await expect(hello.style.fontStyle).toBe('oblique');
+ const miskist = canvas.getByText('#Miskist');
+ await expect(miskist).toBeInTheDocument();
+ await expect(miskist).toHaveAttribute('href', args.isNote ?? true ? '/tags/Miskist' : '/user-tags/Miskist');
+ }
+ const heart = canvas.getByAltText('❤');
+ await expect(heart).toBeInTheDocument();
+ await expect(heart).toHaveAttribute('src', '/twemoji/2764.svg');
+ },
+ args: {
+ text: '@ai *Hello*, #Miskist! ❤',
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
+export const Plain = {
+ ...Default,
+ args: {
+ ...Default.args,
+ plain: true,
+ },
+} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
+export const Nowrap = {
+ ...Default,
+ args: {
+ ...Default.args,
+ nowrap: true,
+ },
+} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
+export const IsNotNote = {
+ ...Default,
+ args: {
+ ...Default.args,
+ isNote: false,
+ },
+} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
diff --git a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
new file mode 100644
index 0000000000..5519d60fc4
--- /dev/null
+++ b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
@@ -0,0 +1,98 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import MkPageHeader from './MkPageHeader.vue';
+export const Empty = {
+ render(args) {
+ return {
+ components: {
+ MkPageHeader,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkPageHeader v-bind="props" />',
+ };
+ },
+ args: {
+ static: true,
+ tabs: [],
+ },
+ parameters: {
+ layout: 'centered',
+ chromatic: {
+ /* This component has animations that are implemented with JavaScript. So it's unstable to take a snapshot. */
+ disableSnapshot: true,
+ },
+ },
+} satisfies StoryObj<typeof MkPageHeader>;
+export const OneTab = {
+ ...Empty,
+ args: {
+ ...Empty.args,
+ tab: 'sometabkey',
+ tabs: [
+ {
+ key: 'sometabkey',
+ title: 'Some Tab Title',
+ },
+ ],
+ },
+} satisfies StoryObj<typeof MkPageHeader>;
+export const Icon = {
+ ...OneTab,
+ args: {
+ ...OneTab.args,
+ tabs: [
+ {
+ ...OneTab.args.tabs[0],
+ icon: 'ti ti-home',
+ },
+ ],
+ },
+} satisfies StoryObj<typeof MkPageHeader>;
+export const IconOnly = {
+ ...Icon,
+ args: {
+ ...Icon.args,
+ tabs: [
+ {
+ ...Icon.args.tabs[0],
+ title: undefined,
+ iconOnly: true,
+ },
+ ],
+ },
+} satisfies StoryObj<typeof MkPageHeader>;
+export const SomeTabs = {
+ ...Empty,
+ args: {
+ ...Empty.args,
+ tab: 'princess',
+ tabs: [
+ {
+ key: 'princess',
+ title: 'Princess',
+ icon: 'ti ti-crown',
+ },
+ {
+ key: 'fairy',
+ title: 'Fairy',
+ icon: 'ti ti-snowflake',
+ },
+ {
+ key: 'angel',
+ title: 'Angel',
+ icon: 'ti ti-feather',
+ },
+ ],
+ },
+} satisfies StoryObj<typeof MkPageHeader>;
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts
new file mode 100644
index 0000000000..6d4460d593
--- /dev/null
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts
@@ -0,0 +1,3 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import MkPageHeader_tabs from './MkPageHeader.tabs.vue';
+void MkPageHeader_tabs;
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index 42760da08f..9e1da64e61 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -33,14 +33,18 @@
<script lang="ts">
export type Tab = {
key: string;
- title: string;
- icon?: string;
- iconOnly?: boolean;
onClick?: (ev: MouseEvent) => void;
-} & {
- iconOnly: true;
- iccn: string;
-};
+} & (
+ | {
+ iconOnly?: false;
+ title: string;
+ icon?: string;
+ }
+ | {
+ iconOnly: true;
+ icon: string;
+ }
+);
</script>
<script lang="ts" setup>
diff --git a/packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts b/packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts
new file mode 100644
index 0000000000..97b8cc0c5b
--- /dev/null
+++ b/packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts
@@ -0,0 +1,3 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import MkStickyContainer from './MkStickyContainer.vue';
+void MkStickyContainer;
diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts
new file mode 100644
index 0000000000..b72601b1ff
--- /dev/null
+++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts
@@ -0,0 +1,312 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { expect } from '@storybook/jest';
+import { StoryObj } from '@storybook/vue3';
+import MkTime from './MkTime.vue';
+import { i18n } from '@/i18n';
+import { dateTimeFormat } from '@/scripts/intl-const';
+const now = new Date('2023-04-01T00:00:00.000Z');
+const future = new Date(8640000000000000);
+const oneHourAgo = new Date(now.getTime() - 3600000);
+const oneDayAgo = new Date(now.getTime() - 86400000);
+const oneWeekAgo = new Date(now.getTime() - 604800000);
+const oneMonthAgo = new Date(now.getTime() - 2592000000);
+const oneYearAgo = new Date(now.getTime() - 31536000000);
+export const Empty = {
+ render(args) {
+ return {
+ components: {
+ MkTime,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkTime v-bind="props" />',
+ };
+ },
+ async play({ canvasElement }) {
+ await expect(canvasElement).toHaveTextContent(i18n.ts._ago.invalid);
+ },
+ args: {
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const RelativeFuture = {
+ ...Empty,
+ async play({ canvasElement }) {
+ await expect(canvasElement).toHaveTextContent(i18n.ts._ago.future);
+ },
+ args: {
+ ...Empty.args,
+ time: future,
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const AbsoluteFuture = {
+ ...Empty,
+ async play({ canvasElement, args }) {
+ await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
+ },
+ args: {
+ ...Empty.args,
+ time: future,
+ mode: 'absolute',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const DetailFuture = {
+ ...Empty,
+ async play(context) {
+ await AbsoluteFuture.play(context);
+ await expect(context.canvasElement).toHaveTextContent(' (');
+ await RelativeFuture.play(context);
+ await expect(context.canvasElement).toHaveTextContent(')');
+ },
+ args: {
+ ...Empty.args,
+ time: future,
+ mode: 'detail',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const RelativeNow = {
+ ...Empty,
+ async play({ canvasElement }) {
+ await expect(canvasElement).toHaveTextContent(i18n.ts._ago.justNow);
+ },
+ args: {
+ ...Empty.args,
+ time: now,
+ origin: now,
+ mode: 'relative',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const AbsoluteNow = {
+ ...Empty,
+ async play({ canvasElement, args }) {
+ await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
+ },
+ args: {
+ ...Empty.args,
+ time: now,
+ origin: now,
+ mode: 'absolute',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const DetailNow = {
+ ...Empty,
+ async play(context) {
+ await AbsoluteNow.play(context);
+ await expect(context.canvasElement).toHaveTextContent(' (');
+ await RelativeNow.play(context);
+ await expect(context.canvasElement).toHaveTextContent(')');
+ },
+ args: {
+ ...Empty.args,
+ time: now,
+ origin: now,
+ mode: 'detail',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const RelativeOneHourAgo = {
+ ...Empty,
+ async play({ canvasElement }) {
+ await expect(canvasElement).toHaveTextContent(i18n.t('_ago.hoursAgo', { n: 1 }));
+ },
+ args: {
+ ...Empty.args,
+ time: oneHourAgo,
+ origin: now,
+ mode: 'relative',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const AbsoluteOneHourAgo = {
+ ...Empty,
+ async play({ canvasElement, args }) {
+ await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
+ },
+ args: {
+ ...Empty.args,
+ time: oneHourAgo,
+ origin: now,
+ mode: 'absolute',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const DetailOneHourAgo = {
+ ...Empty,
+ async play(context) {
+ await AbsoluteOneHourAgo.play(context);
+ await expect(context.canvasElement).toHaveTextContent(' (');
+ await RelativeOneHourAgo.play(context);
+ await expect(context.canvasElement).toHaveTextContent(')');
+ },
+ args: {
+ ...Empty.args,
+ time: oneHourAgo,
+ origin: now,
+ mode: 'detail',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const RelativeOneDayAgo = {
+ ...Empty,
+ async play({ canvasElement }) {
+ await expect(canvasElement).toHaveTextContent(i18n.t('_ago.daysAgo', { n: 1 }));
+ },
+ args: {
+ ...Empty.args,
+ time: oneDayAgo,
+ origin: now,
+ mode: 'relative',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const AbsoluteOneDayAgo = {
+ ...Empty,
+ async play({ canvasElement, args }) {
+ await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
+ },
+ args: {
+ ...Empty.args,
+ time: oneDayAgo,
+ origin: now,
+ mode: 'absolute',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const DetailOneDayAgo = {
+ ...Empty,
+ async play(context) {
+ await AbsoluteOneDayAgo.play(context);
+ await expect(context.canvasElement).toHaveTextContent(' (');
+ await RelativeOneDayAgo.play(context);
+ await expect(context.canvasElement).toHaveTextContent(')');
+ },
+ args: {
+ ...Empty.args,
+ time: oneDayAgo,
+ origin: now,
+ mode: 'detail',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const RelativeOneWeekAgo = {
+ ...Empty,
+ async play({ canvasElement }) {
+ await expect(canvasElement).toHaveTextContent(i18n.t('_ago.weeksAgo', { n: 1 }));
+ },
+ args: {
+ ...Empty.args,
+ time: oneWeekAgo,
+ origin: now,
+ mode: 'relative',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const AbsoluteOneWeekAgo = {
+ ...Empty,
+ async play({ canvasElement, args }) {
+ await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
+ },
+ args: {
+ ...Empty.args,
+ time: oneWeekAgo,
+ origin: now,
+ mode: 'absolute',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const DetailOneWeekAgo = {
+ ...Empty,
+ async play(context) {
+ await AbsoluteOneWeekAgo.play(context);
+ await expect(context.canvasElement).toHaveTextContent(' (');
+ await RelativeOneWeekAgo.play(context);
+ await expect(context.canvasElement).toHaveTextContent(')');
+ },
+ args: {
+ ...Empty.args,
+ time: oneWeekAgo,
+ origin: now,
+ mode: 'detail',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const RelativeOneMonthAgo = {
+ ...Empty,
+ async play({ canvasElement }) {
+ await expect(canvasElement).toHaveTextContent(i18n.t('_ago.monthsAgo', { n: 1 }));
+ },
+ args: {
+ ...Empty.args,
+ time: oneMonthAgo,
+ origin: now,
+ mode: 'relative',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const AbsoluteOneMonthAgo = {
+ ...Empty,
+ async play({ canvasElement, args }) {
+ await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
+ },
+ args: {
+ ...Empty.args,
+ time: oneMonthAgo,
+ origin: now,
+ mode: 'absolute',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const DetailOneMonthAgo = {
+ ...Empty,
+ async play(context) {
+ await AbsoluteOneMonthAgo.play(context);
+ await expect(context.canvasElement).toHaveTextContent(' (');
+ await RelativeOneMonthAgo.play(context);
+ await expect(context.canvasElement).toHaveTextContent(')');
+ },
+ args: {
+ ...Empty.args,
+ time: oneMonthAgo,
+ origin: now,
+ mode: 'detail',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const RelativeOneYearAgo = {
+ ...Empty,
+ async play({ canvasElement }) {
+ await expect(canvasElement).toHaveTextContent(i18n.t('_ago.yearsAgo', { n: 1 }));
+ },
+ args: {
+ ...Empty.args,
+ time: oneYearAgo,
+ origin: now,
+ mode: 'relative',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const AbsoluteOneYearAgo = {
+ ...Empty,
+ async play({ canvasElement, args }) {
+ await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time));
+ },
+ args: {
+ ...Empty.args,
+ time: oneYearAgo,
+ origin: now,
+ mode: 'absolute',
+ },
+} satisfies StoryObj<typeof MkTime>;
+export const DetailOneYearAgo = {
+ ...Empty,
+ async play(context) {
+ await AbsoluteOneYearAgo.play(context);
+ await expect(context.canvasElement).toHaveTextContent(' (');
+ await RelativeOneYearAgo.play(context);
+ await expect(context.canvasElement).toHaveTextContent(')');
+ },
+ args: {
+ ...Empty.args,
+ time: oneYearAgo,
+ origin: now,
+ mode: 'detail',
+ },
+} satisfies StoryObj<typeof MkTime>;
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index 3fa8bb9adc..99169512db 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -14,8 +14,10 @@ import { dateTimeFormat } from '@/scripts/intl-const';
const props = withDefaults(defineProps<{
time: Date | string | number | null;
+ origin?: Date | null;
mode?: 'relative' | 'absolute' | 'detail';
}>(), {
+ origin: null,
mode: 'relative',
});
@@ -25,7 +27,7 @@ const _time = props.time == null ? NaN :
const invalid = Number.isNaN(_time);
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
-let now = $ref((new Date()).getTime());
+let now = $ref((props.origin ?? new Date()).getTime());
const relative = $computed<string>(() => {
if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない
if (invalid) return i18n.ts._ago.invalid;
@@ -46,7 +48,7 @@ const relative = $computed<string>(() => {
let tickId: number;
function tick() {
- now = (new Date()).getTime();
+ now = props.origin ?? (new Date()).getTime();
const ago = (now - _time) / 1000/*ms*/;
const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
diff --git a/packages/frontend/src/components/global/MkUrl.stories.impl.ts b/packages/frontend/src/components/global/MkUrl.stories.impl.ts
new file mode 100644
index 0000000000..2344c4851a
--- /dev/null
+++ b/packages/frontend/src/components/global/MkUrl.stories.impl.ts
@@ -0,0 +1,77 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { expect } from '@storybook/jest';
+import { userEvent, within } from '@storybook/testing-library';
+import { StoryObj } from '@storybook/vue3';
+import { rest } from 'msw';
+import { commonHandlers } from '../../../.storybook/mocks';
+import MkUrl from './MkUrl.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkUrl,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkUrl v-bind="props">Text</MkUrl>',
+ };
+ },
+ async play({ canvasElement }) {
+ const canvas = within(canvasElement);
+ const a = canvas.getByRole<HTMLAnchorElement>('link');
+ await expect(a).toHaveAttribute('href', 'https://misskey-hub.net/');
+ await userEvent.hover(a);
+ /*
+ await tick(); // FIXME: wait for network request
+ const anchors = canvas.getAllByRole<HTMLAnchorElement>('link');
+ const popup = anchors.find(anchor => anchor !== a)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
+ await expect(popup).toBeInTheDocument();
+ await expect(popup).toHaveAttribute('href', 'https://misskey-hub.net/');
+ await expect(popup).toHaveTextContent('Misskey Hub');
+ await expect(popup).toHaveTextContent('Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。');
+ await expect(popup).toHaveTextContent('misskey-hub.net');
+ const icon = within(popup).getByRole('img');
+ await expect(icon).toBeInTheDocument();
+ await expect(icon).toHaveAttribute('src', 'https://misskey-hub.net/favicon.ico');
+ */
+ await userEvent.unhover(a);
+ },
+ args: {
+ url: 'https://misskey-hub.net/',
+ },
+ parameters: {
+ layout: 'centered',
+ msw: {
+ handlers: [
+ ...commonHandlers,
+ rest.get('/url', (req, res, ctx) => {
+ return res(ctx.json({
+ title: 'Misskey Hub',
+ icon: 'https://misskey-hub.net/favicon.ico',
+ description: 'Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。',
+ thumbnail: null,
+ player: {
+ url: null,
+ width: null,
+ height: null,
+ allow: [],
+ },
+ sitename: 'misskey-hub.net',
+ sensitive: false,
+ url: 'https://misskey-hub.net/',
+ }));
+ }),
+ ],
+ },
+ },
+} satisfies StoryObj<typeof MkUrl>;
diff --git a/packages/frontend/src/components/global/MkUserName.stories.impl.ts b/packages/frontend/src/components/global/MkUserName.stories.impl.ts
new file mode 100644
index 0000000000..41b1567a6f
--- /dev/null
+++ b/packages/frontend/src/components/global/MkUserName.stories.impl.ts
@@ -0,0 +1,57 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { expect } from '@storybook/jest';
+import { userEvent, within } from '@storybook/testing-library';
+import { StoryObj } from '@storybook/vue3';
+import { userDetailed } from '../../../.storybook/fakes';
+import MkUserName from './MkUserName.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkUserName,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkUserName v-bind="props"/>',
+ };
+ },
+ async play({ canvasElement }) {
+ await expect(canvasElement).toHaveTextContent(userDetailed.name);
+ },
+ args: {
+ user: userDetailed,
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkUserName>;
+export const Anonymous = {
+ ...Default,
+ async play({ canvasElement }) {
+ await expect(canvasElement).toHaveTextContent(userDetailed.username);
+ },
+ args: {
+ ...Default.args,
+ user: {
+ ...userDetailed,
+ name: null,
+ },
+ },
+} satisfies StoryObj<typeof MkUserName>;
+export const Wrap = {
+ ...Default,
+ args: {
+ ...Default.args,
+ nowrap: false,
+ },
+} satisfies StoryObj<typeof MkUserName>;
diff --git a/packages/frontend/src/components/global/RouterView.stories.impl.ts b/packages/frontend/src/components/global/RouterView.stories.impl.ts
new file mode 100644
index 0000000000..7910b8b3cb
--- /dev/null
+++ b/packages/frontend/src/components/global/RouterView.stories.impl.ts
@@ -0,0 +1,3 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import RouterView from './RouterView.vue';
+void RouterView;
diff --git a/packages/frontend/src/index.mdx b/packages/frontend/src/index.mdx
new file mode 100644
index 0000000000..e30dea2928
--- /dev/null
+++ b/packages/frontend/src/index.mdx
@@ -0,0 +1,12 @@
+import { Meta } from '@storybook/blocks'
+
+<Meta title="index" />
+
+# Welcome to Misskey Storybook
+
+This project uses [Storybook](https://storybook.js.org/) to develop and document components.
+You can find more information about the usage of Storybook in this project in the CONTRIBUTING.md file placed in the root of this repository.
+
+The Misskey Storybook is under development and not all components are documented yet.
+Contributions are welcome! Please refer to [#10336](https://github.com/misskey-dev/misskey/issues/10336) for more information.
+Thank you for your support!
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index a5c7cdaa71..1789606cd8 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -2,8 +2,8 @@
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
- <div v-if="tab === 'all' || tab === 'unread'">
- <XNotifications class="notifications" :include-types="includeTypes" :unread-only="unreadOnly"/>
+ <div v-if="tab === 'all'">
+ <XNotifications class="notifications" :include-types="includeTypes"/>
</div>
<div v-else-if="tab === 'mentions'">
<MkNotes :pagination="mentionsPagination"/>
@@ -26,7 +26,6 @@ import { notificationTypes } from '@/const';
let tab = $ref('all');
let includeTypes = $ref<string[] | null>(null);
-let unreadOnly = $computed(() => tab === 'unread');
const mentionsPagination = {
endpoint: 'notes/mentions' as const,
@@ -77,10 +76,6 @@ const headerTabs = $computed(() => [{
title: i18n.ts.all,
icon: 'ti ti-point',
}, {
- key: 'unread',
- title: i18n.ts.unread,
- icon: 'ti ti-loader',
-}, {
key: 'mentions',
title: i18n.ts.mentions,
icon: 'ti ti-at',
diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue
index 54360024f3..1c7c991aac 100644
--- a/packages/frontend/src/pages/user/activity.following.vue
+++ b/packages/frontend/src/pages/user/activity.following.vue
@@ -77,7 +77,10 @@ async function renderChart() {
barPercentage: 0.7,
categoryPercentage: 0.7,
fill: true,
- } satisfies ChartDataset, extra);
+ /* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
+ } satisfies ChartData, extra);
+ */
+ }, extra);
}
chartInstance = new Chart(chartEl, {
diff --git a/packages/frontend/src/pages/user/activity.heatmap.vue b/packages/frontend/src/pages/user/activity.heatmap.vue
index 2dcb754c9b..ada0166eda 100644
--- a/packages/frontend/src/pages/user/activity.heatmap.vue
+++ b/packages/frontend/src/pages/user/activity.heatmap.vue
@@ -113,6 +113,9 @@ async function renderChart() {
const a = c.chart.chartArea ?? {};
return (a.bottom - a.top) / 7 - marginEachCell;
},
+ /* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
+ }] satisfies ChartData[],
+ */
}],
},
options: {
diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue
index 7dd02ad6d4..8a946aebac 100644
--- a/packages/frontend/src/pages/user/activity.notes.vue
+++ b/packages/frontend/src/pages/user/activity.notes.vue
@@ -76,7 +76,10 @@ async function renderChart() {
borderRadius: 4,
barPercentage: 0.9,
fill: true,
- } satisfies ChartDataset, extra);
+ /* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
+ } satisfies ChartData, extra);
+ */
+ }, extra);
}
chartInstance = new Chart(chartEl, {
diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue
index 6a7506e388..0e9c581e1e 100644
--- a/packages/frontend/src/pages/user/activity.pv.vue
+++ b/packages/frontend/src/pages/user/activity.pv.vue
@@ -77,7 +77,10 @@ async function renderChart() {
barPercentage: 0.7,
categoryPercentage: 0.7,
fill: true,
- } satisfies ChartDataset, extra);
+ /* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
+ } satisfies ChartData, extra);
+ */
+ }, extra);
}
chartInstance = new Chart(chartEl, {
diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts
index c77f8e12d3..25e8b71a12 100644
--- a/packages/frontend/src/scripts/achievements.ts
+++ b/packages/frontend/src/scripts/achievements.ts
@@ -443,11 +443,14 @@ export const ACHIEVEMENT_BADGES = {
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'bronze',
},
+/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
img: string;
bg: string | null;
frame: 'bronze' | 'silver' | 'gold' | 'platinum';
}>;
+ */
+} as const;
export const claimedAchievements: typeof ACHIEVEMENT_TYPES[number][] = ($i && $i.achievements) ? $i.achievements.map(x => x.name) : [];
diff --git a/packages/frontend/src/scripts/test-utils.ts b/packages/frontend/src/scripts/test-utils.ts
new file mode 100644
index 0000000000..3e018f2d7e
--- /dev/null
+++ b/packages/frontend/src/scripts/test-utils.ts
@@ -0,0 +1,6 @@
+/// <reference types="@testing-library/jest-dom"/>
+
+export async function tick(): Promise<void> {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never);
+}
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index e1561cb396..5a32c076a4 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -53,9 +53,7 @@ function onNotification(notification) {
if ($i.mutingNotificationTypes.includes(notification.type)) return;
if (document.visibilityState === 'visible') {
- stream.send('readNotification', {
- id: notification.id,
- });
+ stream.send('readNotification');
notifications.unshift(notification);
window.setTimeout(() => {