diff options
| author | dakkar <dakkar@thenautilus.net> | 2024-12-15 17:27:12 +0000 |
|---|---|---|
| committer | dakkar <dakkar@thenautilus.net> | 2024-12-15 17:27:12 +0000 |
| commit | e2352839e4639b09e2e52b2ada1399097fad1d8d (patch) | |
| tree | 9268cda477b8c1dcfb2c78eaabcb173a1566a469 /packages/frontend/src/components | |
| parent | merge: Fix rate limits under multi-node environments (!809) (diff) | |
| parent | upstream merge checklist: remember to check federated profile fields (diff) | |
| download | sharkey-e2352839e4639b09e2e52b2ada1399097fad1d8d.tar.gz sharkey-e2352839e4639b09e2e52b2ada1399097fad1d8d.tar.bz2 sharkey-e2352839e4639b09e2e52b2ada1399097fad1d8d.zip | |
merge: upstream changes for 2024.11 (!742)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/742
Closes #645 and #646
Approved-by: Hazelnoot <acomputerdog@gmail.com>
Approved-by: Marie <github@yuugi.dev>
Diffstat (limited to 'packages/frontend/src/components')
176 files changed, 3482 insertions, 1854 deletions
diff --git a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts b/packages/frontend/src/components/MkAbuseReport.stories.impl.ts deleted file mode 100644 index cf09c96fd4..0000000000 --- a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; -import { abuseUserReport } from '../../.storybook/fakes.js'; -import { commonHandlers } from '../../.storybook/mocks.js'; -import MkAbuseReport from './MkAbuseReport.vue'; -export const Default = { - render(args) { - return { - components: { - MkAbuseReport, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - resolved: action('resolved'), - }; - }, - }, - template: '<MkAbuseReport v-bind="props" v-on="events" />', - }; - }, - args: { - report: abuseUserReport(), - }, - parameters: { - layout: 'fullscreen', - msw: { - handlers: [ - ...commonHandlers, - http.post('/api/admin/resolve-abuse-user-report', async ({ request }) => { - action('POST /api/admin/resolve-abuse-user-report')(await request.json()); - return HttpResponse.json({}); - }), - ], - }, - }, -} satisfies StoryObj<typeof MkAbuseReport>; diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index d13eedaade..d59c5b2c57 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -4,141 +4,151 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> - <div class="bcekxzvu _margin _panel"> - <div class="target"> - <MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'"> - <MkAvatar class="avatar" :user="report.targetUser" indicator/> - <div class="names"> - <MkUserName class="name" :user="report.targetUser"/> - <MkAcct class="acct" :user="report.targetUser" style="display: block;"/> - </div> - </MkA> - <div class="keyvalCtn"> - <MkKeyValue> - <template #key>{{ i18n.ts.registeredDate }}</template> - <template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template> - </MkKeyValue> - <MkKeyValue> - <template #key>{{ i18n.ts.reporter }}</template> - <template #value><MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></template> - </MkKeyValue> - <MkKeyValue> - <template #key>{{ i18n.ts.createdAt }}</template> - <template #value><MkTime :time="report.createdAt" mode="absolute"/> (<MkTime :time="report.createdAt" mode="relative"/>)</template> - </MkKeyValue> - </div> - <hr> +<MkFolder> + <template #icon> + <i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--MI_THEME-success)"></i> + <i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--MI_THEME-error)"></i> + <i v-else-if="report.resolved" class="ti ti-slash"></i> + <i v-else class="ti ti-exclamation-circle" style="color: var(--MI_THEME-warn)"></i> + </template> + <template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template> + <template #caption>{{ report.comment }}</template> + <template #suffix><MkTime :time="report.createdAt"/></template> + <template #footer> + <div class="_buttons"> + <template v-if="!report.resolved"> + <MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--MI_THEME-success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton> + <MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--MI_THEME-error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton> + <MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton> + </template> + <template v-if="report.targetUser.host != null"> + <MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton> + <div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div> + </template> + <button class="_button" style="margin-left: auto; width: 34px;" @click="showMenu"><i class="ti ti-dots"></i></button> </div> - <div class="detail"> - <div> + </template> + + <div class="_gaps_s"> + <MkFolder :withSpacer="false"> + <template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template> + <template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template> + <template #suffix>#{{ report.targetUserId.toUpperCase() }}</template> + + <div style="container-type: inline-size;"> + <RouterView :router="targetRouter"/> + </div> + </MkFolder> + + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-message-2"></i></template> + <template #label>{{ i18n.ts.details }}</template> + <div class="_gaps_s"> <Mfm :text="report.comment" :isBlock="true" :linkNavigationBehavior="'window'"/> </div> - <hr/> - <div v-if="report.assignee" class="assignee"> - {{ i18n.ts.moderator }}: - <MkA :to="`/admin/user/${report.assignee.id}`" class="_link" :behavior="'window'">@{{ report.assignee.username }}</MkA> + </MkFolder> + + <MkFolder :withSpacer="false"> + <template #icon><MkAvatar :user="report.reporter" style="width: 18px; height: 18px;"/></template> + <template #label>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></template> + <template #suffix>#{{ report.reporterId.toUpperCase() }}</template> + + <div style="container-type: inline-size;"> + <RouterView :router="reporterRouter"/> </div> - <div class="action"> - <MkSwitch v-model="forward" c:disabled="report.targetUser.host == null || report.resolved"> - {{ i18n.ts.forwardReport }} - <template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template> - </MkSwitch> - <MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton> + </MkFolder> + + <MkFolder :defaultOpen="false"> + <template #icon><i class="ti ti-message-2"></i></template> + <template #label>{{ i18n.ts.moderationNote }}</template> + <template #suffix>{{ moderationNote.length > 0 ? '...' : i18n.ts.none }}</template> + <div class="_gaps_s"> + <MkTextarea v-model="moderationNote" manualSave> + <template #caption>{{ i18n.ts.moderationNoteDescription }}</template> + </MkTextarea> </div> + </MkFolder> + + <div v-if="report.assignee"> + {{ i18n.ts.moderator }}: + <MkAcct :user="report.assignee"/> </div> </div> +</MkFolder> </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { provide, ref, watch } from 'vue'; +import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { dateString } from '@/filters/date.js'; +import MkFolder from '@/components/MkFolder.vue'; +import RouterView from '@/components/global/RouterView.vue'; +import { useRouterFactory } from '@/router/supplier'; +import MkTextarea from '@/components/MkTextarea.vue'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; const props = defineProps<{ - report: any; + report: Misskey.entities.AdminAbuseUserReportsResponse[number]; }>(); const emit = defineEmits<{ (ev: 'resolved', reportId: string): void; }>(); -const forward = ref(props.report.forwarded); +const routerFactory = useRouterFactory(); +const targetRouter = routerFactory(`/admin/user/${props.report.targetUserId}`); +targetRouter.init(); +const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`); +reporterRouter.init(); + +const moderationNote = ref(props.report.moderationNote ?? ''); -function resolve() { +watch(moderationNote, async () => { + os.apiWithDialog('admin/update-abuse-user-report', { + reportId: props.report.id, + moderationNote: moderationNote.value, + }).then(() => { + }); +}); + +function resolve(resolvedAs) { os.apiWithDialog('admin/resolve-abuse-user-report', { - forward: forward.value, reportId: props.report.id, + resolvedAs, }).then(() => { emit('resolved', props.report.id); }); } -</script> -<style lang="scss" scoped> -.bcekxzvu { - display: flex; - flex-direction: column; - transition: .1s; - - > .target { - box-sizing: border-box; - text-align: left; - padding: 24px 24px 0px 24px; - - > .info { - display: flex; - box-sizing: border-box; - align-items: center; - padding: 14px; - border-radius: var(--radius-sm); - --c: rgb(255 196 0 / 15%); - background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); - background-size: 16px 16px; - - > .avatar { - width: 42px; - height: 42px; - } - - > .names { - margin-left: 0.3em; - padding: 0 8px; - flex: 1; - - white-space: pre; - overflow: hidden; - - > .name { - font-weight: bold; - } - } - } - - > .keyvalCtn { - display: inline-flex; - gap: 15px; - margin-top: 15px; - } - } - - > .detail { - display: flex; - flex-direction: column; - padding: 0px 24px 24px 24px; +function forward() { + os.apiWithDialog('admin/forward-abuse-user-report', { + reportId: props.report.id, + }).then(() => { - .assignee { - margin-bottom: 15px; - } + }); +} - .action { - display: flex; - flex-direction: column; - gap: 15px; - } - } +function showMenu(ev: MouseEvent) { + os.popupMenu([{ + icon: 'ti ti-id', + text: 'Copy ID', + action: () => { + copyToClipboard(props.report.id); + }, + }, { + icon: 'ti ti-json', + text: 'Copy JSON', + action: () => { + copyToClipboard(JSON.stringify(props.report, null, '\t')); + }, + }], ev.currentTarget ?? ev.target); } +</script> + +<style lang="scss" module> </style> diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue index 796524fce9..0839955d9d 100644 --- a/packages/frontend/src/components/MkAccountMoved.vue +++ b/packages/frontend/src/components/MkAccountMoved.vue @@ -32,9 +32,9 @@ misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u); .root { padding: 16px; font-size: 90%; - background: var(--infoWarnBg); - color: var(--error); - border-radius: var(--radius); + background: var(--MI_THEME-infoWarnBg); + color: var(--MI_THEME-error); + border-radius: var(--MI-radius); } .link { diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index ab7bafc47a..087ad51fe3 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -121,10 +121,10 @@ onMounted(() => { .iconFrame { position: relative; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); padding: 6px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); box-sizing: border-box; pointer-events: none; user-select: none; @@ -191,7 +191,7 @@ onMounted(() => { position: relative; width: 100%; height: 100%; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); box-shadow: 0 1px 0px #ffffff88 inset; } diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue index 835efbd6cd..c8fa6246e0 100644 --- a/packages/frontend/src/components/MkAnalogClock.vue +++ b/packages/frontend/src/components/MkAnalogClock.vue @@ -193,12 +193,12 @@ tick(); function calcColors() { const computedStyle = getComputedStyle(document.documentElement); - const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark(); - const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(); + const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark(); + const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(); majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'; //minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'; - mHandColor.value = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString(); + mHandColor.value = tinycolor(computedStyle.getPropertyValue('--MI_THEME-fg')).toHexString(); hHandColor.value = accent; nowColor.value = accent; } diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue index c81fea175c..6c335d71d9 100644 --- a/packages/frontend/src/components/MkAnnouncementDialog.vue +++ b/packages/frontend/src/components/MkAnnouncementDialog.vue @@ -9,9 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.header"> <span :class="$style.icon"> <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> - <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i> </span> <span :class="$style.title">{{ announcement.title }}</span> </div> @@ -29,7 +29,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { $i, updateAccount } from '@/account.js'; +import { $i, updateAccountPartial } from '@/account.js'; const props = withDefaults(defineProps<{ announcement: Misskey.entities.Announcement; @@ -51,7 +51,7 @@ async function ok() { modal.value?.close(); misskeyApi('i/read-announcement', { announcementId: props.announcement.id }); - updateAccount({ + updateAccountPartial({ unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id), }); } @@ -83,8 +83,8 @@ onMounted(() => { min-width: 320px; max-width: 480px; box-sizing: border-box; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); } .header { diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue index cb7ee3d6ca..e622d57f1e 100644 --- a/packages/frontend/src/components/MkAntennaEditor.vue +++ b/packages/frontend/src/components/MkAntennaEditor.vue @@ -160,7 +160,7 @@ async function deleteAntenna() { function addUser() { os.selectUser({ includeSelf: true }).then(user => { users.value = users.value.trim(); - users.value += '\n@' + Misskey.acct.toString(user as any); + users.value += '\n@' + Misskey.acct.toString(user); users.value = users.value.trim(); }); } @@ -170,6 +170,6 @@ function addUser() { .actions { margin-top: 16px; padding: 24px 0; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index e2af4f034e..c28dbc7ffa 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -106,7 +106,7 @@ const containerStyle = computed(() => { const border = isBordered ? { borderWidth: c.borderWidth ?? '1px', - borderColor: c.borderColor ?? 'var(--divider)', + borderColor: c.borderColor ?? 'var(--MI_THEME-divider)', borderStyle: c.borderStyle ?? 'solid', } : undefined; @@ -165,7 +165,7 @@ function openPostForm() { } .postForm { - background: var(--bg); - border-radius: var(--radius-sm); + background: var(--MI_THEME-bg); + border-radius: var(--MI-radius-sm); } </style> diff --git a/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts b/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts new file mode 100644 index 0000000000..0adc44e204 --- /dev/null +++ b/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkAuthConfirm from './MkAuthConfirm.vue'; +void MkAuthConfirm; diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue new file mode 100644 index 0000000000..f78d2d38f0 --- /dev/null +++ b/packages/frontend/src/components/MkAuthConfirm.vue @@ -0,0 +1,450 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.wrapper"> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_enterActive" + :leaveActiveClass="$style.transition_leaveActive" + :enterFromClass="$style.transition_enterFrom" + :leaveToClass="$style.transition_leaveTo" + + :inert="_waiting" + > + <div v-if="phase === 'accountSelect'" key="accountSelect" :class="$style.root" class="_gaps"> + <div :class="$style.header" class="_gaps_s"> + <div :class="$style.iconFallback"> + <i class="ti ti-user"></i> + </div> + <div :class="$style.headerText">{{ i18n.ts.pleaseSelectAccount }}</div> + </div> + <div> + <div :class="$style.accountSelectorLabel">{{ i18n.ts.selectAccount }}</div> + <div :class="$style.accountSelectorList"> + <template v-for="[id, user] in users"> + <input :id="'account-' + id" v-model="selectedUser" type="radio" name="accountSelector" :value="id" :class="$style.accountSelectorRadio"/> + <label :for="'account-' + id" :class="$style.accountSelectorItem"> + <MkAvatar :user="user" :class="$style.accountSelectorAvatar"/> + <div :class="$style.accountSelectorBody"> + <MkUserName :user="user" :class="$style.accountSelectorName"/> + <MkAcct :user="user" :class="$style.accountSelectorAcct"/> + </div> + </label> + </template> + <button class="_button" :class="[$style.accountSelectorItem, $style.accountSelectorAddAccountRoot]" @click="clickAddAccount"> + <div :class="[$style.accountSelectorAvatar, $style.accountSelectorAddAccountAvatar]"> + <i class="ti ti-user-plus"></i> + </div> + <div :class="[$style.accountSelectorBody, $style.accountSelectorName]">{{ i18n.ts.addAccount }}</div> + </button> + </div> + </div> + <div class="_buttonsCenter"> + <MkButton rounded gradate :disabled="selectedUser === null" @click="clickChooseAccount">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + <div v-else-if="phase === 'consent'" key="consent" :class="$style.root" class="_gaps"> + <div :class="$style.header" class="_gaps_s"> + <img v-if="icon" :class="$style.icon" :src="getProxiedImageUrl(icon, 'preview')"/> + <div v-else :class="$style.iconFallback"> + <i class="ti ti-apps"></i> + </div> + <div :class="$style.headerText">{{ name ? i18n.tsx._auth.shareAccess({ name }) : i18n.ts._auth.shareAccessAsk }}</div> + </div> + <div v-if="permissions && permissions.length > 0" class="_gaps_s" :class="$style.permissionRoot"> + <div>{{ name ? i18n.tsx._auth.permission({ name }) : i18n.ts._auth.permissionAsk }}</div> + <div :class="$style.permissionListWrapper"> + <ul :class="$style.permissionList"> + <li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li> + </ul> + </div> + </div> + <slot name="consentAdditionalInfo"></slot> + <div> + <div :class="$style.accountSelectorLabel"> + {{ i18n.ts._auth.scopeUser }} <button class="_textButton" @click="clickBackToAccountSelect">{{ i18n.ts.switchAccount }}</button> + </div> + <div :class="$style.accountSelectorList"> + <div :class="[$style.accountSelectorItem, $style.static]"> + <MkAvatar :user="users.get(selectedUser!)!" :class="$style.accountSelectorAvatar"/> + <div :class="$style.accountSelectorBody"> + <MkUserName :user="users.get(selectedUser!)!" :class="$style.accountSelectorName"/> + <MkAcct :user="users.get(selectedUser!)!" :class="$style.accountSelectorAcct"/> + </div> + </div> + </div> + </div> + <div class="_buttonsCenter"> + <MkButton rounded @click="clickCancel">{{ i18n.ts.reject }}</MkButton> + <MkButton rounded gradate @click="clickAccept">{{ i18n.ts.accept }}</MkButton> + </div> + </div> + <div v-else-if="phase === 'success'" key="success" :class="$style.root" class="_gaps_s"> + <div :class="$style.header" class="_gaps_s"> + <div :class="$style.iconFallback"> + <i class="ti ti-check"></i> + </div> + <div :class="$style.headerText">{{ i18n.ts._auth.accepted }}</div> + <div :class="$style.headerTextSub">{{ i18n.ts._auth.pleaseGoBack }}</div> + </div> + </div> + <div v-else-if="phase === 'denied'" key="denied" :class="$style.root" class="_gaps_s"> + <div :class="$style.header" class="_gaps_s"> + <div :class="$style.iconFallback"> + <i class="ti ti-x"></i> + </div> + <div :class="$style.headerText">{{ i18n.ts._auth.denied }}</div> + </div> + </div> + <div v-else-if="phase === 'failed'" key="failed" :class="$style.root" class="_gaps_s"> + <div :class="$style.header" class="_gaps_s"> + <div :class="$style.iconFallback"> + <i class="ti ti-x"></i> + </div> + <div :class="$style.headerText">{{ i18n.ts.somethingHappened }}</div> + </div> + </div> + </Transition> + <div v-if="_waiting" :class="$style.waitingRoot"> + <MkLoading/> + </div> +</div> +</template> + +<script setup lang="ts"> +import { ref, computed } from 'vue'; +import * as Misskey from 'misskey-js'; + +import MkButton from '@/components/MkButton.vue'; + +import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js'; +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; +import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; + +const props = defineProps<{ + name?: string; + icon?: string; + permissions?: (typeof Misskey.permissions[number])[]; + manualWaiting?: boolean; + waitOnDeny?: boolean; +}>(); + +const emit = defineEmits<{ + (ev: 'accept', token: string): void; + (ev: 'deny', token: string): void; +}>(); + +const waiting = ref(true); +const _waiting = computed(() => waiting.value || props.manualWaiting); +const phase = ref<'accountSelect' | 'consent' | 'success' | 'denied' | 'failed'>('accountSelect'); + +const selectedUser = ref<string | null>(null); + +const users = ref(new Map<string, Misskey.entities.UserDetailed & { token: string; }>()); + +async function init() { + waiting.value = true; + + users.value.clear(); + + if ($i) { + users.value.set($i.id, $i); + } + + const accounts = await getAccounts(); + + const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id)); + + if (accountIdsToFetch.length > 0) { + const usersRes = await misskeyApi('users/show', { + userIds: accountIdsToFetch, + }); + + for (const user of usersRes) { + if (users.value.has(user.id)) continue; + + users.value.set(user.id, { + ...user, + token: accounts.find(a => a.id === user.id)!.token, + }); + } + } + + waiting.value = false; +} + +init(); + +function clickAddAccount(ev: MouseEvent) { + selectedUser.value = null; + + os.popupMenu([{ + text: i18n.ts.existingAccount, + action: () => { + getAccountWithSigninDialog().then(async (res) => { + if (res != null) { + os.success(); + await init(); + if (users.value.has(res.id)) { + selectedUser.value = res.id; + } + } + }); + }, + }, { + text: i18n.ts.createAccount, + action: () => { + getAccountWithSignupDialog().then(async (res) => { + if (res != null) { + os.success(); + await init(); + if (users.value.has(res.id)) { + selectedUser.value = res.id; + } + } + }); + }, + }], ev.currentTarget ?? ev.target); +} + +function clickChooseAccount() { + if (selectedUser.value === null) return; + + phase.value = 'consent'; +} + +function clickBackToAccountSelect() { + selectedUser.value = null; + phase.value = 'accountSelect'; +} + +function clickCancel() { + if (selectedUser.value === null) return; + + const user = users.value.get(selectedUser.value)!; + + const token = user.token; + + if (props.waitOnDeny) { + waiting.value = true; + } + emit('deny', token); +} + +async function clickAccept() { + if (selectedUser.value === null) return; + + const user = users.value.get(selectedUser.value)!; + + const token = user.token; + + waiting.value = true; + emit('accept', token); +} + +function showUI(state: 'success' | 'denied' | 'failed') { + phase.value = state; + waiting.value = false; +} + +defineExpose({ + showUI, +}); +</script> + +<style lang="scss" module> +.transition_enterActive, +.transition_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_leaveTo { + opacity: 0; + transform: translateX(-50px); +} + +.wrapper { + overflow-x: hidden; + overflow-x: clip; + + position: relative; + width: 100%; + height: 100%; +} + +.waitingRoot { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%); + display: flex; + justify-content: center; + align-items: center; + z-index: 1; + cursor: wait; +} + +.root { + position: relative; + box-sizing: border-box; + width: 100%; + padding: 48px 24px; +} + +.header { + margin: 0 auto; + max-width: 320px; +} + +.icon, +.iconFallback { + display: block; + margin: 0 auto; + width: 54px; + height: 54px; +} + +.icon { + border-radius: 50%; + border: 1px solid var(--MI_THEME-divider); + background-color: #fff; + object-fit: contain; +} + +.iconFallback { + border-radius: 50%; + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + text-align: center; + line-height: 54px; + font-size: 18px; +} + +.headerText, +.headerTextSub { + text-align: center; + word-break: normal; + word-break: auto-phrase; +} + +.headerText { + font-size: 16px; + font-weight: 700; +} + +.permissionRoot { + padding: 16px; + border-radius: var(--MI-radius); + background-color: var(--MI_THEME-bg); +} + +.permissionListWrapper { + max-height: 350px; + overflow-y: auto; + padding: 12px; + border-radius: var(--MI-radius); + background-color: var(--MI_THEME-panel); +} + +.permissionList { + margin: 0 0 0 1.5em; + padding: 0; + font-size: 90%; +} + +.accountSelectorLabel { + font-size: 0.85em; + opacity: 0.7; + margin-bottom: 8px; +} + +.accountSelectorList { + border-radius: var(--MI-radius); + border: 1px solid var(--MI_THEME-divider); + overflow: hidden; + overflow: clip; +} + +.accountSelectorRadio { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; + + &:focus-visible + .accountSelectorItem { + outline: 2px solid var(--MI_THEME-accent); + outline-offset: -4px; + } + + &:checked:focus-visible + .accountSelectorItem { + outline-color: #fff; + } + + &:checked + .accountSelectorItem { + background: var(--MI_THEME-accent); + color: #fff; + } +} + +.accountSelectorItem { + display: flex; + align-items: center; + padding: 8px; + font-size: 14px; + -webkit-tap-highlight-color: transparent; + cursor: pointer; + + &:hover { + background: var(--MI_THEME-buttonHoverBg); + } + + &.static { + cursor: unset; + + &:hover { + background: none; + } + } +} + +.accountSelectorAddAccountRoot { + width: 100%; +} + +.accountSelectorBody { + padding: 0 8px; + min-width: 0; +} + +.accountSelectorAvatar { + width: 45px; + height: 45px; +} + +.accountSelectorAddAccountAvatar { + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + font-size: 16px; + line-height: 45px; + text-align: center; + border-radius: 50%; +} + +.accountSelectorName { + display: block; + font-weight: bold; +} + +.accountSelectorAcct { + opacity: 0.5; +} +</style> diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index de5207f350..a0cd066c06 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -407,16 +407,16 @@ onBeforeUnmount(() => { text-overflow: ellipsis; &:hover { - background: var(--X3); + background: var(--MI_THEME-X3); } &[data-selected='true'] { - background: var(--accent); + background: var(--MI_THEME-accent); color: #fff !important; } &:active { - background: var(--accentDarken); + background: var(--MI_THEME-accentDarken); color: #fff !important; } } @@ -427,7 +427,7 @@ onBeforeUnmount(() => { max-width: 28px; max-height: 28px; margin: 0 8px 0 0; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); } .userName { diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index e30f74460d..a6e5651d63 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -129,8 +129,8 @@ function onMousedown(evt: MouseEvent): void { font-size: 95%; box-shadow: none; text-decoration: none; - background: var(--buttonBg); - border-radius: var(--radius-xs); + background: var(--MI_THEME-buttonBg); + border-radius: var(--MI-radius-xs); overflow: clip; box-sizing: border-box; transition: background 0.1s ease; @@ -140,11 +140,11 @@ function onMousedown(evt: MouseEvent): void { } &:not(:disabled):hover { - background: var(--buttonHoverBg); + background: var(--MI_THEME-buttonHoverBg); } &:not(:disabled):active { - background: var(--buttonHoverBg); + background: var(--MI_THEME-buttonHoverBg); } &.small { @@ -162,20 +162,20 @@ function onMousedown(evt: MouseEvent): void { } &.rounded { - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); } &.primary { font-weight: bold; - color: var(--fgOnAccent) !important; - background: var(--accent); + color: var(--MI_THEME-fgOnAccent) !important; + background: var(--MI_THEME-accent); &:not(:disabled):hover { - background: hsl(from var(--accent) h s calc(l + 5)); + background: hsl(from var(--MI_THEME-accent) h s calc(l + 5)); } &:not(:disabled):active { - background: hsl(from var(--accent) h s calc(l + 5)); + background: hsl(from var(--MI_THEME-accent) h s calc(l + 5)); } } @@ -216,15 +216,15 @@ function onMousedown(evt: MouseEvent): void { &.gradate { font-weight: bold; - color: var(--fgOnAccent) !important; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + color: var(--MI_THEME-fgOnAccent) !important; + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); } &:not(:disabled):active { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); } } @@ -272,7 +272,7 @@ function onMousedown(evt: MouseEvent): void { left: 0; width: 100%; height: 100%; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); overflow: clip; pointer-events: none; } @@ -281,7 +281,7 @@ function onMousedown(evt: MouseEvent): void { position: absolute; width: 2px; height: 2px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); background: rgba(0, 0, 0, 0.1); opacity: 1; transform: scale(1); diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index ab00ea9930..e9493edbd1 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -10,6 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only <div id="mcaptcha__widget-container" class="m-captcha-style"></div> <div ref="captchaEl"></div> </div> + <div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;"> + <img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/> + <div v-if="testcaptchaPassed"> + <div style="color: green;">Test captcha passed!</div> + </div> + <div v-else> + <div style="font-size: 13px; margin-bottom: 4px;">Type "ai-chan-kawaii" to pass captcha</div> + <input v-model="testcaptchaInput" data-cy-testcaptcha-input/> + <button type="button" data-cy-testcaptcha-submit @click="testcaptchaSubmit">Submit</button> + </div> + </div> <div v-else ref="captchaEl"></div> </div> </template> @@ -32,7 +43,7 @@ export type Captcha = { }): void; }; -export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'fc'; +export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'fc' | 'testcaptcha'; type CaptchaContainer = { readonly [_ in CaptchaProvider]?: Captcha; @@ -57,6 +68,9 @@ const available = ref(false); const captchaEl = shallowRef<HTMLDivElement | undefined>(); +const testcaptchaInput = ref(''); +const testcaptchaPassed = ref(false); + const variable = computed(() => { switch (props.provider) { case 'hcaptcha': return 'hcaptcha'; @@ -64,6 +78,7 @@ const variable = computed(() => { case 'turnstile': return 'turnstile'; case 'mcaptcha': return 'mcaptcha'; case 'fc': return 'friendlyChallenge'; + case 'testcaptcha': return 'testcaptcha'; } }); @@ -76,6 +91,7 @@ const src = computed(() => { case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; case 'fc': return 'https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.18/widget.min.js'; case 'mcaptcha': return null; + case 'testcaptcha': return null; } }); @@ -83,7 +99,7 @@ const scriptId = computed(() => `script-${props.provider}`); const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha); -if (loaded || props.provider === 'mcaptcha') { +if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') { available.value = true; } else if (src.value !== null) { (document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), { @@ -96,6 +112,8 @@ if (loaded || props.provider === 'mcaptcha') { function reset() { if (captcha.value.reset) captcha.value.reset(); + testcaptchaPassed.value = false; + testcaptchaInput.value = ''; } async function requestRender() { @@ -104,8 +122,8 @@ async function requestRender() { sitekey: props.sitekey, theme: defaultStore.state.darkMode ? 'dark' : 'light', callback: callback, - 'expired-callback': callback, - 'error-callback': callback, + 'expired-callback': () => callback(undefined), + 'error-callback': () => callback(undefined), }); } else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) { const { default: Widget } = await import('@mcaptcha/vanilla-glue'); @@ -140,6 +158,12 @@ function onReceivedMessage(message: MessageEvent) { } } +function testcaptchaSubmit() { + testcaptchaPassed.value = testcaptchaInput.value === 'ai-chan-kawaii'; + callback(testcaptchaPassed.value ? 'testcaptcha-passed' : undefined); + if (!testcaptchaPassed.value) testcaptchaInput.value = ''; +} + onMounted(() => { if (available.value) { window.addEventListener('message', onReceivedMessage); diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue index 6dace43fde..7aa916134f 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.vue +++ b/packages/frontend/src/components/MkChannelFollowButton.vue @@ -68,13 +68,13 @@ async function onClick() { position: relative; display: inline-block; font-weight: bold; - color: var(--accent); + color: var(--MI_THEME-accent); background: transparent; - border: solid 1px var(--accent); + border: solid 1px var(--MI_THEME-accent); padding: 0; height: 31px; font-size: 16px; - border-radius: var(--radius-xl); + border-radius: var(--MI-radius-xl); background: #fff; &.full { @@ -99,17 +99,17 @@ async function onClick() { } &.active { - color: var(--fgOnAccent); - background: var(--accent); + color: var(--MI_THEME-fgOnAccent); + background: var(--MI_THEME-accent); &:hover { - background: var(--accentLighten); - border-color: var(--accentLighten); + background: var(--MI_THEME-accentLighten); + border-color: var(--MI_THEME-accentLighten); } &:active { - background: var(--accentDarken); - border-color: var(--accentDarken); + background: var(--MI_THEME-accentDarken); + border-color: var(--MI_THEME-accentDarken); } } diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue index 2f7ec34d44..e036fec528 100644 --- a/packages/frontend/src/components/MkChannelPreview.vue +++ b/packages/frontend/src/components/MkChannelPreview.vue @@ -47,11 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref, watch } from 'vue'; +import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; const props = defineProps<{ - channel: Record<string, any>; + channel: Misskey.entities.Channel; }>(); const getLastReadedAt = (): number | null => { @@ -100,7 +101,7 @@ const bannerStyle = computed(() => { height: 100%; border-radius: inherit; pointer-events: none; - box-shadow: inset 0 0 0 2px var(--focus); + box-shadow: inset 0 0 0 2px var(--MI_THEME-focus); } } @@ -117,7 +118,7 @@ const bannerStyle = computed(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); } > .name { @@ -138,7 +139,7 @@ const bannerStyle = computed(() => { padding: 8px 12px; font-size: 80%; background: rgba(0, 0, 0, 0.7); - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); color: #fff; } @@ -148,8 +149,8 @@ const bannerStyle = computed(() => { bottom: 16px; left: 16px; background: rgba(0, 0, 0, 0.7); - color: var(--warn); - border-radius: var(--radius-sm); + color: var(--MI_THEME-warn); + border-radius: var(--MI-radius-sm); font-weight: bold; font-size: 1em; padding: 4px 7px; @@ -167,7 +168,7 @@ const bannerStyle = computed(() => { > footer { padding: 12px 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); > span { opacity: 0.7; @@ -213,9 +214,9 @@ const bannerStyle = computed(() => { top: 0; right: 0; transform: translate(25%, -25%); - background-color: var(--accent); - border: solid var(--bg) 4px; - border-radius: var(--radius-full); + background-color: var(--MI_THEME-accent); + border: solid var(--MI_THEME-bg) 4px; + border-radius: var(--MI-radius-full); width: 1.5rem; height: 1.5rem; aspect-ratio: 1 / 1; diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index 57d325b11a..d05f4921f6 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -863,8 +863,8 @@ onMounted(() => { left: 0; width: 100%; height: 100%; - -webkit-backdrop-filter: var(--blur, blur(12px)); - backdrop-filter: var(--blur, blur(12px)); + -webkit-backdrop-filter: var(--MI-blur, blur(12px)); + backdrop-filter: var(--MI-blur, blur(12px)); display: flex; justify-content: center; align-items: center; diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue index 240c9c919e..e28d6ad6ba 100644 --- a/packages/frontend/src/components/MkChartLegend.vue +++ b/packages/frontend/src/components/MkChartLegend.vue @@ -53,11 +53,11 @@ defineExpose({ > .item { font-size: 85%; padding: 4px 12px 4px 8px; - border: solid 1px var(--divider); - border-radius: var(--radius-ellipse); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-ellipse); &:hover { - border-color: var(--inputBorderHover); + border-color: var(--MI_THEME-inputBorderHover); } &.disabled { @@ -69,7 +69,7 @@ defineExpose({ display: inline-block; width: 12px; height: 12px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); vertical-align: -10%; } } diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue index dd550733cb..5b09ec90dd 100644 --- a/packages/frontend/src/components/MkClipPreview.vue +++ b/packages/frontend/src/components/MkClipPreview.vue @@ -49,13 +49,13 @@ const remaining = computed(() => { outline: none; .root { - box-shadow: inset 0 0 0 2px var(--focus); + box-shadow: inset 0 0 0 2px var(--MI_THEME-focus); } } &:hover { text-decoration: none; - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -65,7 +65,7 @@ const remaining = computed(() => { .divider { height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .description { diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue index 9e54420034..e253b1b55f 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -95,8 +95,8 @@ watch(() => props.lang, (to) => { padding: 1em; margin: .5em 0; overflow: auto; - border-radius: var(--radius-sm); - border: 1px solid var(--divider); + border-radius: var(--MI-radius-sm); + border: 1px solid var(--MI_THEME-divider); font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; color: var(--shiki-fallback); @@ -140,7 +140,7 @@ watch(() => props.lang, (to) => { & :global(.shiki) { padding: 12px; margin: 0; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); border: none; min-height: 130px; pointer-events: none; diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index 05cde89dd9..a46b220101 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -71,11 +71,11 @@ function copy() { .codeBlockFallbackRoot { display: block; overflow-wrap: anywhere; - background: var(--bg); + background: var(--MI_THEME-bg); padding: 1em; margin: .5em 0; overflow: auto; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .codeBlockFallbackCode { @@ -91,11 +91,11 @@ function copy() { cursor: pointer; box-sizing: border-box; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); padding: 24px; margin-top: 4px; - color: var(--fg); - background: var(--bg); + color: var(--MI_THEME-fg); + background: var(--MI_THEME-bg); } .codePlaceholderContainer { diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index b233189ab0..49b083815a 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -140,7 +140,7 @@ watch(v, newValue => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -158,20 +158,20 @@ watch(v, newValue => { overflow-y: hidden; box-sizing: border-box; margin: 0; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); padding: 0; - color: var(--fg); - border: solid 1px var(--panel); + color: var(--MI_THEME-fg); + border: solid 1px var(--MI_THEME-panel); transition: border-color 0.1s ease-out; font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; &:hover { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } .focused.codeEditorRoot { - border-color: var(--accent) !important; - border-radius: var(--radius-sm); + border-color: var(--MI_THEME-accent) !important; + border-radius: var(--MI-radius-sm); } .codeEditorScroller { @@ -196,10 +196,10 @@ watch(v, newValue => { resize: none; text-align: left; color: transparent; - caret-color: var(--fg); + caret-color: var(--MI_THEME-fg); background-color: transparent; border: 0; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); box-sizing: border-box; outline: 0; min-width: calc(100% - 24px); @@ -213,6 +213,6 @@ watch(v, newValue => { } .textarea::selection { - color: var(--bg); + color: var(--MI_THEME-bg); } </style> diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue index 6add80d1bc..04b6e54108 100644 --- a/packages/frontend/src/components/MkCodeInline.vue +++ b/packages/frontend/src/components/MkCodeInline.vue @@ -18,7 +18,7 @@ const props = defineProps<{ display: inline-block; font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; overflow-wrap: anywhere; - background: var(--bg); + background: var(--MI_THEME-bg); padding: .1em; border-radius: .3em; } diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue index 99aa46d561..babf356942 100644 --- a/packages/frontend/src/components/MkColorInput.vue +++ b/packages/frontend/src/components/MkColorInput.vue @@ -60,7 +60,7 @@ const onInput = () => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -72,8 +72,8 @@ const onInput = () => { &.focused { > .inputCore { - border-color: var(--accent) !important; - //box-shadow: 0 0 0 4px var(--focus); + border-color: var(--MI_THEME-accent) !important; + //box-shadow: 0 0 0 4px var(--MI_THEME-focus); } } @@ -98,17 +98,17 @@ const onInput = () => { font: inherit; font-weight: normal; font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + color: var(--MI_THEME-fg); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); outline: none; box-shadow: none; box-sizing: border-box; transition: border-color 0.1s ease-out; &:hover { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } </style> diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index 5f71e289b8..f4d20c7d8c 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -64,26 +64,30 @@ const showBody = ref(props.expanded); const ignoreOmit = ref(false); const omitted = ref(false); -function enter(el) { +function enter(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; - el.style.height = 0; + el.style.height = '0'; el.offsetHeight; // reflow - el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px'; + el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`; } -function afterEnter(el) { - el.style.height = null; +function afterEnter(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } -function leave(el) { +function leave(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + 'px'; + el.style.height = `${elementHeight}px`; el.offsetHeight; // reflow - el.style.height = 0; + el.style.height = '0'; } -function afterLeave(el) { - el.style.height = null; +function afterLeave(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } const calcOmit = () => { @@ -138,7 +142,7 @@ onUnmounted(() => { position: relative; overflow: clip; contain: content; - background: color-mix(in srgb, var(--panel) 65%, transparent); + background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent); &.naked { background: transparent !important; box-shadow: none !important; @@ -165,13 +169,14 @@ onUnmounted(() => { .header { position: sticky; - top: var(--stickyTop, 0px); + top: var(--MI-stickyTop, 0px); left: 0; - color: var(--panelHeaderFg); - border-bottom: solid 0.5px var(--panelHeaderDivider); + color: var(--MI_THEME-panelHeaderFg); + background: var(--MI_THEME-panelHeaderBg); + border-bottom: solid 0.5px var(--MI_THEME-panelHeaderDivider); z-index: 2; line-height: 1.4em; - background: color-mix(in srgb, var(--panelHeaderBg) 35%, transparent); + background: color-mix(in srgb, var(--MI_THEME-panelHeaderBg) 35%, transparent); } .title { @@ -201,7 +206,7 @@ onUnmounted(() => { } .content { - --stickyTop: 0px; + --MI-stickyTop: 0px; &.omitted { position: relative; @@ -216,20 +221,20 @@ onUnmounted(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; - background: var(--panel); + background: var(--MI_THEME-panel); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } &:hover { > .fadeLabel { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } } } diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 2e1e92cbdf..0186cfc2c0 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only :withOkButton="true" @close="cancel()" @ok="ok()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header>{{ i18n.ts.cropImage }}</template> <template #default="{ width, height }"> @@ -125,7 +125,7 @@ onMounted(() => { const computedStyle = getComputedStyle(document.documentElement); const selection = cropper.getCropperSelection()!; - selection.themeColor = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(); + selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(); selection.aspectRatio = props.aspectRatio; selection.initialAspectRatio = props.aspectRatio; selection.outlined = true; @@ -170,8 +170,8 @@ onMounted(() => { display: flex; align-items: center; justify-content: center; - -webkit-backdrop-filter: var(--blur, blur(10px)); - backdrop-filter: var(--blur, blur(10px)); + -webkit-backdrop-filter: var(--MI-blur, blur(10px)); + backdrop-filter: var(--MI-blur, blur(10px)); background: rgba(0, 0, 0, 0.5); } diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue index c7f1288729..ecbee864dc 100644 --- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue +++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')"> +<MkModalWindow ref="dialogEl" @close="cancel()" @closed="emit('closed')"> <template #header>:{{ emoji.name }}:</template> <template #default> <MkSpacer> @@ -85,8 +85,8 @@ function cancel() { .emojiImgWrapper { max-width: 100%; height: 40cqh; - background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--X5) 8px, var(--X5) 14px); - border-radius: var(--radius); + background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-X5) 8px, var(--MI_THEME-X5) 14px); + border-radius: var(--MI-radius); margin: auto; overflow-y: hidden; } @@ -101,8 +101,8 @@ function cancel() { display: inline-block; word-break: break-all; padding: 3px 10px; - background-color: var(--X5); - border: solid 1px var(--divider); - border-radius: var(--radius); + background-color: var(--MI_THEME-X5); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius); } </style> diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 98bf5191f7..4b9666c55c 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -9,6 +9,7 @@ import MkAd from '@/components/global/MkAd.vue'; import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { instance } from '@/instance.js'; import { defaultStore } from '@/store.js'; import { MisskeyEntity } from '@/types/date-separated-list.js'; @@ -99,11 +100,13 @@ export default defineComponent({ return [el, separator]; } else { - if (props.ad && item._shouldInsertAd_) { - return [h(MkAd, { + if (props.ad && instance.ads.length > 0 && item._shouldInsertAd_) { + return [h('div', { key: item.id + ':ad', + class: $style['ad-wrapper'], + }, [h(MkAd, { prefer: ['horizontal', 'horizontal-big'], - }), el]; + })]), el]; } else { return el; } @@ -125,14 +128,14 @@ export default defineComponent({ return children; }; - function onBeforeLeave(element: Element) { - const el = element as HTMLElement; + function onBeforeLeave(el: Element) { + if (!(el instanceof HTMLElement)) return; el.style.top = `${el.offsetTop}px`; el.style.left = `${el.offsetLeft}px`; } - function onLeaveCancelled(element: Element) { - const el = element as HTMLElement; + function onLeaveCancelled(el: Element) { + if (!(el instanceof HTMLElement)) return; el.style.top = ''; el.style.left = ''; } @@ -182,12 +185,12 @@ export default defineComponent({ } &:not(.date-separated-list-nogap) > *:not(:last-child) { - margin-bottom: var(--margin); + margin-bottom: var(--MI-margin); } } .date-separated-list-nogap { - border-radius: var(--radius); + border-radius: var(--MI-radius); > * { margin: 0 !important; @@ -196,7 +199,7 @@ export default defineComponent({ box-shadow: none; &:not(:last-child) { - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); } } } @@ -237,7 +240,7 @@ export default defineComponent({ line-height: 32px; text-align: center; font-size: 12px; - color: var(--dateLabelFg); + color: var(--MI_THEME-dateLabelFg); } .date-1 { @@ -255,5 +258,11 @@ export default defineComponent({ .date-2-icon { margin-left: 8px; } + +.ad-wrapper { + 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); +} </style> diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 7dc381b662..9a59a9aac7 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </MkSelect> <div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons"> - <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton> + <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton> <MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton> </div> <div v-if="actions" :class="$style.buttons"> @@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{ text: string; primary?: boolean, danger?: boolean, - callback: (...args: any[]) => void; + callback: (...args: unknown[]) => void; }[]; showOkButton?: boolean; showCancelButton?: boolean; @@ -185,8 +185,8 @@ function onInputKeydown(evt: KeyboardEvent) { max-width: 480px; box-sizing: border-box; text-align: center; - background: var(--panel); - border-radius: var(--radius-md); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-md); } .icon { @@ -207,15 +207,15 @@ function onInputKeydown(evt: KeyboardEvent) { } .type_success { - color: var(--success); + color: var(--MI_THEME-success); } .type_error { - color: var(--error); + color: var(--MI_THEME-error); } .type_warning { - color: var(--warn); + color: var(--MI_THEME-warn); } .title { diff --git a/packages/frontend/src/components/MkDivider.vue b/packages/frontend/src/components/MkDivider.vue index e4e3af99e4..f72f091383 100644 --- a/packages/frontend/src/components/MkDivider.vue +++ b/packages/frontend/src/components/MkDivider.vue @@ -27,6 +27,6 @@ defineProps<{ <style scoped lang="scss"> .default { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue index 1dfdebf0d4..43d2002204 100644 --- a/packages/frontend/src/components/MkDonation.vue +++ b/packages/frontend/src/components/MkDonation.vue @@ -75,22 +75,22 @@ function neverShow() { .root { position: fixed; z-index: v-bind(zIndex); - bottom: var(--margin); + bottom: var(--MI-margin); left: 0; right: 0; margin: auto; box-sizing: border-box; - width: calc(100% - (var(--margin) * 2)); + width: calc(100% - (var(--MI-margin) * 2)); max-width: 500px; display: flex; - backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } .icon { text-align: center; padding-top: 25px; width: 100px; - color: var(--accent); + color: var(--MI_THEME-accent); } @media (max-width: 500px) { .icon { diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 20ad2984d8..5dee448329 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -112,7 +112,7 @@ function onDragend() { position: relative; padding: 8px 0 0 0; min-height: 180px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); cursor: pointer; &:hover { @@ -152,14 +152,14 @@ function onDragend() { } &.isSelected { - background: var(--accent); + background: var(--MI_THEME-accent); &:hover { - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); } &:active { - background: var(--accentDarken); + background: var(--MI_THEME-accentDarken); } > .label { @@ -248,7 +248,7 @@ function onDragend() { font-size: 0.8em; text-align: center; word-break: break-all; - color: var(--fg); + color: var(--MI_THEME-fg); overflow: hidden; } </style> diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 44788a6ffb..8496890f60 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, defineAsyncComponent, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import type { MenuItem } from '@/types/menu.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -313,8 +313,8 @@ function onContextmenu(ev: MouseEvent) { position: relative; padding: 8px; height: 64px; - background: var(--driveFolderBg); - border-radius: var(--radius-xs); + background: var(--MI_THEME-driveFolderBg); + border-radius: var(--MI-radius-xs); cursor: pointer; &.draghover { @@ -326,8 +326,8 @@ function onContextmenu(ev: MouseEvent) { right: -4px; bottom: -4px; left: -4px; - border: 2px dashed var(--focus); - border-radius: var(--radius-xs); + border: 2px dashed var(--MI_THEME-focus); + border-radius: var(--MI-radius-xs); } } } @@ -345,13 +345,13 @@ function onContextmenu(ev: MouseEvent) { width: 18px; height: 18px; background: #fff; - border: solid 2px var(--divider); + border: solid 2px var(--MI_THEME-divider); border-radius: 4px; box-sizing: border-box; &.checked { - border-color: var(--accent); - background: var(--accent); + border-color: var(--MI_THEME-accent); + background: var(--MI_THEME-accent); &::after { content: "\ea5e"; @@ -368,14 +368,13 @@ function onContextmenu(ev: MouseEvent) { } &:hover { - background: var(--accentedBg); + background: var(--MI_THEME-accentedBg); } } .name { margin: 0; font-size: 0.9em; - color: var(--desktopDriveFolderFg); } .icon { @@ -388,6 +387,5 @@ function onContextmenu(ev: MouseEvent) { margin: 4px 4px; font-size: 0.8em; text-align: right; - color: var(--desktopDriveFolderFg); } </style> diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index a471457b44..5a0803f1e3 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -167,7 +167,12 @@ const ilFilesObserver = new IntersectionObserver( (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(), ); +const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt'); + watch(folder, () => emit('cd', folder.value)); +watch(sortModeSelect, () => { + fetch(); +}); function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) { addFile(file, true); @@ -203,7 +208,7 @@ function onStreamDriveFolderDeleted(folderId: string) { removeFolder(folderId); } -function onDragover(ev: DragEvent): any { +function onDragover(ev: DragEvent) { if (!ev.dataTransfer) return; // ドラッグ元が自分自身の所有するアイテムだったら @@ -248,7 +253,7 @@ function onDragleave() { draghover.value = false; } -function onDrop(ev: DragEvent): any { +function onDrop(ev: DragEvent) { draghover.value = false; if (!ev.dataTransfer) return; @@ -337,7 +342,7 @@ function createFolder() { title: i18n.ts.createFolder, placeholder: i18n.ts.folderName, }).then(({ canceled, result: name }) => { - if (canceled) return; + if (canceled || name == null) return; misskeyApi('drive/folders/create', { name: name, parentId: folder.value ? folder.value.id : undefined, @@ -570,6 +575,7 @@ async function fetch() { type: props.type, limit: filesMax + 1, searchQuery: searchQuery.value.toString().trim(), + sort: sortModeSelect.value, }).then(fetchedFiles => { if (fetchedFiles.length === filesMax + 1) { moreFiles.value = true; @@ -621,6 +627,7 @@ function fetchMoreFiles() { untilId: files.value.at(-1)?.id, limit: max + 1, searchQuery: searchQuery.value.toString().trim(), + sort: sortModeSelect.value, }).then(files => { if (files.length === max + 1) { moreFiles.value = true; @@ -656,6 +663,43 @@ function getMenu() { type: 'label', }); + menu.push({ + type: 'parent', + text: i18n.ts.sort, + icon: 'ti ti-arrows-sort', + children: [{ + text: `${i18n.ts.registeredDate} (${i18n.ts.descendingOrder})`, + icon: 'ti ti-sort-descending-letters', + action: () => { sortModeSelect.value = '+createdAt'; }, + active: sortModeSelect.value === '+createdAt', + }, { + text: `${i18n.ts.registeredDate} (${i18n.ts.ascendingOrder})`, + icon: 'ti ti-sort-ascending-letters', + action: () => { sortModeSelect.value = '-createdAt'; }, + active: sortModeSelect.value === '-createdAt', + }, { + text: `${i18n.ts.size} (${i18n.ts.descendingOrder})`, + icon: 'ti ti-sort-descending-letters', + action: () => { sortModeSelect.value = '+size'; }, + active: sortModeSelect.value === '+size', + }, { + text: `${i18n.ts.size} (${i18n.ts.ascendingOrder})`, + icon: 'ti ti-sort-ascending-letters', + action: () => { sortModeSelect.value = '-size'; }, + active: sortModeSelect.value === '-size', + }, { + text: `${i18n.ts.name} (${i18n.ts.descendingOrder})`, + icon: 'ti ti-sort-descending-letters', + action: () => { sortModeSelect.value = '+name'; }, + active: sortModeSelect.value === '+name', + }, { + text: `${i18n.ts.name} (${i18n.ts.ascendingOrder})`, + icon: 'ti ti-sort-ascending-letters', + action: () => { sortModeSelect.value = '-name'; }, + active: sortModeSelect.value === '-name', + }], + }); + if (folder.value) { menu.push({ text: i18n.ts.renameFolder, @@ -735,7 +779,7 @@ onBeforeUnmount(() => { box-sizing: border-box; overflow: auto; font-size: 0.9em; - box-shadow: 0 1px 0 var(--divider); + box-shadow: 0 1px 0 var(--MI_THEME-divider); user-select: none; } @@ -787,7 +831,7 @@ onBeforeUnmount(() => { .main { flex: 1; overflow: auto; - padding: var(--margin); + padding: var(--MI-margin); user-select: none; &.fetching { @@ -834,7 +878,7 @@ onBeforeUnmount(() => { top: 38px; width: 100%; height: calc(100% - 38px); - border: dashed 2px var(--focus); + border: dashed 2px var(--MI_THEME-focus); pointer-events: none; } </style> diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue index 543cf24022..1079e52030 100644 --- a/packages/frontend/src/components/MkDriveFileThumbnail.vue +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -69,8 +69,8 @@ const isThumbnailAvailable = computed(() => { .root { position: relative; display: flex; - background: var(--panel); - border-radius: var(--radius-sm); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); overflow: clip; } @@ -83,7 +83,7 @@ const isThumbnailAvailable = computed(() => { height: 100%; pointer-events: none; border-radius: inherit; - box-shadow: inset 0 0 0 4px var(--warn); + box-shadow: inset 0 0 0 4px var(--MI_THEME-warn); } .iconSub { diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue index c060c3a659..6e9eb75920 100644 --- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue +++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only :scroll="false" :withOkButton="false" @close="cancel()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template> @@ -306,9 +306,9 @@ onUnmounted(() => { .embedCodeGenPreviewRoot { position: relative; - background-color: var(--bg); + background-color: var(--MI_THEME-bg); background-size: auto auto; - background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--panel) 6px, var(--panel) 12px); + background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px); cursor: not-allowed; } @@ -381,8 +381,8 @@ onUnmounted(() => { .embedCodeGenResultHeadingIcon { margin: 0 auto; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); text-align: center; height: 64px; width: 64px; diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 151843b18c..a4f763e895 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと --> <!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) --> -<section v-if="!hasChildSection" v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);"> +<section v-if="!hasChildSection" v-panel style="border-radius: var(--MI-radius-sm); border-bottom: 0.5px solid var(--MI_THEME-divider);"> <header class="_acrylic" @click="shown = !shown"> <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ph-smiley-sticker ph-bold ph-lg"></i>:{{ emojis.length }}) </header> @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </section> <!-- フォルダの中にはカスタム絵文字やフォルダがある --> -<section v-else v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);"> +<section v-else v-panel style="border-radius: var(--MI-radius-sm); border-bottom: 0.5px solid var(--MI_THEME-divider);"> <header class="_acrylic" @click="shown = !shown"> <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ph-smiley-sticker ph-bold ph-lg ti-fw"></i>:{{ emojis.length }}) </header> @@ -90,7 +90,7 @@ function computeButtonTitle(ev: MouseEvent): void { elm.title = getEmojiName(emoji); } -function nestedChosen(emoji: any, ev: MouseEvent) { +function nestedChosen(emoji: string, ev: MouseEvent) { emit('chosen', emoji, ev); } </script> diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 949ed4db91..dc589a28e0 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -409,7 +409,7 @@ function computeButtonTitle(ev: MouseEvent): void { elm.title = getEmojiName(emoji); } -function chosen(emoji: any, ev?: MouseEvent) { +function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) { const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -426,7 +426,7 @@ function chosen(emoji: any, ev?: MouseEvent) { // 最近使った絵文字更新 if (!pinned.value?.includes(key)) { let recents = defaultStore.state.recentlyUsedEmojis; - recents = recents.filter((emoji: any) => emoji !== key); + recents = recents.filter((emoji) => emoji !== key); recents.unshift(key); defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); } @@ -580,7 +580,7 @@ defineExpose({ &:disabled { cursor: not-allowed; - background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%); opacity: 1; > .emoji { @@ -615,7 +615,7 @@ defineExpose({ &:disabled { cursor: not-allowed; - background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%); opacity: 1; > .emoji { @@ -638,7 +638,7 @@ defineExpose({ outline: none; border: none; background: transparent; - color: var(--fg); + color: var(--MI_THEME-fg); &:not(:focus):not(.filled) { margin-bottom: env(safe-area-inset-bottom, 0px); @@ -647,7 +647,7 @@ defineExpose({ &:not(.filled) { order: 1; z-index: 2; - box-shadow: 0px -1px 0 0px var(--divider); + box-shadow: 0px -1px 0 0px var(--MI_THEME-divider); } } @@ -658,11 +658,11 @@ defineExpose({ > .tab { flex: 1; height: 38px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); &.active { - border-top: solid 1px var(--accent); - color: var(--accent); + border-top: solid 1px var(--MI_THEME-accent); + color: var(--MI_THEME-accent); } } } @@ -681,7 +681,7 @@ defineExpose({ > .group { &:not(.index) { padding: 4px 0 8px 0; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } > header { @@ -708,7 +708,7 @@ defineExpose({ cursor: pointer; &:hover { - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -722,7 +722,7 @@ defineExpose({ width: var(--eachSize); height: var(--eachSize); contain: strict; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); font-size: 24px; &:hover { @@ -730,13 +730,13 @@ defineExpose({ } &:active { - background: var(--accent); + background: var(--MI_THEME-accent); box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15); } &:disabled { cursor: not-allowed; - background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%); opacity: 1; > .emoji { @@ -757,7 +757,7 @@ defineExpose({ } &.result { - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); &:empty { display: none; diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 18e80d8445..3178f72498 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -87,7 +87,7 @@ function opening() { <style lang="scss" module> .drawer { - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); border-bottom-right-radius: 0; border-bottom-left-radius: 0; } diff --git a/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts new file mode 100644 index 0000000000..6763f7c546 --- /dev/null +++ b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { StoryObj } from '@storybook/vue3'; +import MkExtensionInstaller from './MkExtensionInstaller.vue'; +import lightTheme from '@@/themes/_light.json5'; + +export const Plugin = { + render(args) { + return { + components: { + MkExtensionInstaller, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkExtensionInstaller v-bind="props" />', + }; + }, + args: { + extension: { + type: 'plugin', + raw: '"do nothing"', + meta: { + name: 'do nothing plugin', + version: '1.0', + author: 'syuilo and misskey-project', + description: 'a plugin that does nothing', + permissions: ['read:account'], + config: { + 'doNothing': true, + }, + }, + }, + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkExtensionInstaller>; + +export const Theme = { + render(args) { + return { + components: { + MkExtensionInstaller, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkExtensionInstaller v-bind="props" />', + }; + }, + args: { + extension: { + type: 'theme', + raw: JSON.stringify(lightTheme), + meta: lightTheme, + }, + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkExtensionInstaller>; diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue new file mode 100644 index 0000000000..d59b20435e --- /dev/null +++ b/packages/frontend/src/components/MkExtensionInstaller.vue @@ -0,0 +1,146 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="_gaps_m" :class="$style.extInstallerRoot"> + <div :class="$style.extInstallerIconWrapper"> + <i v-if="isPlugin" class="ti ti-plug"></i> + <i v-else-if="isTheme" class="ti ti-palette"></i> + <!-- 拡張用? --> + <i v-else class="ti ti-download"></i> + </div> + <h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].title }}</h2> + <div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div> + <MkInfo v-if="isPlugin" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo> + <FormSection> + <template #label>{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].metaTitle }}</template> + <div class="_gaps_s"> + <FormSplit> + <MkKeyValue> + <template #key>{{ i18n.ts.name }}</template> + <template #value>{{ extension.meta.name }}</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.author }}</template> + <template #value>{{ extension.meta.author }}</template> + </MkKeyValue> + </FormSplit> + <MkKeyValue v-if="isPlugin"> + <template #key>{{ i18n.ts.description }}</template> + <template #value>{{ extension.meta.description ?? i18n.ts.none }}</template> + </MkKeyValue> + <MkKeyValue v-if="isPlugin"> + <template #key>{{ i18n.ts.version }}</template> + <template #value>{{ extension.meta.version }}</template> + </MkKeyValue> + <MkKeyValue v-if="isPlugin"> + <template #key>{{ i18n.ts.permission }}</template> + <template #value> + <ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList"> + <li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li> + </ul> + <template v-else>{{ i18n.ts.none }}</template> + </template> + </MkKeyValue> + <MkKeyValue v-if="isTheme"> + <template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template> + <template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template> + </MkKeyValue> + <MkFolder> + <template #icon><i class="ti ti-code"></i></template> + <template #label>{{ i18n.ts._plugin.viewSource }}</template> + + <MkCode :code="extension.raw"/> + </MkFolder> + </div> + </FormSection> + <slot name="additionalInfo"/> + <div class="_buttonsCenter"> + <MkButton primary @click="emits('confirm')"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton> + </div> +</div> +</template> + +<script lang="ts"> +export type Extension = { + type: 'plugin'; + raw: string; + meta: { + name: string; + version: string; + author: string; + description?: string; + permissions?: string[]; + config?: Record<string, unknown>; + }; +} | { + type: 'theme'; + raw: string; + meta: { + name: string; + author: string; + base?: 'light' | 'dark'; + }; +}; +</script> +<script lang="ts" setup> +import { computed } from 'vue'; +import MkButton from '@/components/MkButton.vue'; +import FormSection from '@/components/form/section.vue'; +import FormSplit from '@/components/form/split.vue'; +import MkCode from '@/components/MkCode.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; +import { i18n } from '@/i18n.js'; + +const isPlugin = computed(() => props.extension.type === 'plugin'); +const isTheme = computed(() => props.extension.type === 'theme'); + +const props = defineProps<{ + extension: Extension; +}>(); + +const emits = defineEmits<{ + (ev: 'confirm'): void; +}>(); +</script> + +<style lang="scss" module> +.extInstallerRoot { + border-radius: var(--MI-radius); + background: var(--MI_THEME-panel); + padding: 1.5rem; +} + +.extInstallerIconWrapper { + width: 48px; + height: 48px; + font-size: 24px; + line-height: 48px; + text-align: center; + border-radius: 50%; + margin-left: auto; + margin-right: auto; + + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); +} + +.extInstallerTitle { + font-size: 1.2rem; + text-align: center; + margin: 0; +} + +.extInstallerNormDesc { + text-align: center; +} + +.extInstallerKVList { + margin-top: 0; + margin-bottom: 0; +} +</style> diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue index 7af68a32ba..204bf69f48 100644 --- a/packages/frontend/src/components/MkFileListForAdmin.vue +++ b/packages/frontend/src/components/MkFileListForAdmin.vue @@ -66,7 +66,7 @@ const props = defineProps<{ align-items: center; &:hover { - color: var(--accent); + color: var(--MI_THEME-accent); } > .thumbnail { @@ -108,7 +108,7 @@ const props = defineProps<{ padding: 2px 4px; background: #ff0000bf; color: #fff; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); font-size: 85%; animation: sensitive-blink 1s infinite; } diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue index 85f7917658..0bb1450f87 100644 --- a/packages/frontend/src/components/MkFlashPreview.vue +++ b/packages/frontend/src/components/MkFlashPreview.vue @@ -35,7 +35,7 @@ const props = defineProps<{ &:hover { text-decoration: none; - color: var(--accent); + color: var(--MI_THEME-accent); } &:focus-visible { @@ -82,7 +82,6 @@ const props = defineProps<{ > p { display: inline-block; margin: 0; - color: var(--urlPreviewInfo); font-size: 0.8em; line-height: 16px; vertical-align: top; @@ -91,7 +90,7 @@ const props = defineProps<{ } &:global(.gray) { - --c: var(--bg); + --c: var(--MI_THEME-bg); background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); background-size: 16px 16px; } diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue index f10d58b38a..5bf3fdfe76 100644 --- a/packages/frontend/src/components/MkFoldableSection.vue +++ b/packages/frontend/src/components/MkFoldableSection.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div ref="rootEl" :class="$style.root"> - <header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody"> + <header :class="$style.header" class="_button" @click="showBody = !showBody"> <div :class="$style.title"><div><slot name="header"></slot></div></div> <div :class="$style.divider"></div> <button class="_button" :class="$style.button"> @@ -32,21 +32,23 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref, shallowRef, watch } from 'vue'; -import tinycolor from 'tinycolor2'; import { miLocalStorage } from '@/local-storage.js'; import { defaultStore } from '@/store.js'; +import { getBgColor } from '@/scripts/get-bg-color.js'; const miLocalStoragePrefix = 'ui:folder:' as const; const props = withDefaults(defineProps<{ expanded?: boolean; - persistKey?: string; + persistKey?: string | null; }>(), { expanded: true, + persistKey: null, }); -const rootEl = shallowRef<HTMLDivElement>(); -const bg = ref<string>(); +const rootEl = shallowRef<HTMLElement>(); +const parentBg = ref<string | null>(null); +// eslint-disable-next-line vue/no-setup-props-reactivity-loss const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded); watch(showBody, () => { @@ -55,47 +57,34 @@ watch(showBody, () => { } }); -function enter(element: Element) { - const el = element as HTMLElement; +function enter(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; el.style.height = '0'; el.offsetHeight; // reflow - el.style.height = elementHeight + 'px'; + el.style.height = `${elementHeight}px`; } -function afterEnter(element: Element) { - const el = element as HTMLElement; - el.style.height = 'unset'; +function afterEnter(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } -function leave(element: Element) { - const el = element as HTMLElement; +function leave(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + 'px'; + el.style.height = `${elementHeight}px`; el.offsetHeight; // reflow el.style.height = '0'; } -function afterLeave(element: Element) { - const el = element as HTMLElement; - el.style.height = 'unset'; +function afterLeave(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } onMounted(() => { - function getParentBg(el?: HTMLElement | null): string { - if (el == null || el.tagName === 'BODY') return 'var(--bg)'; - const background = el.style.background || el.style.backgroundColor; - if (background) { - return background; - } else { - return getParentBg(el.parentElement); - } - } - - const rawBg = getParentBg(rootEl.value); - const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); - _bg.setAlpha(0.85); - bg.value = _bg.toRgbString(); + parentBg.value = getBgColor(rootEl.value?.parentElement); }); </script> @@ -118,9 +107,10 @@ onMounted(() => { position: relative; z-index: 10; position: sticky; - top: var(--stickyTop, 0px); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(20px)); + top: var(--MI-stickyTop, 0px); + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(20px)); + background-color: color(from v-bind("parentBg ?? 'var(--MI_THEME-bg)'") srgb r g b / 0.85); } .title { @@ -134,7 +124,7 @@ onMounted(() => { flex: 1; margin: auto; height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .button { diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 392963fdb9..0b4114d252 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -38,9 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only > <KeepAlive> <div v-show="opened"> - <MkSpacer :marginMin="14" :marginMax="22"> + <MkSpacer v-if="withSpacer" :marginMin="14" :marginMax="22"> <slot></slot> </MkSpacer> + <div v-else> + <slot></slot> + </div> <div v-if="$slots.footer" :class="$style.footer"> <slot name="footer"></slot> </div> @@ -53,51 +56,49 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, onMounted, shallowRef, ref } from 'vue'; +import { nextTick, onMounted, ref, shallowRef } from 'vue'; import { defaultStore } from '@/store.js'; +import { getBgColor } from '@/scripts/get-bg-color.js'; const props = withDefaults(defineProps<{ defaultOpen?: boolean; maxHeight?: number | null; + withSpacer?: boolean; }>(), { defaultOpen: false, maxHeight: null, + withSpacer: true, }); -const getBgColor = (el: HTMLElement) => { - const style = window.getComputedStyle(el); - if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { - return style.backgroundColor; - } else { - return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; - } -}; - const rootEl = shallowRef<HTMLElement>(); const bgSame = ref(false); const opened = ref(props.defaultOpen); const openedAtLeastOnce = ref(props.defaultOpen); -function enter(el) { +function enter(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; - el.style.height = 0; + el.style.height = '0'; el.offsetHeight; // reflow - el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px'; + el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`; } -function afterEnter(el) { - el.style.height = null; +function afterEnter(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } -function leave(el) { +function leave(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + 'px'; + el.style.height = `${elementHeight}px`; el.offsetHeight; // reflow - el.style.height = 0; + el.style.height = '0'; } -function afterLeave(el) { - el.style.height = null; +function afterLeave(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } function toggle() { @@ -112,8 +113,8 @@ function toggle() { onMounted(() => { const computedStyle = getComputedStyle(document.documentElement); - const parentBg = getBgColor(rootEl.value!.parentElement!); - const myBg = computedStyle.getPropertyValue('--panel'); + const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent'; + const myBg = computedStyle.getPropertyValue('--MI_THEME-panel'); bgSame.value = parentBg === myBg; }); </script> @@ -139,15 +140,15 @@ onMounted(() => { width: 100%; box-sizing: border-box; padding: 9px 12px 9px 12px; - background: var(--folderHeaderBg); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - border-radius: var(--radius-sm); + background: var(--MI_THEME-folderHeaderBg); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + border-radius: var(--MI-radius-sm); transition: border-radius 0.3s; &:hover { text-decoration: none; - background: var(--folderHeaderHoverBg); + background: var(--MI_THEME-folderHeaderHoverBg); } &:focus-within { @@ -155,12 +156,12 @@ onMounted(() => { } &.active { - color: var(--accent); - background: var(--folderHeaderHoverBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-folderHeaderHoverBg); } &.opened { - border-radius: var(--radius-sm) var(--radius-sm) 0 0; + border-radius: var(--MI-radius-sm) var(--MI-radius-sm) 0 0; } } @@ -170,7 +171,7 @@ onMounted(() => { } .headerLower { - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); font-size: .85em; padding-left: 4px; } @@ -204,13 +205,13 @@ onMounted(() => { } .headerTextSub { - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); font-size: .85em; } .headerRight { margin-left: auto; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); white-space: nowrap; } @@ -219,26 +220,26 @@ onMounted(() => { } .body { - background: var(--panel); - border-radius: 0 0 var(--radius-sm) var(--radius-sm); + background: var(--MI_THEME-panel); + border-radius: 0 0 var(--MI-radius-sm) var(--MI-radius-sm); container-type: inline-size; &.bgSame { - background: var(--bg); + background: var(--MI_THEME-bg); } } .footer { position: sticky !important; z-index: 1; - bottom: var(--stickyBottom, 0px); + bottom: var(--MI-stickyBottom, 0px); left: 0; padding: 12px; - background: var(--acrylicBg); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + background: var(--MI_THEME-acrylicBg); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); background-size: auto auto; - background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--panel) 5px, var(--panel) 10px); + background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--MI_THEME-panel) 5px, var(--MI_THEME-panel) 10px); border-radius: 0 0 6px 6px; } </style> diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index 52497a2994..c7965aaac4 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -37,13 +37,13 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onBeforeUnmount, onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { host } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { pleaseLogin } from '@/scripts/please-login.js'; -import { host } from '@@/js/config.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; @@ -80,7 +80,7 @@ function onFollowChange(user: Misskey.entities.UserDetailed) { } async function onClick() { - pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` }); + pleaseLogin({ openOnRemote: { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` } }); wait.value = true; @@ -91,7 +91,10 @@ async function onClick() { text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }), }); - if (canceled) return; + if (canceled) { + wait.value = false; + return; + } await misskeyApi('following/delete', { userId: props.user.id, @@ -125,7 +128,10 @@ async function onClick() { }); hasPendingFollowRequestFromYou.value = true; - if ($i == null) return; + if ($i == null) { + wait.value = false; + return; + } claimAchievement('following1'); @@ -165,12 +171,12 @@ onBeforeUnmount(() => { position: relative; display: inline-block; font-weight: bold; - color: var(--fgOnWhite); - border: solid 1px var(--accent); + color: var(--MI_THEME-fgOnWhite); + border: solid 1px var(--MI_THEME-accent); padding: 0; height: 31px; font-size: 16px; - border-radius: var(--radius-xl); + border-radius: var(--MI-radius-xl); background: #fff; &.full { @@ -201,17 +207,17 @@ onBeforeUnmount(() => { } &.active { - color: var(--fgOnAccent); - background: var(--accent); + color: var(--MI_THEME-fgOnAccent); + background: var(--MI_THEME-accent); &:hover { - background: var(--accentLighten); - border-color: var(--accentLighten); + background: var(--MI_THEME-accentLighten); + border-color: var(--MI_THEME-accentLighten); } &:active { - background: var(--accentDarken); - border-color: var(--accentDarken); + background: var(--MI_THEME-accentDarken); + border-color: var(--MI_THEME-accentDarken); } } diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue index 9360594236..ecb6cf882b 100644 --- a/packages/frontend/src/components/MkFormDialog.file.vue +++ b/packages/frontend/src/components/MkFormDialog.file.vue @@ -66,6 +66,6 @@ function selectButton(ev: MouseEvent) { <style module> .fileNotSelected { font-weight: 700; - color: var(--infoWarnFg); + color: var(--MI_THEME-infoWarnFg); } </style> diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index 124f114111..a639eae208 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only @click="cancel()" @ok="ok()" @close="cancel()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header> {{ title }} diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue index 1e88d59d8e..f409f6ce50 100644 --- a/packages/frontend/src/components/MkFormFooter.vue +++ b/packages/frontend/src/components/MkFormFooter.vue @@ -36,7 +36,7 @@ const props = defineProps<{ } .text { - color: var(--warn); + color: var(--MI_THEME-warn); font-size: 90%; animation: modified-blink 2s infinite; } diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue new file mode 100644 index 0000000000..8b1c56fca4 --- /dev/null +++ b/packages/frontend/src/components/MkFukidashi.vue @@ -0,0 +1,100 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + :class="[ + $style.root, + tail === 'left' ? $style.left : $style.right, + negativeMargin === true && $style.negativeMargin, + shadow === true && $style.shadow, + ]" +> + <div :class="$style.bg"> + <svg v-if="tail !== 'none'" :class="$style.tail" version="1.1" viewBox="0 0 14.597 14.58" xmlns="http://www.w3.org/2000/svg"> + <g transform="translate(-173.71 -87.184)"> + <path d="m188.19 87.657c-1.469 2.3218-3.9315 3.8312-6.667 4.0865-2.2309-1.7379-4.9781-2.6816-7.8061-2.6815h-5.1e-4v12.702h12.702v-5.1e-4c2e-5 -1.9998-0.47213-3.9713-1.378-5.754 2.0709-1.6834 3.2732-4.2102 3.273-6.8791-6e-5 -0.49375-0.0413-0.98662-0.1235-1.4735z" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-width=".33225" style="paint-order:stroke fill markers"/> + </g> + </svg> + <div :class="$style.content"> + <slot></slot> + </div> + </div> +</div> +</template> + +<script setup lang="ts"> +withDefaults(defineProps<{ + tail?: 'left' | 'right' | 'none'; + negativeMargin?: boolean; + shadow?: boolean; +}>(), { + tail: 'right', + negativeMargin: false, + shadow: false, +}); +</script> + +<style module lang="scss"> +.root { + --fukidashi-radius: var(--MI-radius); + --fukidashi-bg: var(--MI_THEME-panel); + + position: relative; + display: inline-block; + min-height: calc(var(--fukidashi-radius) * 2); + padding-top: calc(var(--fukidashi-radius) * .13); + + &.shadow { + filter: drop-shadow(0 4px 32px var(--MI_THEME-shadow)); + } + + &.left { + padding-left: calc(var(--fukidashi-radius) * .13); + + &.negativeMargin { + margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1); + } + } + + &.right { + padding-right: calc(var(--fukidashi-radius) * .13); + + &.negativeMargin { + margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1); + } + } +} + +.bg { + width: 100%; + height: 100%; + background: var(--fukidashi-bg); + border-radius: var(--fukidashi-radius); +} + +.content { + position: relative; + padding: 8px 12px; +} + +.tail { + position: absolute; + top: 0; + display: block; + width: calc(var(--fukidashi-radius) * 1.13); + height: auto; + fill: var(--fukidashi-bg); +} + +.left .tail { + left: 0; + transform: rotateY(180deg); +} + +.right .tail { + right: 0; +} +</style> diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue index 2bb5b8762a..22f8355acf 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.vue +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -75,7 +75,7 @@ function leaveHover(): void { &:hover { text-decoration: none; - color: var(--accent); + color: var(--MI_THEME-accent); > .thumbnail { transform: scale(1.1); diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue index 54c7585f18..6cdab2479e 100644 --- a/packages/frontend/src/components/MkGoogle.vue +++ b/packages/frontend/src/components/MkGoogle.vue @@ -41,8 +41,8 @@ const search = () => { width: 100%; height: 40px; font-size: 16px; - border: solid 1px var(--divider); - border-radius: var(--radius-xs) 0 0 var(--radius-xs); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs) 0 0 var(--MI-radius-xs); -webkit-appearance: textfield; } @@ -50,9 +50,9 @@ const search = () => { flex-shrink: 0; margin: 0; padding: 0 16px; - border: solid 1px var(--divider); + border: solid 1px var(--MI_THEME-divider); border-left: none; - border-radius: 0 var(--radius-xs) var(--radius-xs) 0; + border-radius: 0 var(--MI-radius-xs) var(--MI-radius-xs) 0; &:active { box-shadow: 0 2px 4px rgba(#000, 0.15) inset; diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue index 46c2db4b1f..bdc38f5142 100644 --- a/packages/frontend/src/components/MkInfo.vue +++ b/packages/frontend/src/components/MkInfo.vue @@ -36,15 +36,15 @@ function close() { align-items: center; padding: 12px 14px; font-size: 90%; - background: color-mix(in srgb, var(--infoBg) 65%, transparent); - color: var(--infoFg); - border-radius: var(--radius); + background: color-mix(in srgb, var(--MI_THEME-infoBg) 65%, transparent); + color: var(--MI_THEME-infoFg); + border-radius: var(--MI-radius); white-space: pre-wrap; z-index: 1; &.warn { - background: color-mix(in srgb, var(--infoWarnBg) 65%, transparent); - color: var(--infoWarnFg); + background: color-mix(in srgb, var(--MI_THEME-infoWarnBg) 65%, transparent); + color: var(--MI_THEME-infoWarnFg); } } diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 42e1146e27..ec299dce36 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; +import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs, InputHTMLAttributes } from 'vue'; import { debounce } from 'throttle-debounce'; import MkButton from '@/components/MkButton.vue'; import { useInterval } from '@@/js/use-interval.js'; @@ -53,7 +53,7 @@ import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js'; const props = defineProps<{ modelValue: string | number | null; - type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local'; + type?: InputHTMLAttributes['type']; required?: boolean; readonly?: boolean; disabled?: boolean; @@ -64,8 +64,8 @@ const props = defineProps<{ mfmAutocomplete?: boolean | SuggestionType[], autocapitalize?: string; spellcheck?: boolean; - inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal'; - step?: any; + inputmode?: InputHTMLAttributes['inputmode']; + step?: InputHTMLAttributes['step']; datalist?: string[]; min?: number; max?: number | string; @@ -199,7 +199,7 @@ defineExpose({ .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -216,8 +216,8 @@ defineExpose({ &.focused { > .inputCore { - border-color: var(--accent) !important; - //box-shadow: 0 0 0 4px var(--focus); + border-color: var(--MI_THEME-accent) !important; + //box-shadow: 0 0 0 4px var(--MI_THEME-focus); } } @@ -242,17 +242,17 @@ defineExpose({ font: inherit; font-weight: normal; font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + color: var(--MI_THEME-fg); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); outline: none; box-shadow: none; box-sizing: border-box; transition: border-color 0.1s ease-out; &:hover { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue index 10b390e7f9..b063b82b17 100644 --- a/packages/frontend/src/components/MkInstanceCardMini.vue +++ b/packages/frontend/src/components/MkInstanceCardMini.vue @@ -46,15 +46,15 @@ function getInstanceIcon(instance): string { display: flex; align-items: center; padding: 16px; - background: var(--panel); - border-radius: var(--radius-sm); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); > :global(.icon) { display: block; width: ($bodyTitleHieght + $bodyInfoHieght); height: ($bodyTitleHieght + $bodyInfoHieght); object-fit: cover; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); margin-right: 10px; } @@ -62,7 +62,7 @@ function getInstanceIcon(instance): string { flex: 1; overflow: hidden; font-size: 0.9em; - color: var(--fg); + color: var(--MI_THEME-fg); padding-right: 8px; > :global(.host) { @@ -109,7 +109,7 @@ function getInstanceIcon(instance): string { } &:global(.gray) { - --c: var(--bg); + --c: var(--MI_THEME-bg); background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); background-size: 16px 16px; } diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index d74c885041..8ccbf61e48 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -121,7 +121,7 @@ function createDoughnut(chartEl, tooltip, data) { labels: data.map(x => x.name), datasets: [{ backgroundColor: data.map(x => x.color), - borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'), + borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'), borderWidth: 2, hoverOffset: 0, data: data.map(x => x.value), @@ -256,8 +256,8 @@ onMounted(() => { flex: 1; min-width: 0; position: relative; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); padding: 24px; max-height: 300px; diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index 46d42248d3..2a8d5c9f71 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -46,7 +46,7 @@ $height: 2ex; display: flex; align-items: center; height: $height; - border-radius: var(--radius-xs) 0 0 var(--radius-xs); + border-radius: var(--MI-radius-xs) 0 0 var(--MI-radius-xs); overflow: clip; color: #fff; text-shadow: /* .866 ≈ sin(60deg) */ diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue index 4aee64f78e..1a71f6574f 100644 --- a/packages/frontend/src/components/MkInviteCode.vue +++ b/packages/frontend/src/components/MkInviteCode.vue @@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ invite.code }}</template> <template #suffix> <span v-if="invite.used">{{ i18n.ts.used }}</span> - <span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span> - <span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span> + <span v-else-if="isExpired" style="color: var(--MI_THEME-error)">{{ i18n.ts.expired }}</span> + <span v-else style="color: var(--MI_THEME-success)">{{ i18n.ts.unused }}</span> </template> <template #footer> <div class="_buttons"> diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index e0880ec3e7..c7af75e2e7 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -12,13 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="icon" :class="item.icon"></i> <div class="text">{{ item.text }}</div> <span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span> - <span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span> + <span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span> </button> <MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()"> <i class="icon" :class="item.icon"></i> <div class="text">{{ item.text }}</div> <span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span> - <span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span> + <span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span> </MkA> </template> </div> @@ -77,12 +77,12 @@ function close() { overflow: auto; overscroll-behavior: contain; text-align: left; - border-radius: var(--radius-md); + border-radius: var(--MI-radius-md); &.asDrawer { width: 100%; padding: 16px 16px max(env(safe-area-inset-bottom, 0px), 16px) 16px; - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); border-bottom-right-radius: 0; border-bottom-left-radius: 0; text-align: center; @@ -100,13 +100,13 @@ function close() { justify-content: center; vertical-align: bottom; height: 100px; - border-radius: var(--radius); + border-radius: var(--MI-radius); padding: 10px; box-sizing: border-box; &:hover { - color: var(--accent); - background: var(--accentedBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-accentedBg); text-decoration: none; } @@ -137,9 +137,8 @@ function close() { position: absolute; top: 32px; left: 32px; - color: var(--indicator); + color: var(--MI_THEME-indicator); font-size: 8px; - animation: global-blink 1s infinite; @media (max-width: 500px) { top: 16px; diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index 2d7cde1af2..10450fb621 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -394,8 +394,8 @@ onDeactivated(() => { .audioContainer { container-type: inline-size; position: relative; - border: .5px solid var(--divider); - border-radius: var(--radius); + border: .5px solid var(--MI_THEME-divider); + border-radius: var(--MI-radius); overflow: clip; &:focus-visible { @@ -415,7 +415,7 @@ onDeactivated(() => { height: 100%; pointer-events: none; border-radius: inherit; - box-shadow: inset 0 0 0 4px var(--warn); + box-shadow: inset 0 0 0 4px var(--MI_THEME-warn); } } @@ -457,12 +457,12 @@ onDeactivated(() => { .controlButton { padding: 6px; - border-radius: calc(var(--radius) / 2); + border-radius: calc(var(--MI-radius) / 2); font-size: 1.05rem; &:hover { - color: var(--accent); - background-color: var(--accentedBg); + color: var(--MI_THEME-accent); + background-color: var(--MI_THEME-accentedBg); } &:focus-visible { diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 77a86ff2fb..56048a33d8 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -53,7 +53,7 @@ async function show() { <style lang="scss" module> .root { width: 100%; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); margin-top: 4px; overflow: clip; } @@ -68,7 +68,6 @@ async function show() { } .download { - background: var(--noteAttachedFile); } .sensitive { @@ -77,7 +76,7 @@ async function show() { } .audio { - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); overflow: clip; } </style> diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 02c054956c..4aca7256a5 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.indicators"> <div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div> <div v-if="image.comment" :class="$style.indicator">ALT</div> - <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> + <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> <div v-if="!image.comment" :class="$style.indicator" title="Image lacks descriptive text"><i class="ph-pencil-simple ph-bold ph-lg-off"></i></div> </div> <button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button> @@ -166,7 +166,7 @@ function showMenu(ev: MouseEvent) { height: 100%; pointer-events: none; border-radius: inherit; - box-shadow: inset 0 0 0 4px var(--warn); + box-shadow: inset 0 0 0 4px var(--MI_THEME-warn); } } @@ -186,9 +186,9 @@ function showMenu(ev: MouseEvent) { .hide { display: block; position: absolute; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); background-color: black; - color: var(--accentLighten); + color: var(--MI_THEME-accentLighten); font-size: 12px; opacity: .5; padding: 5px 8px; @@ -207,28 +207,28 @@ function showMenu(ev: MouseEvent) { .visible { position: relative; - //box-shadow: 0 0 0 1px var(--divider) inset; - background: var(--bg); + //box-shadow: 0 0 0 1px var(--MI_THEME-divider) inset; + background: var(--MI_THEME-bg); background-size: 16px 16px; } html[data-color-scheme=dark] .visible { --c: rgb(255 255 255 / 2%); - background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); + background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%); } html[data-color-scheme=light] .visible { --c: rgb(0 0 0 / 2%); - background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); + background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%); } .menu { display: block; position: absolute; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); background-color: rgba(0, 0, 0, 0.3); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); color: #fff; font-size: 0.8em; width: 28px; @@ -259,10 +259,10 @@ html[data-color-scheme=light] .visible { } .indicator { - /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */ + /* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */ background-color: black; - border-radius: var(--radius-sm); - color: var(--accentLighten); + border-radius: var(--MI-radius-sm); + color: var(--MI_THEME-accentLighten); display: inline-block; font-weight: bold; font-size: 0.8em; diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 11529c186f..487cf509d6 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -339,19 +339,19 @@ defineExpose({ .media { overflow: hidden; // clipにするとバグる - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } :global(.pswp) { --pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important; - --pswp-bg: var(--modalBg) !important; + --pswp-bg: var(--MI_THEME-modalBg) !important; } </style> <style lang="scss"> .pswp__bg { - background: var(--modalBg); - backdrop-filter: var(--modalBgFilter); + background: var(--MI_THEME-modalBg); + backdrop-filter: var(--MI-modalBgFilter); } .pswp__alt-text-container { @@ -369,14 +369,14 @@ defineExpose({ } .pswp__alt-text { - color: var(--fg); + color: var(--MI_THEME-fg); margin: 0 auto; text-align: center; - padding: var(--margin); - border-radius: var(--radius); + padding: var(--MI-margin); + border-radius: var(--MI-radius); max-height: 8em; overflow-y: auto; - text-shadow: var(--bg) 0 0 10px, var(--bg) 0 0 3px, var(--bg) 0 0 3px; + text-shadow: var(--MI_THEME-bg) 0 0 10px, var(--MI_THEME-bg) 0 0 3px, var(--MI_THEME-bg) 0 0 3px; white-space: pre-line; } </style> diff --git a/packages/frontend/src/components/MkMediaRange.vue b/packages/frontend/src/components/MkMediaRange.vue index 86ed8ba2cf..df7505b0c3 100644 --- a/packages/frontend/src/components/MkMediaRange.vue +++ b/packages/frontend/src/components/MkMediaRange.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- Media系専用のinput range --> <template> -<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'"> +<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--MI_THEME-scrollbarHandle);'"> <div :class="$style.controlsSeekbar"> <progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress> <input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/> @@ -48,7 +48,7 @@ const modelValue = computed({ background: transparent; border: 0; border-radius: 26px; - color: var(--accent); + color: var(--MI_THEME-accent); display: block; height: 19px; margin: 0; diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 0502bdd401..04f8e9f8d8 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i> <div :class="$style.indicators"> <div v-if="video.comment" :class="$style.indicator">ALT</div> - <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> + <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> </div> </div> @@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i> <div :class="$style.indicators"> <div v-if="video.comment" :class="$style.indicator">ALT</div> - <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> + <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> </div> <div :class="$style.videoControls" @click.self="togglePlayPause"> <div :class="[$style.controlsChild, $style.controlsLeft]"> @@ -121,7 +121,7 @@ import { hms } from '@/filters/hms.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { isFullscreenNotSupported } from '@/scripts/device-kind.js'; +import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js'; import hasAudio from '@/scripts/media-has-audio.js'; import MkMediaRange from '@/components/MkMediaRange.vue'; import { $i, iAmModerator } from '@/account.js'; @@ -337,26 +337,21 @@ function togglePlayPause() { } function toggleFullscreen() { - if (isFullscreenNotSupported && videoEl.value) { - if (isFullscreen.value) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - videoEl.value.webkitExitFullscreen(); - isFullscreen.value = false; - } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - videoEl.value.webkitEnterFullscreen(); - isFullscreen.value = true; - } - } else if (playerEl.value) { - if (isFullscreen.value) { - document.exitFullscreen(); - isFullscreen.value = false; - } else { - playerEl.value.requestFullscreen({ navigationUI: 'hide' }); - isFullscreen.value = true; - } + if (playerEl.value == null || videoEl.value == null) return; + if (isFullscreen.value) { + exitFullscreen({ + videoEl: videoEl.value, + }); + isFullscreen.value = false; + } else { + requestFullscreen({ + videoEl: videoEl.value, + playerEl: playerEl.value, + options: { + navigationUI: 'hide', + }, + }); + isFullscreen.value = true; } } @@ -457,8 +452,10 @@ watch(loop, (to) => { }); watch(hide, (to) => { - if (to && isFullscreen.value) { - document.exitFullscreen(); + if (videoEl.value && to && isFullscreen.value) { + exitFullscreen({ + videoEl: videoEl.value, + }); isFullscreen.value = false; } }); @@ -511,7 +508,7 @@ onDeactivated(() => { height: 100%; pointer-events: none; border-radius: inherit; - box-shadow: inset 0 0 0 4px var(--warn); + box-shadow: inset 0 0 0 4px var(--MI_THEME-warn); } } @@ -526,10 +523,10 @@ onDeactivated(() => { } .indicator { - /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */ + /* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */ background-color: black; border-radius: 6px; - color: var(--accentLighten); + color: var(--MI_THEME-accentLighten); display: inline-block; font-weight: bold; font-size: 0.8em; @@ -539,9 +536,9 @@ onDeactivated(() => { .hide { display: block; position: absolute; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); background-color: black; - color: var(--accentLighten); + color: var(--MI_THEME-accentLighten); font-size: 12px; opacity: .5; padding: 5px 8px; @@ -595,7 +592,7 @@ onDeactivated(() => { opacity: 0; transition: opacity .4s ease-in-out; - background: var(--accent); + background: var(--MI_THEME-accent); color: #fff; padding: 1rem; border-radius: 99rem; @@ -661,12 +658,12 @@ onDeactivated(() => { .controlButton { padding: 6px; - border-radius: calc(var(--radius) / 2); + border-radius: calc(var(--MI-radius) / 2); transition: background-color .2s ease-in-out; font-size: 1.05rem; &:hover { - background-color: var(--accent); + background-color: var(--MI_THEME-accent); } &:focus-visible { @@ -728,10 +725,10 @@ onDeactivated(() => { } .indicator { - /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */ + /* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */ background-color: black; - border-radius: var(--radius-sm); - color: var(--accentLighten); + border-radius: var(--MI-radius-sm); + color: var(--MI_THEME-accentLighten); display: inline-block; font-weight: bold; font-size: 0.8em; diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index de2048b6f2..f64ca4bc77 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="navigationBehavior"> +<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :behavior="navigationBehavior"> <img :class="$style.icon" :src="avatarUrl" alt=""> <span> <span>@{{ username }}</span> @@ -16,7 +16,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { toUnicode } from 'punycode'; import { computed } from 'vue'; -import tinycolor from 'tinycolor2'; import { host as localHost } from '@@/js/config.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; @@ -37,11 +36,7 @@ const isMe = $i && ( `@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase() ); -const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention')); -bg.setAlpha(0.1); -const bgCss = bg.toRgbString(); - -const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages +const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar ? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`) : `/avatar/@${props.username}@${props.host}`, ); @@ -51,12 +46,14 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages .root { display: inline-block; padding: 4px 8px 4px 4px; - border-radius: var(--radius-ellipse); - color: var(--mention); + border-radius: var(--MI-radius-ellipse); + color: var(--MI_THEME-mention); + background: color(from var(--MI_THEME-mention) srgb r g b / 0.1); white-space: nowrap; &.isMe { - color: var(--mentionMe); + color: var(--MI_THEME-mentionMe); + background: color(from var(--MI_THEME-mentionMe) srgb r g b / 0.1); } } @@ -66,7 +63,7 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages object-fit: cover; margin: 0 0.2em 0 0; vertical-align: bottom; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); } .host { diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index fe6df7090c..b0a1b80210 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <div :class="$style.item_content"> <span :class="$style.item_content_text">{{ item.text }}</span> - <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </MkA> <a @@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <div :class="$style.item_content"> <span :class="$style.item_content_text">{{ item.text }}</span> - <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </a> <button @@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/> <div v-if="item.indicate" :class="$style.item_content"> - <span :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </button> <button @@ -161,7 +161,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <div :class="$style.item_content"> <span :class="$style.item_content_text">{{ item.text }}</span> - <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </button> </template> @@ -437,9 +437,11 @@ onBeforeUnmount(() => { &.big:not(.asDrawer) { > .menu { + min-width: 230px; + > .item { padding: 6px 20px; - font-size: 1em; + font-size: 0.95em; line-height: 24px; } } @@ -452,7 +454,7 @@ onBeforeUnmount(() => { > .menu { padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; width: 100%; - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); border-bottom-right-radius: 0; border-bottom-left-radius: 0; @@ -462,7 +464,7 @@ onBeforeUnmount(() => { &::before { width: calc(100% - 24px); - border-radius: var(--radius); + border-radius: var(--MI-radius); } > .icon { @@ -505,7 +507,7 @@ onBeforeUnmount(() => { overflow: hidden; text-overflow: ellipsis; text-decoration: none !important; - color: var(--menuFg, var(--fg)); + color: var(--menuFg, var(--MI_THEME-fg)); &::before { content: ""; @@ -518,14 +520,14 @@ onBeforeUnmount(() => { margin: auto; width: calc(100% - 16px); height: 100%; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } &:focus-visible { outline: none; &:not(:hover):not(:active)::before { - outline: var(--focus) solid 2px; + outline: var(--MI_THEME-focus) solid 2px; outline-offset: -2px; } } @@ -534,19 +536,19 @@ onBeforeUnmount(() => { &:hover, &:focus-visible:active, &:focus-visible.active { - color: var(--menuHoverFg, var(--accent)); + color: var(--menuHoverFg, var(--MI_THEME-accent)); &::before { - background-color: var(--menuHoverBg, var(--accentedBg)); + background-color: var(--menuHoverBg, var(--MI_THEME-accentedBg)); } } &:not(:focus-visible):active, &:not(:focus-visible).active { - color: var(--menuActiveFg, var(--fgOnAccent)); + color: var(--menuActiveFg, var(--MI_THEME-fgOnAccent)); &::before { - background-color: var(--menuActiveBg, var(--accent)); + background-color: var(--menuActiveBg, var(--MI_THEME-accent)); } } } @@ -564,13 +566,13 @@ onBeforeUnmount(() => { } &.radio { - --menuActiveFg: var(--accent); - --menuActiveBg: var(--accentedBg); + --menuActiveFg: var(--MI_THEME-accent); + --menuActiveBg: var(--MI_THEME-accentedBg); } &.parent { - --menuActiveFg: var(--accent); - --menuActiveBg: var(--accentedBg); + --menuActiveFg: var(--MI_THEME-accent); + --menuActiveBg: var(--MI_THEME-accentedBg); } &.label { @@ -635,14 +637,13 @@ onBeforeUnmount(() => { .indicator { display: flex; align-items: center; - color: var(--indicator); + color: var(--MI_THEME-indicator); font-size: 12px; - animation: global-blink 1s infinite; } .divider { margin: 8px 0; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .radioIcon { @@ -652,11 +653,11 @@ onBeforeUnmount(() => { height: 1em; vertical-align: -0.125em; border-radius: 50%; - border: solid 2px var(--divider); - background-color: var(--panel); + border: solid 2px var(--MI_THEME-divider); + background-color: var(--MI_THEME-panel); &.radioChecked { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); &::after { content: ""; @@ -668,7 +669,7 @@ onBeforeUnmount(() => { width: 50%; height: 50%; border-radius: 50%; - background-color: var(--accent); + background-color: var(--MI_THEME-accent); } } } diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue index 1b6f6cef31..7ea585ecc2 100644 --- a/packages/frontend/src/components/MkMiniChart.vue +++ b/packages/frontend/src/components/MkMiniChart.vue @@ -48,7 +48,7 @@ const polygonPoints = ref(''); const headX = ref<number | null>(null); const headY = ref<number | null>(null); const clock = ref<number | null>(null); -const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent')); +const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent')); const color = accent.toRgbString(); function draw(): void { diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index f26959888b..f06cfffee4 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -26,11 +26,11 @@ import { onMounted, onUnmounted, shallowRef, ref } from 'vue'; import MkModal from './MkModal.vue'; const props = withDefaults(defineProps<{ - withOkButton: boolean; - withCloseButton: boolean; - okButtonDisabled: boolean; - width: number; - height: number; + withOkButton?: boolean; + withCloseButton?: boolean; + okButtonDisabled?: boolean; + width?: number; + height?: number; }>(), { withOkButton: false, withCloseButton: true, @@ -90,12 +90,12 @@ defineExpose({ display: flex; flex-direction: column; contain: content; - border-radius: var(--radius); + border-radius: var(--MI-radius); --root-margin: 24px; - --headerHeight: 46px; - --headerHeightNarrow: 42px; + --MI_THEME-headerHeight: 46px; + --MI_THEME-headerHeightNarrow: 42px; @media (max-width: 500px) { --root-margin: 16px; @@ -105,24 +105,24 @@ defineExpose({ .header { display: flex; flex-shrink: 0; - background: var(--windowHeader); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + background: var(--MI_THEME-windowHeader); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } .headerButton { - height: var(--headerHeight); - width: var(--headerHeight); + height: var(--MI_THEME-headerHeight); + width: var(--MI_THEME-headerHeight); @media (max-width: 500px) { - height: var(--headerHeightNarrow); - width: var(--headerHeightNarrow); + height: var(--MI_THEME-headerHeightNarrow); + width: var(--MI_THEME-headerHeightNarrow); } } .title { flex: 1; - line-height: var(--headerHeight); + line-height: var(--MI_THEME-headerHeight); padding-left: 32px; font-weight: bold; white-space: nowrap; @@ -131,7 +131,7 @@ defineExpose({ pointer-events: none; @media (max-width: 500px) { - line-height: var(--headerHeightNarrow); + line-height: var(--MI_THEME-headerHeightNarrow); padding-left: 16px; } } @@ -143,7 +143,7 @@ defineExpose({ .body { flex: 1; overflow: auto; - background: var(--panel); + background: var(--MI_THEME-panel); container-type: size; } </style> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 323f6e994e..39f4806f0c 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-show="!isDeleted" ref="rootEl" v-hotkey="keymap" - :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]" + :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]" :tabindex="isDeleted ? '-1' : '0'" > <div v-if="appearNote.reply && inReplyToCollapsed" :class="$style.collapsedInReplyTo"> @@ -132,7 +132,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" :class="$style.footerButton" class="_button" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @click.stop @mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()" > @@ -156,8 +156,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> - <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i> + <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -201,6 +201,9 @@ import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } fro import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; +import { shouldCollapsed } from '@@/js/collapsed.js'; +import { host } from '@@/js/config.js'; +import type { MenuItem } from '@/types/menu.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; @@ -215,6 +218,7 @@ import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkButton from '@/components/MkButton.vue'; import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; +import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; import number from '@/filters/number.js'; import * as os from '@/os.js'; @@ -233,13 +237,10 @@ import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; -import type { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { useRouter } from '@/router/supplier.js'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; -import { shouldCollapsed } from '@@/js/collapsed.js'; -import { host } from '@@/js/config.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { type Keymap } from '@/scripts/hotkey.js'; import { focusPrev, focusNext } from '@/scripts/focus.js'; @@ -264,6 +265,7 @@ const emit = defineEmits<{ const router = useRouter(); const inTimeline = inject<boolean>('inTimeline', false); +const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true)); const inChannel = inject('inChannel', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); @@ -343,15 +345,18 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute'; */ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' { - if (mutedWords == null) return false; - - if (checkWordMute(noteToCheck, $i, mutedWords)) return true; - if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; - if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; + if (mutedWords != null) { + if (checkWordMute(noteToCheck, $i, mutedWords)) return true; + if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; + if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; + } if (checkOnly) return false; - if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute'; + if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) { + return 'sensitiveMute'; + } + return false; } @@ -514,7 +519,7 @@ function boostVisibility() { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); renoting = true; @@ -564,7 +569,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (props.mock) { return; @@ -625,7 +630,7 @@ function quote() { } function reply(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); if (props.mock) { return; } @@ -638,7 +643,7 @@ function reply(): void { } function like(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); if (props.mock) { @@ -660,7 +665,7 @@ function like(): void { } function react(viaKeyboard = false): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -808,15 +813,24 @@ function showRenoteMenu(): void { }; } + const renoteDetailsMenu: MenuItem = { + type: 'link', + text: i18n.ts.renoteDetails, + icon: 'ti ti-info-circle', + to: notePage(note.value), + }; + if (isMyRenote) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); os.popupMenu([ + renoteDetailsMenu, getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getUnrenote(), ], renoteTime.value); } else { os.popupMenu([ + renoteDetailsMenu, getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote), @@ -877,14 +891,6 @@ function emitUpdReaction(emoji: string, delta: number) { overflow: clip; contain: content; - // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、 - // 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう - // ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、 - // 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる - // 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?) - //content-visibility: auto; - //contain-intrinsic-size: 0 128px; - &:focus-visible { outline: none; @@ -901,8 +907,8 @@ function emitUpdReaction(emoji: string, delta: number) { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: dashed 2px var(--focus); - border-radius: var(--radius); + border: dashed 2px var(--MI_THEME-focus); + border-radius: var(--MI-radius); box-sizing: border-box; } } @@ -929,9 +935,9 @@ function emitUpdReaction(emoji: string, delta: number) { right: 12px; padding: 0 4px; margin-bottom: 0 !important; - background: var(--popup); - border-radius: var(--radius-sm); - box-shadow: 0px 4px 32px var(--shadow); + background: var(--MI_THEME-popup); + border-radius: var(--MI-radius-sm); + box-shadow: 0px 4px 32px var(--MI_THEME-shadow); } .footerButton { @@ -950,6 +956,11 @@ function emitUpdReaction(emoji: string, delta: number) { } } +.skipRender { + content-visibility: auto; + contain-intrinsic-size: 0 150px; +} + .tip { display: flex; align-items: center; @@ -976,7 +987,7 @@ function emitUpdReaction(emoji: string, delta: number) { padding: 16px 32px 8px 32px; line-height: 28px; white-space: pre; - color: var(--renote); + color: var(--MI_THEME-renote); & + .article { padding-top: 8px; @@ -1070,7 +1081,7 @@ function emitUpdReaction(emoji: string, delta: number) { left: 8px; width: 5px; height: calc(100% - 16px); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); pointer-events: none; } @@ -1078,10 +1089,10 @@ function emitUpdReaction(emoji: string, delta: number) { flex-shrink: 0; display: block !important; margin: 0 14px 0 0; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); position: sticky !important; - top: calc(22px + var(--stickyTop, 0px)); + top: calc(22px + var(--MI-stickyTop, 0px)); left: 0; } @@ -1101,15 +1112,15 @@ function emitUpdReaction(emoji: string, delta: number) { width: 100%; margin-top: 14px; position: sticky; - bottom: calc(var(--stickyBottom, 0px) - 100px); + bottom: calc(var(--MI-stickyBottom, 0px) - 100px); } .showLessLabel { display: inline-block; - background: var(--popup); + background: var(--MI_THEME-popup); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } @@ -1127,19 +1138,19 @@ function emitUpdReaction(emoji: string, delta: number) { z-index: 2; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + //background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); &:hover > .collapsedLabel { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } } .collapsedLabel { display: inline-block; - background: var(--panel); + background: var(--MI_THEME-panel); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } @@ -1149,13 +1160,13 @@ function emitUpdReaction(emoji: string, delta: number) { } .replyIcon { - color: var(--accent); + color: var(--MI_THEME-accent); margin-right: 0.5em; } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -1178,8 +1189,8 @@ function emitUpdReaction(emoji: string, delta: number) { .quoteNote { padding: 16px; - border: dashed 1px var(--renote); - border-radius: var(--radius-sm); + border: dashed 1px var(--MI_THEME-renote); + border-radius: var(--MI-radius-sm); overflow: clip; } @@ -1202,7 +1213,7 @@ function emitUpdReaction(emoji: string, delta: number) { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -1277,7 +1288,7 @@ function emitUpdReaction(emoji: string, delta: number) { margin: 0 10px 0 0; width: 46px; height: 46px; - top: calc(14px + var(--stickyTop, 0px)); + top: calc(14px + var(--MI-stickyTop, 0px)); } } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 5acb18c871..a537d8afb9 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -63,7 +63,14 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> </div> </div> - <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> + <div :class="$style.noteHeaderUsernameAndBadgeRoles"> + <div :class="$style.noteHeaderUsername"> + <MkAcct :user="appearNote.user"/> + </div> + <div v-if="appearNote.user.badgeRoles" :class="$style.noteHeaderBadgeRoles"> + <img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/> + </div> + </div> <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> </div> </header> @@ -135,7 +142,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" class="_button" :class="$style.noteFooterButton" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @mousedown.prevent="renoted ? undoRenote() : boostVisibility()" > <i class="ti ti-repeat"></i> @@ -157,8 +164,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> - <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i> + <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -236,6 +243,7 @@ import { computed, inject, onMounted, provide, ref, shallowRef, watch } from 'vu import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; +import { host } from '@@/js/config.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; @@ -259,10 +267,8 @@ import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { host } from '@@/js/config.js'; import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; - import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; @@ -507,7 +513,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); renoting = true; @@ -553,7 +559,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -611,7 +617,7 @@ function quote() { } function reply(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); os.post({ reply: appearNote.value, @@ -622,7 +628,7 @@ function reply(): void { } function react(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -659,7 +665,7 @@ function react(): void { } function like(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -741,7 +747,7 @@ async function clip(): Promise<void> { function showRenoteMenu(): void { if (!isMyRenote) return; - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); os.popupMenu([{ text: i18n.ts.unrenote, icon: 'ti ti-trash', @@ -843,8 +849,8 @@ function animatedMFM() { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: dashed 2px var(--focus); - border-radius: var(--radius); + border: dashed 2px var(--MI_THEME-focus); + border-radius: var(--MI-radius); box-sizing: border-box; } } @@ -874,7 +880,7 @@ function animatedMFM() { padding: 16px 32px 8px 32px; line-height: 28px; white-space: pre; - color: var(--renote); + color: var(--MI_THEME-renote); } .renoteAvatar { @@ -883,7 +889,7 @@ function animatedMFM() { width: 28px; height: 28px; margin: 0 8px 0 0; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .renoteText { @@ -932,8 +938,8 @@ function animatedMFM() { .noteHeaderAvatar { display: block; flex-shrink: 0; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); } .noteHeaderBody { @@ -956,16 +962,21 @@ function animatedMFM() { padding: 4px 6px; font-size: 80%; line-height: 1; - border: solid 0.5px var(--divider); - border-radius: var(--radius-xs); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs); } .noteHeaderInfo { float: right; } +.noteHeaderUsernameAndBadgeRoles { + display: flex; +} + .noteHeaderUsername { margin-bottom: 2px; + margin-right: 0.5em; line-height: 1.3; word-wrap: anywhere; } @@ -974,6 +985,19 @@ function animatedMFM() { margin-top: 5px; } +.noteHeaderBadgeRoles { + margin: 0 .5em 0 0; +} + +.noteHeaderBadgeRole { + height: 1.3em; + vertical-align: -20%; + + & + .noteHeaderBadgeRole { + margin-left: 0.2em; + } +} + .noteContent { container-type: inline-size; overflow-wrap: break-word; @@ -989,19 +1013,19 @@ function animatedMFM() { } .noteReplyTarget { - color: var(--accent); + color: var(--MI_THEME-accent); margin-right: 0.5em; } .rn { margin-left: 4px; font-style: oblique; - color: var(--renote); + color: var(--MI_THEME-renote); } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -1016,8 +1040,8 @@ function animatedMFM() { .quoteNote { padding: 16px; - border: dashed 1px var(--renote); - border-radius: var(--radius-sm); + border: dashed 1px var(--MI_THEME-renote); + border-radius: var(--MI-radius-sm); overflow: clip; } @@ -1042,7 +1066,7 @@ function animatedMFM() { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -1052,17 +1076,17 @@ function animatedMFM() { opacity: 0.7; &.reacted { - color: var(--accent); + color: var(--MI_THEME-accent); } } .reply:not(:first-child) { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .tabs { - border-top: solid 0.5px var(--divider); - border-bottom: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); display: flex; } @@ -1074,7 +1098,7 @@ function animatedMFM() { } .tabActive { - border-bottom: solid 2px var(--accent); + border-bottom: solid 2px var(--MI_THEME-accent); } .tab_renotes { @@ -1094,12 +1118,12 @@ function animatedMFM() { .reactionTab { padding: 4px 6px; - border: solid 1px var(--divider); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-sm); } .reactionTabActive { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); } @container (max-width: 500px) { diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 2c69048ec5..3b15242685 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -5,18 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <header :class="$style.root"> - <component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.5" style="min-width: 0;"> - <div style="display: flex; white-space: nowrap; align-items: baseline;"> - <div v-if="mock" :class="$style.name"> - <MkUserName :user="note.user"/> - </div> - <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> - <MkUserName :user="note.user"/> - </MkA> - <div v-if="note.user.isBot" :class="$style.isBot">bot</div> - <div :class="$style.username"><MkAcct :user="note.user"/></div> - </div> - </component> + <div v-if="mock" :class="$style.name"> + <MkUserName :user="note.user"/> + </div> + <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <MkUserName :user="note.user"/> + </MkA> + <div v-if="note.user.isBot" :class="$style.isBot">bot</div> + <div :class="$style.username"><MkAcct :user="note.user"/></div> <div v-if="note.user.badgeRoles" :class="$style.badgeRoles"> <img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/> </div> @@ -98,8 +94,8 @@ const mock = inject<boolean>('mock', false); margin: 0 .5em 0 0; padding: 1px 6px; font-size: 80%; - border: solid 0.5px var(--divider); - border-radius: var(--radius-xs); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs); } .username { diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue index 7ccc2c0320..4f907231bb 100644 --- a/packages/frontend/src/components/MkNotePreview.vue +++ b/packages/frontend/src/components/MkNotePreview.vue @@ -56,7 +56,7 @@ const props = defineProps<{ margin: 0 10px 0 0 !important; width: 40px !important; height: 40px !important; - border-radius: var(--radius-sm) !important; + border-radius: var(--MI-radius-sm) !important; pointer-events: none !important; } diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index a369d84783..9ef45a0ec3 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -95,8 +95,8 @@ watch(() => props.expandAllCws, (expandAllCws) => { } .button{ - margin-right: var(--margin); - margin-bottom: var(--margin); + margin-right: var(--MI-margin); + margin-bottom: var(--MI-margin); } .avatar { @@ -105,9 +105,9 @@ watch(() => props.expandAllCws, (expandAllCws) => { margin: 0 10px 0 0; width: 34px; height: 34px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); position: sticky !important; - top: calc(16px + var(--stickyTop, 0px)); + top: calc(16px + var(--MI-stickyTop, 0px)); left: 0; } diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 45276839ad..3523babe46 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" class="_button" :class="$style.noteFooterButton" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @mousedown="renoted ? undoRenote() : boostVisibility()" > <i class="ph-rocket-launch ph-bold ph-lg"></i> @@ -98,7 +98,8 @@ import { $i } from '@/account.js'; import { userPage } from '@/filters/user.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { defaultStore } from '@/store.js'; -import { pleaseLogin } from '@/scripts/please-login.js'; +import { host } from '@@/js/config.js'; +import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { reactionPicker } from '@/scripts/reaction-picker.js'; @@ -145,6 +146,11 @@ const isRenote = ( props.note.poll == null ); +const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ + type: 'lookup', + url: `https://${host}/notes/${appearNote.value.id}`, +})); + async function addReplyTo(replyNote: Misskey.entities.Note) { replies.value.unshift(replyNote); appearNote.value.repliesCount += 1; @@ -182,7 +188,7 @@ function focus() { } function reply(viaKeyboard = false): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); os.post({ reply: props.note, @@ -194,7 +200,7 @@ function reply(viaKeyboard = false): void { } function react(viaKeyboard = false): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); if (props.note.reactionAcceptance === 'likeOnly') { @@ -228,7 +234,7 @@ function react(viaKeyboard = false): void { } function like(): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -288,7 +294,7 @@ function boostVisibility() { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -332,7 +338,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -438,7 +444,7 @@ if (props.detail) { left: 8px; width: 5px; height: calc(100% - 8px); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); pointer-events: none; } @@ -448,7 +454,7 @@ if (props.detail) { margin: 0 8px 0 0; width: 38px; height: 38px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .body { @@ -475,7 +481,7 @@ if (props.detail) { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -493,7 +499,7 @@ if (props.detail) { opacity: 0.7; &.reacted { - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -510,7 +516,7 @@ if (props.detail) { } .reply, .more { - border-left: solid 0.5px var(--divider); + border-left: solid 0.5px var(--MI_THEME-divider); margin-top: 10px; } @@ -531,8 +537,8 @@ if (props.detail) { .muted { text-align: center; padding: 8px !important; - border: 1px solid var(--divider); + border: 1px solid var(--MI_THEME-divider); margin: 8px 8px 0 8px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } </style> diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index 15173fbd99..b13df2813b 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -61,20 +61,20 @@ defineExpose({ <style lang="scss" module> .root { &.noGap { - border-radius: var(--radius); + border-radius: var(--MI-radius); > .notes { - background: color-mix(in srgb, var(--panel) 65%, transparent); + background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent); } } &:not(.noGap) { > .notes { - background: var(--bg); + background: var(--MI_THEME-bg); .note { - background: color-mix(in srgb, var(--panel) 65%, transparent); - border-radius: var(--radius); + background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent); + border-radius: var(--MI-radius); } } } diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index f713d46137..4620b966af 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -7,13 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.root"> <div :class="$style.head"> <MkAvatar v-if="['pollEnded', 'note', 'edited', 'scheduledNotePosted'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/> - <MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'scheduledNoteFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> - <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> + <MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'scheduledNoteFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> + <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/> <MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/> - <MkAvatar v-else-if="notification.type === 'exportCompleted'" :class="$style.icon" :user="$i" link preview/> <img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/> <div :class="[$style.subIcon, { @@ -27,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_pollEnded]: notification.type === 'pollEnded', [$style.t_achievementEarned]: notification.type === 'achievementEarned', [$style.t_exportCompleted]: notification.type === 'exportCompleted', + [$style.t_login]: notification.type === 'login', [$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null, [$style.t_pollEnded]: notification.type === 'edited', [$style.t_roleAssigned]: notification.type === 'scheduledNoteFailed', @@ -43,6 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> <i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i> + <i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i> <template v-else-if="notification.type === 'roleAssigned'"> <img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/> <i v-else class="ti ti-badges"></i> @@ -66,6 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span> <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> + <span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span> <MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> @@ -243,13 +245,16 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) overflow-wrap: break-word; display: flex; contain: content; + content-visibility: auto; + contain-intrinsic-size: 0 100px; --eventFollow: #36aed2; --eventRenote: #36d298; --eventReply: #007aff; - --eventReactionHeart: var(--love); + --eventReactionHeart: var(--MI_THEME-love); --eventReaction: #e99a0b; --eventAchievement: #cb9a11; + --eventLogin: #007aff; --eventOther: #88a6b7; } @@ -277,7 +282,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) width: 80%; height: 80%; font-size: 15px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); color: #fff; } @@ -294,7 +299,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) } .icon_app { - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .subIcon { @@ -305,9 +310,9 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) width: 20px; height: 20px; box-sizing: border-box; - border-radius: var(--radius-full); - background: var(--panel); - box-shadow: 0 0 0 3px var(--panel); + border-radius: var(--MI-radius-full); + background: var(--MI_THEME-panel); + box-shadow: 0 0 0 3px var(--MI_THEME-panel); font-size: 11px; text-align: center; color: #fff; @@ -371,6 +376,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) pointer-events: none; } +.t_login { + padding: 3px; + background: var(--eventLogin); + pointer-events: none; +} + .tail { flex: 1; min-width: 0; @@ -452,9 +463,9 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) width: 20px; height: 20px; box-sizing: border-box; - border-radius: var(--radius-full); - background: var(--panel); - box-shadow: 0 0 0 3px var(--panel); + border-radius: var(--MI-radius-full); + background: var(--MI_THEME-panel); + box-shadow: 0 0 0 3px var(--MI_THEME-panel); font-size: 11px; text-align: center; color: #fff; diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue index 47a9c79e45..d07827d11a 100644 --- a/packages/frontend/src/components/MkNotificationSelectWindow.vue +++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue @@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); -const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any); +const typesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as TypesMap); function ok() { emit('done', { diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index a395734add..51c4ea7ce4 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -111,6 +111,6 @@ defineExpose({ <style lang="scss" module> .list { - background: var(--panel); + background: var(--MI_THEME-panel); } </style> diff --git a/packages/frontend/src/components/MkNumberDiff.vue b/packages/frontend/src/components/MkNumberDiff.vue index 1825cc5405..80c634fdce 100644 --- a/packages/frontend/src/components/MkNumberDiff.vue +++ b/packages/frontend/src/components/MkNumberDiff.vue @@ -24,11 +24,11 @@ const isZero = computed(() => props.value === 0); <style lang="scss" module> .isPlus { - color: var(--success); + color: var(--MI_THEME-success); } .isMinus { - color: var(--error); + color: var(--MI_THEME-error); } .isZero { diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue index 870599aa94..7fa8c23c6c 100644 --- a/packages/frontend/src/components/MkObjectView.value.vue +++ b/packages/frontend/src/components/MkObjectView.value.vue @@ -39,7 +39,7 @@ import number from '@/filters/number.js'; import XValue from '@/components/MkObjectView.value.vue'; const props = defineProps<{ - value: any; + value: unknown; }>(); const collapsed = reactive({}); @@ -50,19 +50,19 @@ if (isObject(props.value)) { } } -function isObject(v): boolean { +function isObject(v: unknown): v is Record<PropertyKey, unknown> { return typeof v === 'object' && !Array.isArray(v) && v !== null; } -function isArray(v): boolean { +function isArray(v: unknown): v is unknown[] { return Array.isArray(v); } -function isEmpty(v): boolean { +function isEmpty(v: unknown): v is Record<PropertyKey, never> | never[] { return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0); } -function collapsable(v): boolean { +function collapsable(v: unknown): boolean { return (isObject(v) || isArray(v)) && !isEmpty(v); } </script> @@ -78,7 +78,7 @@ function collapsable(v): boolean { > .boolean { display: inline; - color: var(--codeBoolean); + color: var(--MI_THEME-codeBoolean); &.true { font-weight: bold; @@ -91,12 +91,12 @@ function collapsable(v): boolean { > .string { display: inline; - color: var(--codeString); + color: var(--MI_THEME-codeString); } > .number { display: inline; - color: var(--codeNumber); + color: var(--MI_THEME-codeNumber); } > .array.empty { @@ -127,7 +127,7 @@ function collapsable(v): boolean { > .toggle { width: 16px; - color: var(--accent); + color: var(--MI_THEME-accent); visibility: hidden; &.visible { diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue index 94cbaf5c91..b978a71c15 100644 --- a/packages/frontend/src/components/MkOmit.vue +++ b/packages/frontend/src/components/MkOmit.vue @@ -47,7 +47,7 @@ onUnmounted(() => { <style lang="scss" module> .content { - --stickyTop: 0px; + --MI-stickyTop: 0px; &.omitted { position: relative; @@ -62,20 +62,20 @@ onUnmounted(() => { left: 0; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + //background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; - background: var(--panel); + background: var(--MI_THEME-panel); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } &:hover { > .fadeLabel { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } } } diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue index 361cc520de..29fdc91a2b 100644 --- a/packages/frontend/src/components/MkPagePreview.vue +++ b/packages/frontend/src/components/MkPagePreview.vue @@ -41,7 +41,7 @@ const props = defineProps<{ .eyeCatchingImageRoot { width: 100%; height: 200px; - border-radius: var(--radius) var(--radius) 0 0; + border-radius: var(--MI-radius) var(--MI-radius) 0 0; overflow: hidden; } </style> @@ -53,7 +53,7 @@ const props = defineProps<{ &:hover { text-decoration: none; - color: var(--accent); + color: var(--MI_THEME-accent); } &:focus-within { @@ -66,22 +66,22 @@ const props = defineProps<{ left: 0; width: 100%; height: 100%; - border-radius: var(--radius); + border-radius: var(--MI-radius); pointer-events: none; - box-shadow: inset 0 0 0 2px var(--focus); + box-shadow: inset 0 0 0 2px var(--MI_THEME-focus); } } > .thumbnail { & + article { - border-radius: 0 0 var(--radius) var(--radius); + border-radius: 0 0 var(--MI-radius) var(--MI-radius); } } > article { - background-color: var(--panel); + background-color: var(--MI_THEME-panel); padding: 16px; - border-radius: var(--radius); + border-radius: var(--MI-radius); > header { margin-bottom: 8px; @@ -114,7 +114,6 @@ const props = defineProps<{ > p { display: inline-block; margin: 0; - color: var(--urlPreviewInfo); font-size: 0.8em; line-height: 16px; vertical-align: top; diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index f67a1e5b63..84189211b6 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only :buttonsLeft="buttonsLeft" :buttonsRight="buttonsRight" :contextmenu="contextmenu" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header> <template v-if="pageMetadata"> @@ -30,17 +30,17 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue'; +import { url } from '@@/js/config.js'; +import { getScrollContainer } from '@@/js/scroll.js'; import RouterView from '@/components/global/RouterView.vue'; import MkWindow from '@/components/MkWindow.vue'; import { popout as _popout } from '@/scripts/popout.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { url } from '@@/js/config.js'; import { useScrollPositionManager } from '@/nirax.js'; import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { openingWindowsCount } from '@/os.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import { getScrollContainer } from '@@/js/scroll.js'; import { useRouterFactory } from '@/router/supplier.js'; import { mainRouter } from '@/router/main.js'; import MkUserName from './global/MkUserName.vue'; @@ -49,7 +49,7 @@ const props = defineProps<{ initialPath: string; }>(); -defineEmits<{ +const emit = defineEmits<{ (ev: 'closed'): void; }>(); @@ -59,7 +59,7 @@ const windowRouter = routerFactory(props.initialPath); const contents = shallowRef<HTMLElement | null>(null); const pageMetadata = ref<null | PageMetadata>(null); const windowEl = shallowRef<InstanceType<typeof MkWindow>>(); -const history = ref<{ path: string; key: any; }[]>([{ +const history = ref<{ path: string; key: string; }[]>([{ path: windowRouter.getCurrentPath(), key: windowRouter.getCurrentKey(), }]); @@ -181,8 +181,8 @@ defineExpose({ overscroll-behavior: contain; min-height: 100%; - background: var(--bg); + background: var(--MI_THEME-bg); - --margin: var(--marginHalf); + --MI-margin: var(--MI-marginHalf); } </style> diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 592a511fb0..a414676bda 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <li v-for="(choice, i) in props.poll.choices" :key="i" :class="$style.choice" @click="vote(i)"> <div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <span :class="$style.fg"> - <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template> + <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template> <Mfm :text="choice.text" :plain="true"/> <span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span> </span> @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only <p v-if="!readOnly" :class="$style.info"> <span>{{ i18n.tsx._poll.totalVotes({ n: total }) }}</span> <span v-if="poll.multiple"> · </span> - <span v-if="poll.multiple" style="color: var(--accent); font-weight: bolder;">{{ i18n.ts._poll.multiple }}</span> + <span v-if="poll.multiple" style="color: var(--MI_THEME-accent); font-weight: bolder;">{{ i18n.ts._poll.multiple }}</span> <span> · </span> <a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a> <span v-if="isVoted">{{ i18n.ts._poll.voted }}</span> @@ -33,14 +33,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { host } from '@@/js/config.js'; +import { useInterval } from '@@/js/use-interval.js'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { sum } from '@/scripts/array.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { host } from '@@/js/config.js'; -import { useInterval } from '@@/js/use-interval.js'; import { $i } from '@/account.js'; const props = defineProps<{ @@ -91,7 +91,7 @@ if (props.poll.expiresAt) { const vote = async (id) => { if (props.readOnly || closed.value || isVoted.value) return; - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); if (!props.poll.multiple) { const { canceled } = await os.confirm({ @@ -115,7 +115,7 @@ const vote = async (id) => { }; const refreshVotes = async () => { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); if (props.readOnly || closed.value) return; await misskeyApi('notes/polls/refresh', { @@ -139,9 +139,9 @@ const refreshVotes = async () => { position: relative; margin: 4px 0; padding: 4px; - //border: solid 0.5px var(--divider); - background: var(--accentedBg); - border-radius: var(--radius-xs); + //border: solid 0.5px var(--MI_THEME-divider); + background: var(--MI_THEME-accentedBg); + border-radius: var(--MI-radius-xs); overflow: clip; cursor: pointer; } @@ -151,8 +151,8 @@ const refreshVotes = async () => { top: 0; left: 0; height: 100%; - background: var(--accent); - background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB)); + background: var(--MI_THEME-accent); + background: linear-gradient(90deg,var(--MI_THEME-buttonGradateA),var(--MI_THEME-buttonGradateB)); transition: width 1s ease; } @@ -160,12 +160,12 @@ const refreshVotes = async () => { position: relative; display: inline-block; padding: 3px 5px; - background: var(--panel); - border-radius: var(--radius-xs); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-xs); } .info { - color: var(--fg); + color: var(--MI_THEME-fg); } .done { diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue index d14873008b..f1b5ff4de0 100644 --- a/packages/frontend/src/components/MkPopupMenu.vue +++ b/packages/frontend/src/components/MkPopupMenu.vue @@ -19,7 +19,7 @@ defineProps<{ items: MenuItem[]; align?: 'center' | string; width?: number; - src?: any; + src?: HTMLElement | null; returnFocusTo?: HTMLElement | null; }>(); @@ -73,7 +73,7 @@ function close() { <style lang="scss" module> .drawer { - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); border-bottom-right-radius: 0; border-bottom-left-radius: 0; } diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 93328fe52f..11ae6dbd6a 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -61,17 +61,17 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAcct :user="u"/> <button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button> </span> - <button class="_buttonPrimary" style="padding: 4px; border-radius: var(--radius-sm);" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button> + <button class="_buttonPrimary" style="padding: 4px; border-radius: var(--MI-radius-sm);" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button> </div> </div> <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> <div v-show="useCw" :class="$style.cwFrame"> - <input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> + <input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd"> <div v-if="maxCwLength - cwLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: cwLength > maxCwLength }]">{{ maxCwLength - cwLength }}</div> </div> <div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div> - <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text dir="auto" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> + <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text dir="auto" @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> </div> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> @@ -136,28 +136,14 @@ import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; +import type { PostFormProps } from '@/types/post-form.js'; import MkScheduleEditor from '@/components/MkScheduleEditor.vue'; const $i = signinRequired(); const modal = inject('modal'); -const props = withDefaults(defineProps<{ - reply?: Misskey.entities.Note; - renote?: Misskey.entities.Note; - channel?: Misskey.entities.Channel; // TODO - mention?: Misskey.entities.User; - specified?: Misskey.entities.UserDetailed; - initialText?: string; - initialCw?: string; - initialVisibility?: (typeof Misskey.noteVisibilities)[number]; - initialFiles?: Misskey.entities.DriveFile[]; - initialLocalOnly?: boolean; - initialVisibleUsers?: Misskey.entities.UserDetailed[]; - initialNote?: Misskey.entities.Note & { - isSchedule?: boolean, - }; - instant?: boolean; +const props = withDefaults(defineProps<PostFormProps & { fixed?: boolean; autofocus?: boolean; freezeAfterPosted?: boolean; @@ -212,6 +198,7 @@ const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]' const imeText = ref(''); const showingOptions = ref(false); const textAreaReadOnly = ref(false); +const justEndedComposition = ref(false); const scheduleNote = ref<{ scheduledAt: number | null; } | null>(null); @@ -602,7 +589,13 @@ function clear() { function onKeydown(ev: KeyboardEvent) { if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post(); - if (ev.key === 'Escape') emit('esc'); + // justEndedComposition.value is for Safari, which keyDown occurs after compositionend. + // ev.isComposing is for another browsers. + if (ev.key === 'Escape' && !justEndedComposition.value && !ev.isComposing) emit('esc'); +} + +function onKeyup(ev: KeyboardEvent) { + justEndedComposition.value = false; } function onCompositionUpdate(ev: CompositionEvent) { @@ -611,6 +604,7 @@ function onCompositionUpdate(ev: CompositionEvent) { function onCompositionEnd(ev: CompositionEvent) { imeText.value = ''; + justEndedComposition.value = true; } async function onPaste(ev: ClipboardEvent) { @@ -1015,8 +1009,8 @@ function showActions(ev: MouseEvent) { action.handler({ text: text.value, cw: cw.value, - }, (key, value: any) => { - if (typeof key !== 'string') return; + }, (key, value) => { + if (typeof key !== 'string' || typeof value !== 'string') return; if (key === 'text') { text.value = value; } if (key === 'cw') { useCw.value = value !== null; cw.value = value; } }); @@ -1228,8 +1222,8 @@ defineExpose({ &:focus-visible { outline: none; - .submitInner { - outline: 2px solid var(--fgOnAccent); + > .submitInner { + outline: 2px solid var(--MI_THEME-fgOnAccent); outline-offset: -4px; } } @@ -1243,14 +1237,14 @@ defineExpose({ } &:not(:disabled):hover { - > .inner { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + > .submitInner { + background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); } } &:not(:disabled):active { - > .inner { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + > .submitInner { + background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); } } } @@ -1261,7 +1255,7 @@ defineExpose({ left: 12px; width: 5px; height: 100% ; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); pointer-events: none; } @@ -1269,20 +1263,20 @@ defineExpose({ padding: 0 12px; line-height: 34px; font-weight: bold; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); min-width: 90px; box-sizing: border-box; - color: var(--fgOnAccent); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + color: var(--MI_THEME-fgOnAccent); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); } .headerRightItem { margin: 0; padding: 8px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); &:hover { - background: var(--X5); + background: var(--MI_THEME-X5); } &:disabled { @@ -1326,7 +1320,7 @@ defineExpose({ .withQuote { margin: 0 0 8px 0; - color: var(--accent); + color: var(--MI_THEME-accent); } .toSpecified { @@ -1345,8 +1339,8 @@ defineExpose({ .visibleUser { margin-right: 14px; padding: 8px 0 8px 8px; - border-radius: var(--radius-sm); - background: var(--X4); + border-radius: var(--MI-radius-sm); + background: var(--MI_THEME-X4); } .hasNotSpecifiedMentions { @@ -1365,7 +1359,7 @@ defineExpose({ border: none; border-radius: 0; background: transparent; - color: var(--fg); + color: var(--MI_THEME-fg); font-family: inherit; &:focus { @@ -1380,7 +1374,7 @@ defineExpose({ .cwFrame { z-index: 1; padding-bottom: 8px; - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); width: 100%; position: relative; @@ -1390,7 +1384,7 @@ defineExpose({ z-index: 1; padding-top: 8px; padding-bottom: 8px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .textOuter { @@ -1416,8 +1410,8 @@ defineExpose({ right: 2px; padding: 4px 6px; font-size: .9em; - color: var(--warn); - border-radius: var(--radius-sm); + color: var(--MI_THEME-warn); + border-radius: var(--MI-radius-sm); min-width: 1.6em; text-align: center; @@ -1457,19 +1451,19 @@ defineExpose({ font-size: 1em; width: auto; height: 100%; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); &:hover { - background: var(--X5); + background: var(--MI_THEME-X5); } &.footerButtonActive { - color: var(--accent); + color: var(--MI_THEME-accent); } } .previewButtonActive { - color: var(--accent); + color: var(--MI_THEME-accent); } @container (max-width: 500px) { diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index f90fcfef33..11444d8d78 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div v-show="props.modelValue.length != 0" :class="$style.root"> <Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)"> - <template #item="{element}"> + <template #item="{ element }"> <div :class="$style.file" role="button" @@ -38,14 +38,14 @@ import type { MenuItem } from '@/types/menu.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const props = defineProps<{ - modelValue: any[]; + modelValue: Misskey.entities.DriveFile[]; detachMediaFn?: (id: string) => void; }>(); const mock = inject<boolean>('mock', false); const emit = defineEmits<{ - (ev: 'update:modelValue', value: any[]): void; + (ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void; (ev: 'detach', id: string): void; (ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void; (ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void; @@ -113,7 +113,7 @@ async function rename(file) { }); } -async function describe(file) { +async function describe(file: Misskey.entities.DriveFile) { if (mock) return; const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { @@ -203,7 +203,7 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar width: 64px; height: 64px; margin-right: 4px; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); overflow: hidden; cursor: move; @@ -216,7 +216,7 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar width: 100%; height: 100%; z-index: 1; - color: var(--fg); + color: var(--MI_THEME-fg); } .sensitive { diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index 811a6378f2..0fd17e12c7 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -11,23 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { shallowRef } from 'vue'; -import * as Misskey from 'misskey-js'; import MkModal from '@/components/MkModal.vue'; import MkPostForm from '@/components/MkPostForm.vue'; +import * as Misskey from 'misskey-js'; +import type { PostFormProps } from '@/types/post-form.js'; -const props = withDefaults(defineProps<{ - reply?: Misskey.entities.Note; - renote?: Misskey.entities.Note; - channel?: any; // TODO - mention?: Misskey.entities.User; - specified?: Misskey.entities.UserDetailed; - initialText?: string; - initialCw?: string; - initialVisibility?: (typeof Misskey.noteVisibilities)[number]; - initialFiles?: Misskey.entities.DriveFile[]; - initialLocalOnly?: boolean; - initialVisibleUsers?: Misskey.entities.UserDetailed[]; - initialNote?: Misskey.entities.Note; +const props = withDefaults(defineProps<PostFormProps & { instant?: boolean; fixed?: boolean; autofocus?: boolean; diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue index e02f76a58f..5bd50170d8 100644 --- a/packages/frontend/src/components/MkRadio.vue +++ b/packages/frontend/src/components/MkRadio.vue @@ -24,17 +24,17 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> -<script lang="ts" setup> +<script lang="ts" setup generic="T extends unknown"> import { computed } from 'vue'; const props = defineProps<{ - modelValue: any; - value: any; + modelValue: T; + value: T; disabled?: boolean; }>(); const emit = defineEmits<{ - (ev: 'update:modelValue', value: any): void; + (ev: 'update:modelValue', value: T): void; }>(); const checked = computed(() => props.modelValue === props.value); @@ -53,10 +53,10 @@ function toggle(): void { cursor: pointer; padding: 7px 10px; min-width: 60px; - background-color: var(--panel); + background-color: var(--MI_THEME-panel); background-clip: padding-box !important; - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); font-size: 90%; transition: all 0.2s; user-select: none; @@ -67,25 +67,25 @@ function toggle(): void { } &:hover { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } &:focus-within { outline: none; - box-shadow: 0 0 0 2px var(--focus); + box-shadow: 0 0 0 2px var(--MI_THEME-focus); } &.checked { - background-color: var(--accentedBg) !important; - border-color: var(--accentedBg) !important; - color: var(--accent); + background-color: var(--MI_THEME-accentedBg) !important; + border-color: var(--MI_THEME-accentedBg) !important; + color: var(--MI_THEME-accent); cursor: default !important; > .button { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); &::after { - background-color: var(--accent); + background-color: var(--MI_THEME-accent); transform: scale(1); opacity: 1; } @@ -106,8 +106,8 @@ function toggle(): void { width: 14px; height: 14px; background: none; - border: solid 2px var(--inputBorder); - border-radius: var(--radius-full); + border: solid 2px var(--MI_THEME-inputBorder); + border-radius: var(--MI-radius-full); transition: inherit; &::after { @@ -118,7 +118,7 @@ function toggle(): void { right: 3px; bottom: 3px; left: 3px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); opacity: 0; transform: scale(0); transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue index 705c93f770..af81eb814d 100644 --- a/packages/frontend/src/components/MkRadios.vue +++ b/packages/frontend/src/components/MkRadios.vue @@ -77,7 +77,7 @@ export default defineComponent({ > .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index 22c187c357..d009f3858c 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -212,7 +212,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { > .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -224,9 +224,9 @@ function onMousedown(ev: MouseEvent | TouchEvent) { > .body { padding: 7px 12px; - background: var(--panel); - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); > .container { position: relative; @@ -242,7 +242,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { width: calc(100% - #{$thumbWidth}); height: 3px; background: rgba(0, 0, 0, 0.1); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); overflow: clip; > .highlight { @@ -250,7 +250,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { top: 0; left: 0; height: 100%; - background: var(--accent); + background: var(--MI_THEME-accent); opacity: 0.5; } } @@ -272,8 +272,8 @@ function onMousedown(ev: MouseEvent | TouchEvent) { width: $tickWidth; height: 3px; margin-left: - math.div($tickWidth, 2); - background: var(--divider); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-divider); + border-radius: var(--MI-radius-ellipse); } } @@ -282,11 +282,11 @@ function onMousedown(ev: MouseEvent | TouchEvent) { width: $thumbWidth; height: $thumbHeight; cursor: grab; - background: var(--accent); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-accent); + border-radius: var(--MI-radius-ellipse); &:hover { - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); } } } diff --git a/packages/frontend/src/components/MkReactionEffect.vue b/packages/frontend/src/components/MkReactionEffect.vue index 361e246e9f..5a59a5e055 100644 --- a/packages/frontend/src/components/MkReactionEffect.vue +++ b/packages/frontend/src/components/MkReactionEffect.vue @@ -60,7 +60,7 @@ onMounted(() => { right: 0; bottom: 0; margin: auto; - color: var(--accent); + color: var(--MI_THEME-accent); font-size: 18px; font-weight: bold; transform: translateY(-30px); diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index 6fdeb3a3ab..e60ac86315 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; +import * as Misskey from 'misskey-js'; import { getEmojiName } from '@@/js/emojilist.js'; import MkTooltip from './MkTooltip.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; @@ -30,7 +31,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue'; defineProps<{ showing: boolean; reaction: string; - users: any[]; // TODO + users: Misskey.entities.UserLite[]; count: number; targetElement: HTMLElement; }>(); @@ -57,7 +58,7 @@ function getReactionName(reaction: string): string { max-width: 100px; padding-right: 10px; text-align: center; - border-right: solid 0.5px var(--divider); + border-right: solid 0.5px var(--MI_THEME-divider); } .reactionIcon { diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 957ee0e76b..9cd972639f 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -175,12 +175,12 @@ if (!mock) { margin: 2px; padding: 0 6px; font-size: 1.5em; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); align-items: center; justify-content: center; &.canToggle { - background: var(--buttonBg); + background: var(--MI_THEME-buttonBg); &:hover { background: rgba(0, 0, 0, 0.1); @@ -194,7 +194,7 @@ if (!mock) { &.small { height: 32px; font-size: 1em; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); > .count { font-size: 0.9em; @@ -205,7 +205,7 @@ if (!mock) { &.large { height: 52px; font-size: 2em; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); > .count { font-size: 0.6em; @@ -214,12 +214,12 @@ if (!mock) { } &.reacted, &.reacted:hover { - background: var(--accentedBg); - color: var(--accent); - box-shadow: 0 0 0 1px var(--accent) inset; + background: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + box-shadow: 0 0 0 1px var(--MI_THEME-accent) inset; > .count { - color: var(--accent); + color: var(--MI_THEME-accent); } > .icon { diff --git a/packages/frontend/src/components/MkRemoteCaution.vue b/packages/frontend/src/components/MkRemoteCaution.vue index 2b59eab9d9..6391468204 100644 --- a/packages/frontend/src/components/MkRemoteCaution.vue +++ b/packages/frontend/src/components/MkRemoteCaution.vue @@ -19,15 +19,15 @@ defineProps<{ .root { font-size: 0.8em; padding: 16px; - background: color-mix(in srgb, var(--infoWarnBg) 65%, transparent); - color: var(--infoWarnFg); - border-radius: var(--radius); + background: color-mix(in srgb, var(--MI_THEME-infoWarnBg) 65%, transparent); + color: var(--MI_THEME-infoWarnFg); + border-radius: var(--MI-radius); overflow: clip; z-index: 1; } .link { margin-left: 4px; - color: var(--accent); + color: var(--MI_THEME-accent); } </style> diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue index c3daa9c9a4..d41793b0fa 100644 --- a/packages/frontend/src/components/MkRetentionLineChart.vue +++ b/packages/frontend/src/components/MkRetentionLineChart.vue @@ -44,7 +44,7 @@ onMounted(async () => { const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; - const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent')); + const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent')); const color = accent.toHex(); if (chartEl.value == null) return; diff --git a/packages/frontend/src/components/MkRippleEffect.vue b/packages/frontend/src/components/MkRippleEffect.vue index ee5bb73ebf..2949cf156d 100644 --- a/packages/frontend/src/components/MkRippleEffect.vue +++ b/packages/frontend/src/components/MkRippleEffect.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> <svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"> - <circle fill="none" cx="64" cy="64" style="stroke: var(--accent);"> + <circle fill="none" cx="64" cy="64" style="stroke: var(--MI_THEME-accent);"> <animate attributeName="r" begin="0s" dur="0.5s" @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only /> </circle> <g fill="none" fill-rule="evenodd"> - <circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--accent);"> + <circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--MI_THEME-accent);"> <animate attributeName="r" begin="0s" dur="0.8s" diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue index ce17ae08e0..3f14c5b5e0 100644 --- a/packages/frontend/src/components/MkRolePreview.vue +++ b/packages/frontend/src/components/MkRolePreview.vue @@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }"> <template v-if="forModeration"> - <i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--success)"></i> - <i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--warn)"></i> + <i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i> + <i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i> </template> <div v-adaptive-bg class="_panel" :class="$style.body"> @@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only <img :class="$style.bodyBadge" :src="role.iconUrl"/> </template> <template v-else> - <i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i> - <i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i> + <i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--MI_THEME-accent);"></i> + <i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--MI_THEME-accent);"></i> <i v-else class="ti ti-user" style="opacity: 0.7;"></i> </template> </span> diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 150a5c6d54..79a56b68a8 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only @keydown.space.enter="show" > <div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div> - <select + <div ref="inputEl" - v-model="v" v-adaptive-border tabindex="-1" :class="$style.inputCore" @@ -26,55 +25,48 @@ SPDX-License-Identifier: AGPL-3.0-only :required="required" :readonly="readonly" :placeholder="placeholder" - @input="onInput" @mousedown.prevent="() => {}" @keydown.prevent="() => {}" > - <slot></slot> - </select> + <div style="pointer-events: none;">{{ currentValueText ?? '' }}</div> + <div style="display: none;"> + <slot></slot> + </div> + </div> <div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div> </div> <div :class="$style.caption"><slot name="caption"></slot></div> - - <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </template> <script lang="ts" setup> import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue'; -import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os.js'; import { useInterval } from '@@/js/use-interval.js'; -import { i18n } from '@/i18n.js'; import type { MenuItem } from '@/types/menu.js'; +import * as os from '@/os.js'; const props = defineProps<{ - modelValue: string | null; + modelValue: string | number | null; required?: boolean; readonly?: boolean; disabled?: boolean; placeholder?: string; autofocus?: boolean; inline?: boolean; - manualSave?: boolean; small?: boolean; large?: boolean; }>(); const emit = defineEmits<{ - (ev: 'changeByUser', value: string | null): void; - (ev: 'update:modelValue', value: string | null): void; + (ev: 'update:modelValue', value: string | number | null): void; }>(); const slots = useSlots(); const { modelValue, autofocus } = toRefs(props); -const v = ref(modelValue.value); const focused = ref(false); const opening = ref(false); -const changed = ref(false); -const invalid = ref(false); -const filled = computed(() => v.value !== '' && v.value != null); +const currentValueText = ref<string | null>(null); const inputEl = ref<HTMLObjectElement | null>(null); const prefixEl = ref<HTMLElement | null>(null); const suffixEl = ref<HTMLElement | null>(null); @@ -85,26 +77,6 @@ const height = 36; const focus = () => container.value?.focus(); -const onInput = (ev) => { - changed.value = true; -}; - -const updated = () => { - changed.value = false; - emit('update:modelValue', v.value); -}; - -watch(modelValue, newValue => { - v.value = newValue; -}); - -watch(v, () => { - if (!props.manualSave) { - updated(); - } - - invalid.value = inputEl.value?.validity.badInput ?? true; -}); // このコンポーネントが作成された時、非表示状態である場合がある // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する @@ -134,6 +106,31 @@ onMounted(() => { }); }); +watch(modelValue, () => { + const scanOptions = (options: VNodeChild[]) => { + for (const vnode of options) { + if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue; + if (vnode.type === 'optgroup') { + const optgroup = vnode; + if (Array.isArray(optgroup.children)) scanOptions(optgroup.children); + } else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある + const fragment = vnode; + if (Array.isArray(fragment.children)) scanOptions(fragment.children); + } else if (vnode.props == null) { // v-if で条件が false のときにこうなる + // nop? + } else { + const option = vnode; + if (option.props?.value === modelValue.value) { + currentValueText.value = option.children as string; + break; + } + } + } + }; + + scanOptions(slots.default!()); +}, { immediate: true }); + function show() { if (opening.value) return; focus(); @@ -146,11 +143,9 @@ function show() { const pushOption = (option: VNode) => { menu.push({ text: option.children as string, - active: computed(() => v.value === option.props?.value), + active: computed(() => modelValue.value === option.props?.value), action: () => { - v.value = option.props?.value; - changed.value = true; - emit('changeByUser', v.value); + emit('update:modelValue', option.props?.value); }, }); }; @@ -202,7 +197,7 @@ function show() { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -220,8 +215,8 @@ function show() { &.focused { > .inputCore { - border-color: var(--accent) !important; - //box-shadow: 0 0 0 4px var(--focus); + border-color: var(--MI_THEME-accent) !important; + //box-shadow: 0 0 0 4px var(--MI_THEME-focus); } } @@ -240,7 +235,7 @@ function show() { &:hover { > .inputCore { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } } @@ -248,7 +243,8 @@ function show() { .inputCore { appearance: none; -webkit-appearance: none; - display: block; + display: flex; + align-items: center; height: v-bind("height + 'px'"); width: 100%; margin: 0; @@ -256,10 +252,10 @@ function show() { font: inherit; font-weight: normal; font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + color: var(--MI_THEME-fg); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); outline: none; box-shadow: none; box-sizing: border-box; diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue new file mode 100644 index 0000000000..34c22abc31 --- /dev/null +++ b/packages/frontend/src/components/MkSignin.input.vue @@ -0,0 +1,206 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.wrapper" data-cy-signin-page-input> + <div :class="$style.root"> + <div :class="$style.avatar"> + <i class="ti ti-user"></i> + </div> + + <!-- ログイン画面メッセージ --> + <MkInfo v-if="message"> + {{ message }} + </MkInfo> + + <!-- 外部サーバーへの転送 --> + <div v-if="openOnRemote" class="_gaps_m"> + <div class="_gaps_s"> + <MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)"> + {{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i> + </MkButton> + <button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)"> + {{ i18n.ts.specifyServerHost }} + </button> + </div> + <div :class="$style.orHr"> + <p :class="$style.orMsg">{{ i18n.ts.or }}</p> + </div> + </div> + + <!-- username入力 --> + <form class="_gaps_s" @submit.prevent="emit('usernameSubmitted', username)"> + <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username> + <template #prefix>@</template> + <template #suffix>@{{ host }}</template> + </MkInput> + <MkButton type="submit" large primary rounded style="margin: 0 auto;" data-cy-signin-page-input-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </form> + + <!-- パスワードレスログイン --> + <div :class="$style.orHr"> + <p :class="$style.orMsg">{{ i18n.ts.or }}</p> + </div> + <div> + <MkButton type="submit" style="margin: auto auto;" large rounded primary gradate @click="emit('passkeyClick', $event)"> + <i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }} + </MkButton> + </div> + </div> +</div> +</template> + +<script setup lang="ts"> +import { ref } from 'vue'; +import { toUnicode } from 'punycode/'; + +import { query, extractDomain } from '@@/js/url.js'; +import { host as configHost } from '@@/js/config.js'; +import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; + +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkInfo from '@/components/MkInfo.vue'; + +const props = withDefaults(defineProps<{ + message?: string, + openOnRemote?: OpenOnRemoteOptions, +}>(), { + message: '', + openOnRemote: undefined, +}); + +const emit = defineEmits<{ + (ev: 'usernameSubmitted', v: string): void; + (ev: 'passkeyClick', v: MouseEvent): void; +}>(); + +const host = toUnicode(configHost); + +const username = ref(''); + +//#region Open on remote +function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void { + switch (options.type) { + case 'web': + case 'lookup': { + let _path: string; + + if (options.type === 'lookup') { + // TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼ + // _path = `/lookup?uri=${encodeURIComponent(_path)}`; + _path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`; + } else { + _path = options.path; + } + + if (targetHost) { + window.open(`https://${targetHost}${_path}`, '_blank', 'noopener'); + } else { + window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener'); + } + break; + } + case 'share': { + const params = query(options.params); + if (targetHost) { + window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener'); + } else { + window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener'); + } + break; + } + } +} + +async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> { + const { canceled, result: hostTemp } = await os.inputText({ + title: i18n.ts.inputHostName, + placeholder: 'misskey.example.com', + }); + + if (canceled) return; + + let targetHost: string | null = hostTemp; + + // ドメイン部分だけを取り出す + targetHost = extractDomain(targetHost ?? ''); + if (targetHost == null) { + os.alert({ + type: 'error', + title: i18n.ts.invalidValue, + text: i18n.ts.tryAgain, + }); + return; + } + openRemote(options, targetHost); +} +//#endregion +</script> + +<style lang="scss" module> +.root { + display: flex; + flex-direction: column; + gap: 20px; +} + +.wrapper { + display: flex; + align-items: center; + width: 100%; + min-height: 336px; + + > .root { + width: 100%; + } +} + +.avatar { + margin: 0 auto; + background-color: color-mix(in srgb, var(--MI_THEME-fg), transparent 85%); + color: color-mix(in srgb, var(--MI_THEME-fg), transparent 25%); + text-align: center; + height: 64px; + width: 64px; + font-size: 24px; + line-height: 64px; + border-radius: 50%; +} + +.instanceManualSelectButton { + display: block; + text-align: center; + opacity: .7; + font-size: .8em; + + &:hover { + text-decoration: underline; + } +} + +.orHr { + position: relative; + margin: .4em auto; + width: 100%; + height: 1px; + background: var(--MI_THEME-divider); +} + +.orMsg { + position: absolute; + top: -.6em; + display: inline-block; + padding: 0 1em; + background: var(--MI_THEME-panel); + font-size: 0.8em; + color: var(--MI_THEME-fgOnPanel); + margin: 0; + left: 50%; + transform: translateX(-50%); +} +</style> diff --git a/packages/frontend/src/components/MkSignin.passkey.vue b/packages/frontend/src/components/MkSignin.passkey.vue new file mode 100644 index 0000000000..e5a56ab66d --- /dev/null +++ b/packages/frontend/src/components/MkSignin.passkey.vue @@ -0,0 +1,92 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.wrapper"> + <div class="_gaps" :class="$style.root"> + <div class="_gaps_s"> + <div :class="$style.passkeyIcon"> + <i class="ti ti-fingerprint"></i> + </div> + <div :class="$style.passkeyDescription">{{ i18n.ts.useSecurityKey }}</div> + </div> + + <MkButton large primary rounded :disabled="queryingKey" style="margin: 0 auto;" @click="queryKey">{{ i18n.ts.retry }}</MkButton> + + <MkButton v-if="isPerformingPasswordlessLogin !== true" transparent rounded :disabled="queryingKey" style="margin: 0 auto;" @click="emit('useTotp')">{{ i18n.ts.useTotp }}</MkButton> + </div> +</div> +</template> + +<script setup lang="ts"> +import { ref, onMounted } from 'vue'; +import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill'; + +import { i18n } from '@/i18n.js'; + +import MkButton from '@/components/MkButton.vue'; + +import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill'; + +const props = defineProps<{ + credentialRequest: CredentialRequestOptions; + isPerformingPasswordlessLogin?: boolean; +}>(); + +const emit = defineEmits<{ + (ev: 'done', credential: AuthenticationPublicKeyCredential): void; + (ev: 'useTotp'): void; +}>(); + +const queryingKey = ref(true); + +async function queryKey() { + queryingKey.value = true; + await webAuthnRequest(props.credentialRequest) + .catch(() => { + return Promise.reject(null); + }) + .then((credential) => { + emit('done', credential); + }) + .finally(() => { + queryingKey.value = false; + }); +} + +onMounted(() => { + queryKey(); +}); +</script> + +<style lang="scss" module> +.wrapper { + display: flex; + align-items: center; + width: 100%; + min-height: 336px; + + > .root { + width: 100%; + } +} + +.passkeyIcon { + margin: 0 auto; + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + text-align: center; + height: 64px; + width: 64px; + font-size: 24px; + line-height: 64px; + border-radius: 50%; +} + +.passkeyDescription { + text-align: center; + font-size: 1.1em; +} +</style> diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue new file mode 100644 index 0000000000..ff7c598b50 --- /dev/null +++ b/packages/frontend/src/components/MkSignin.password.vue @@ -0,0 +1,195 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.wrapper" data-cy-signin-page-password> + <div class="_gaps" :class="$style.root"> + <div :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined }"></div> + <div :class="$style.welcomeBackMessage"> + <I18n :src="i18n.ts.welcomeBackWithName" tag="span"> + <template #name><Mfm :text="user.name ?? user.username" :plain="true"/></template> + </I18n> + </div> + + <!-- password入力 --> + <form class="_gaps_s" @submit.prevent="onSubmit"> + <!-- ブラウザ オートコンプリート用 --> + <input type="hidden" name="username" autocomplete="username" :value="user.username"> + + <MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required autofocus data-cy-signin-password> + <template #prefix><i class="ti ti-lock"></i></template> + <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> + </MkInput> + + <div v-if="needCaptcha"> + <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> + <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/> + <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> + <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> + <MkCaptcha v-if="instance.enableFC" ref="fc" v-model="fcResponse" provider="fc" :sitekey="instance.fcSiteKey"/> + <MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha"/> + </div> + + <MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </form> + </div> +</div> +</template> + +<script lang="ts"> +export type PwResponse = { + password: string; + captcha: { + hCaptchaResponse: string | null; + mCaptchaResponse: string | null; + reCaptchaResponse: string | null; + turnstileResponse: string | null; + fcResponse: string | null; + testcaptchaResponse: string | null; + }; +}; +</script> + +<script setup lang="ts"> +import { ref, computed, useTemplateRef, defineAsyncComponent } from 'vue'; +import * as Misskey from 'misskey-js'; + +import { instance } from '@/instance.js'; +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; + +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkCaptcha from '@/components/MkCaptcha.vue'; + +const props = defineProps<{ + user: Misskey.entities.UserDetailed; + needCaptcha: boolean; +}>(); + +const emit = defineEmits<{ + (ev: 'passwordSubmitted', v: PwResponse): void; +}>(); + +const password = ref(''); + +const hCaptcha = useTemplateRef('hcaptcha'); +const mCaptcha = useTemplateRef('mcaptcha'); +const reCaptcha = useTemplateRef('recaptcha'); +const turnstile = useTemplateRef('turnstile'); +const fc = useTemplateRef('fc'); +const testcaptcha = useTemplateRef('testcaptcha'); + +const hCaptchaResponse = ref<string | null>(null); +const mCaptchaResponse = ref<string | null>(null); +const reCaptchaResponse = ref<string | null>(null); +const turnstileResponse = ref<string | null>(null); +const fcResponse = ref<string | null>(null); +const testcaptchaResponse = ref<string | null>(null); + +const captchaFailed = computed((): boolean => { + return ( + (instance.enableHcaptcha && !hCaptchaResponse.value) || + (instance.enableMcaptcha && !mCaptchaResponse.value) || + (instance.enableRecaptcha && !reCaptchaResponse.value) || + (instance.enableTurnstile && !turnstileResponse.value) || + (instance.enableFC && !fcResponse.value) || + (instance.enableTestcaptcha && !testcaptchaResponse.value) + ); +}); + +function resetPassword(): void { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, { + closed: () => dispose(), + }); +} + +function onSubmit() { + emit('passwordSubmitted', { + password: password.value, + captcha: { + hCaptchaResponse: hCaptchaResponse.value, + mCaptchaResponse: mCaptchaResponse.value, + reCaptchaResponse: reCaptchaResponse.value, + turnstileResponse: turnstileResponse.value, + fcResponse: fcResponse.value, + testcaptchaResponse: testcaptchaResponse.value, + }, + }); +} + +function resetCaptcha() { + hCaptcha.value?.reset(); + mCaptcha.value?.reset(); + reCaptcha.value?.reset(); + turnstile.value?.reset(); + fc.value?.reset(); + testcaptcha.value?.reset(); +} + +defineExpose({ + resetCaptcha, +}); +</script> + +<style lang="scss" module> +.wrapper { + display: flex; + align-items: center; + width: 100%; + min-height: 336px; + + > .root { + width: 100%; + } +} + +.avatar { + margin: 0 auto 0 auto; + width: 64px; + height: 64px; + background: #ddd; + background-position: center; + background-size: cover; + border-radius: 100%; +} + +.welcomeBackMessage { + text-align: center; + font-size: 1.1em; +} + +.instanceManualSelectButton { + display: block; + text-align: center; + opacity: .7; + font-size: .8em; + + &:hover { + text-decoration: underline; + } +} + +.orHr { + position: relative; + margin: .4em auto; + width: 100%; + height: 1px; + background: var(--MI_THEME-divider); +} + +.orMsg { + position: absolute; + top: -.6em; + display: inline-block; + padding: 0 1em; + background: var(--MI_THEME-panel); + font-size: 0.8em; + color: var(--MI_THEME-fgOnPanel); + margin: 0; + left: 50%; + transform: translateX(-50%); +} +</style> diff --git a/packages/frontend/src/components/MkSignin.totp.vue b/packages/frontend/src/components/MkSignin.totp.vue new file mode 100644 index 0000000000..670b8057c2 --- /dev/null +++ b/packages/frontend/src/components/MkSignin.totp.vue @@ -0,0 +1,74 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.wrapper"> + <div class="_gaps" :class="$style.root"> + <div class="_gaps_s"> + <div :class="$style.totpIcon"> + <i class="ti ti-key"></i> + </div> + <div :class="$style.totpDescription">{{ i18n.ts['2fa'] }}</div> + </div> + + <!-- totp入力 --> + <form class="_gaps_s" @submit.prevent="emit('totpSubmitted', token)"> + <MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required autofocus :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'"> + <template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template> + <template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template> + <template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template> + </MkInput> + + <MkButton type="submit" large primary rounded style="margin: 0 auto;">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </form> + </div> +</div> +</template> + +<script setup lang="ts"> +import { ref } from 'vue'; + +import { i18n } from '@/i18n.js'; + +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; + +const emit = defineEmits<{ + (ev: 'totpSubmitted', token: string): void; +}>(); + +const token = ref(''); +const isBackupCode = ref(false); +</script> + +<style lang="scss" module> +.wrapper { + display: flex; + align-items: center; + width: 100%; + min-height: 336px; + + > .root { + width: 100%; + } +} + +.totpIcon { + margin: 0 auto; + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + text-align: center; + height: 64px; + width: 64px; + font-size: 24px; + line-height: 64px; + border-radius: 50%; +} + +.totpDescription { + text-align: center; + font-size: 1.1em; +} +</style> diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 82e0df8a01..4a6219071b 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -4,245 +4,290 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> - <div class="_gaps_m"> - <div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div> - <MkInfo v-if="message"> - {{ message }} - </MkInfo> - <div v-if="openOnRemote" class="_gaps_m"> - <div class="_gaps_s"> - <MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)"> - {{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i> - </MkButton> - <button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)"> - {{ i18n.ts.specifyServerHost }} - </button> - </div> - <div :class="$style.orHr"> - <p :class="$style.orMsg">{{ i18n.ts.or }}</p> - </div> - </div> - <div v-if="!totpLogin" class="normal-signin _gaps_m"> - <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange"> - <template #prefix>@</template> - <template #suffix>@{{ host }}</template> - </MkInput> - <MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password> - <template #prefix><i class="ti ti-lock"></i></template> - <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> - </MkInput> - <MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> - </div> - <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> - <div v-if="user && user.securityKeys" class="twofa-group tap-group"> - <p>{{ i18n.ts.useSecurityKey }}</p> - <MkButton v-if="!queryingKey" @click="query2FaKey"> - {{ i18n.ts.retry }} - </MkButton> - </div> - <div v-if="user && user.securityKeys" :class="$style.orHr"> - <p :class="$style.orMsg">{{ i18n.ts.or }}</p> - </div> - <div class="twofa-group totp-group _gaps"> - <MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'"> - <template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template> - <template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template> - <template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template> - </MkInput> - <MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> - </div> - </div> - <div v-if="!totpLogin && usePasswordLessLogin" :class="$style.orHr"> - <p :class="$style.orMsg">{{ i18n.ts.or }}</p> - </div> - <div v-if="!totpLogin && usePasswordLessLogin" class="twofa-group tap-group"> - <MkButton v-if="!queryingKey" type="submit" :disabled="signing" style="margin: auto auto;" rounded large primary @click="onPasskeyLogin"> - <i class="ti ti-device-usb" style="font-size: medium;"></i> - {{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }} - </MkButton> - <p v-if="queryingKey">{{ i18n.ts.useSecurityKey }}</p> - </div> +<div :class="$style.signinRoot"> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_enterActive" + :leaveActiveClass="$style.transition_leaveActive" + :enterFromClass="$style.transition_enterFrom" + :leaveToClass="$style.transition_leaveTo" + + :inert="waiting" + > + <!-- 1. 外部サーバーへの転送・username入力・パスキー --> + <XInput + v-if="page === 'input'" + key="input" + :message="message" + :openOnRemote="openOnRemote" + + @usernameSubmitted="onUsernameSubmitted" + @passkeyClick="onPasskeyLogin" + /> + + <!-- 2. パスワード入力 --> + <XPassword + v-else-if="page === 'password'" + key="password" + ref="passwordPageEl" + + :user="userInfo!" + :needCaptcha="needCaptcha" + + @passwordSubmitted="onPasswordSubmitted" + /> + + <!-- 3. ワンタイムパスワード --> + <XTotp + v-else-if="page === 'totp'" + key="totp" + + @totpSubmitted="onTotpSubmitted" + /> + + <!-- 4. パスキー --> + <XPasskey + v-else-if="page === 'passkey'" + key="passkey" + + :credentialRequest="credentialRequest!" + :isPerformingPasswordlessLogin="doingPasskeyFromInputPage" + + @done="onPasskeyDone" + @useTotp="onUseTotp" + /> + </Transition> + <div v-if="waiting" :class="$style.waitingRoot"> + <MkLoading/> </div> -</form> +</div> </template> -<script lang="ts" setup> -import { defineAsyncComponent, ref } from 'vue'; -import { toUnicode } from 'punycode/'; +<script setup lang="ts"> +import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; -import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; -import { SigninWithPasskeyResponse } from 'misskey-js/entities.js'; -import { query, extractDomain } from '@@/js/url.js'; -import { host as configHost } from '@@/js/config.js'; -import MkDivider from './MkDivider.vue'; +import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; + +import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; -import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; -import MkButton from '@/components/MkButton.vue'; -import MkInput from '@/components/MkInput.vue'; -import MkInfo from '@/components/MkInfo.vue'; -import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; +import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; import { showSystemAccountDialog } from '@/scripts/show-system-account-dialog.js'; +import * as os from '@/os.js'; -const signing = ref(false); -const user = ref<Misskey.entities.UserDetailed | null>(null); -const usePasswordLessLogin = ref<Misskey.entities.UserDetailed['usePasswordLessLogin']>(true); -const username = ref(''); -const password = ref(''); -const token = ref(''); -const host = ref(toUnicode(configHost)); -const totpLogin = ref(false); -const isBackupCode = ref(false); -const queryingKey = ref(false); -let credentialRequest: CredentialRequestOptions | null = null; -const passkey_context = ref(''); +import XInput from '@/components/MkSignin.input.vue'; +import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue'; +import XTotp from '@/components/MkSignin.totp.vue'; +import XPasskey from '@/components/MkSignin.passkey.vue'; const emit = defineEmits<{ - (ev: 'login', v: any): void; + (ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void; }>(); const props = withDefaults(defineProps<{ - withAvatar?: boolean; autoSet?: boolean; message?: string, openOnRemote?: OpenOnRemoteOptions, }>(), { - withAvatar: true, autoSet: false, message: '', openOnRemote: undefined, }); -function onUsernameChange(): void { - const usernameRequested = username.value; - misskeyApi('users/show', { - username: usernameRequested, - }).then(userResponse => { - if (userResponse.username === username.value) { - user.value = userResponse; - usePasswordLessLogin.value = userResponse.usePasswordLessLogin; - } - }, () => { - if (usernameRequested === username.value) { - user.value = null; - usePasswordLessLogin.value = true; - } - }); -} +const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input'); +const waiting = ref(false); -function onLogin(res: any): Promise<void> | void { - if (props.autoSet) { - return login(res.i); - } -} +const passwordPageEl = useTemplateRef('passwordPageEl'); +const needCaptcha = ref(false); -async function query2FaKey(): Promise<void> { - if (credentialRequest == null) return; - queryingKey.value = true; - await webAuthnRequest(credentialRequest) - .catch(() => { - queryingKey.value = false; - return Promise.reject(null); - }).then(credential => { - credentialRequest = null; - queryingKey.value = false; - signing.value = true; - return misskeyApi('signin', { - username: username.value, - password: password.value, - credential: credential.toJSON(), - }); - }).then(res => { - emit('login', res); - return onLogin(res); - }).catch(err => { - if (err === null) return; - os.alert({ - type: 'error', - text: i18n.ts.signinFailed, - }); - signing.value = false; - }); -} +const userInfo = ref<null | Misskey.entities.UserDetailed>(null); +const password = ref(''); + +//#region Passkey Passwordless +const credentialRequest = shallowRef<CredentialRequestOptions | null>(null); +const passkeyContext = ref(''); +const doingPasskeyFromInputPage = ref(false); function onPasskeyLogin(): void { - signing.value = true; if (webAuthnSupported()) { + doingPasskeyFromInputPage.value = true; + waiting.value = true; misskeyApi('signin-with-passkey', {}) - .then((res: SigninWithPasskeyResponse) => { - totpLogin.value = false; - signing.value = false; - queryingKey.value = true; - passkey_context.value = res.context ?? ''; - credentialRequest = parseRequestOptionsFromJSON({ + .then((res) => { + passkeyContext.value = res.context ?? ''; + credentialRequest.value = parseRequestOptionsFromJSON({ publicKey: res.option, }); + + page.value = 'passkey'; + waiting.value = false; }) - .then(() => queryPasskey()) - .catch(loginFailed); + .catch(onSigninApiError); } } -async function queryPasskey(): Promise<void> { - if (credentialRequest == null) return; - queryingKey.value = true; - console.log('Waiting passkey auth...'); - await webAuthnRequest(credentialRequest) - .catch((err) => { - console.warn('Passkey Auth fail!: ', err); - queryingKey.value = false; - return Promise.reject(null); - }).then(credential => { - credentialRequest = null; - queryingKey.value = false; - signing.value = true; - return misskeyApi('signin-with-passkey', { - credential: credential.toJSON(), - context: passkey_context.value, - }); - }).then((res: SigninWithPasskeyResponse) => { +function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void { + waiting.value = true; + + if (doingPasskeyFromInputPage.value) { + misskeyApi('signin-with-passkey', { + credential: credential.toJSON(), + context: passkeyContext.value, + }).then((res) => { + if (res.signinResponse == null) { + onSigninApiError(); + return; + } emit('login', res.signinResponse); - return onLogin(res.signinResponse); + }).catch(onSigninApiError); + } else if (userInfo.value != null) { + tryLogin({ + username: userInfo.value.username, + password: password.value, + credential: credential.toJSON(), }); + } } -function onSubmit(): void { - signing.value = true; - if (!totpLogin.value && user.value && user.value.twoFactorEnabled) { - if (webAuthnSupported() && user.value.securityKeys) { - misskeyApi('signin', { - username: username.value, - password: password.value, - }).then(res => { - totpLogin.value = true; - signing.value = false; - credentialRequest = parseRequestOptionsFromJSON({ - publicKey: res, - }); - }) - .then(() => query2FaKey()) - .catch(loginFailed); - } else { - totpLogin.value = true; - signing.value = false; - } +function onUseTotp(): void { + page.value = 'totp'; +} +//#endregion + +async function onUsernameSubmitted(username: string) { + waiting.value = true; + + userInfo.value = await misskeyApi('users/show', { + username, + }).catch(() => null); + + await tryLogin({ + username, + }); +} + +async function onPasswordSubmitted(pw: PwResponse) { + waiting.value = true; + password.value = pw.password; + + if (userInfo.value == null) { + await os.alert({ + type: 'error', + title: i18n.ts.noSuchUser, + text: i18n.ts.signinFailed, + }); + waiting.value = false; + return; + } else { + await tryLogin({ + username: userInfo.value.username, + password: pw.password, + 'hcaptcha-response': pw.captcha.hCaptchaResponse, + 'm-captcha-response': pw.captcha.mCaptchaResponse, + 'g-recaptcha-response': pw.captcha.reCaptchaResponse, + 'frc-captcha-solution': pw.captcha.fcResponse, + 'turnstile-response': pw.captcha.turnstileResponse, + 'testcaptcha-response': pw.captcha.testcaptchaResponse, + }); + } +} + +async function onTotpSubmitted(token: string) { + waiting.value = true; + + if (userInfo.value == null) { + await os.alert({ + type: 'error', + title: i18n.ts.noSuchUser, + text: i18n.ts.signinFailed, + }); + waiting.value = false; + return; } else { - misskeyApi('signin', { - username: username.value, + await tryLogin({ + username: userInfo.value.username, password: password.value, - token: user.value?.twoFactorEnabled ? token.value : undefined, - }).then(res => { + token, + }); + } +} + +async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promise<Misskey.entities.SigninFlowResponse> { + const _req = { + username: req.username ?? userInfo.value?.username, + ...req, + }; + + function assertIsSigninFlowRequest(x: Partial<Misskey.entities.SigninFlowRequest>): x is Misskey.entities.SigninFlowRequest { + return x.username != null; + } + + if (!assertIsSigninFlowRequest(_req)) { + throw new Error('Invalid request'); + } + + return await misskeyApi('signin-flow', _req).then(async (res) => { + if (res.finished) { emit('login', res); - onLogin(res); - }).catch(loginFailed); + await onLoginSucceeded(res); + } else { + switch (res.next) { + case 'captcha': { + needCaptcha.value = true; + page.value = 'password'; + break; + } + case 'password': { + needCaptcha.value = false; + page.value = 'password'; + break; + } + case 'totp': { + page.value = 'totp'; + break; + } + case 'passkey': { + if (webAuthnSupported()) { + credentialRequest.value = parseRequestOptionsFromJSON({ + publicKey: res.authRequest, + }); + page.value = 'passkey'; + } else { + page.value = 'totp'; + } + break; + } + } + + if (doingPasskeyFromInputPage.value === true) { + doingPasskeyFromInputPage.value = false; + page.value = 'input'; + password.value = ''; + } + passwordPageEl.value?.resetCaptcha(); + nextTick(() => { + waiting.value = false; + }); + } + return res; + }).catch((err) => { + onSigninApiError(err); + return Promise.reject(err); + }); +} + +async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true }) { + if (props.autoSet) { + await login(res.i); } } -function loginFailed(err: any): void { - switch (err.id) { +function onSigninApiError(err?: any): void { + const id = err?.id ?? null; + + switch (id) { case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { os.alert({ type: 'error', @@ -275,6 +320,14 @@ function loginFailed(err: any): void { }); break; } + case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.incorrectTotp, + }); + break; + } case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': { os.alert({ type: 'error', @@ -283,6 +336,14 @@ function loginFailed(err: any): void { }); break; } + case '93b86c4b-72f9-40eb-9815-798928603d1e': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.passkeyVerificationFailed, + }); + break; + } case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': { os.alert({ type: 'error', @@ -309,113 +370,55 @@ function loginFailed(err: any): void { } } - totpLogin.value = false; - signing.value = false; -} - -function resetPassword(): void { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, { - closed: () => dispose(), - }); -} - -function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void { - switch (options.type) { - case 'web': - case 'lookup': { - let _path: string; - - if (options.type === 'lookup') { - // TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼ - // _path = `/lookup?uri=${encodeURIComponent(_path)}`; - _path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`; - } else { - _path = options.path; - } - - if (targetHost) { - window.open(`https://${targetHost}${_path}`, '_blank', 'noopener'); - } else { - window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener'); - } - break; - } - case 'share': { - const params = query(options.params); - if (targetHost) { - window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener'); - } else { - window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener'); - } - break; - } + if (doingPasskeyFromInputPage.value === true) { + doingPasskeyFromInputPage.value = false; + page.value = 'input'; + password.value = ''; } -} - -async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> { - const { canceled, result: hostTemp } = await os.inputText({ - title: i18n.ts.inputHostName, - placeholder: 'misskey.example.com', + passwordPageEl.value?.resetCaptcha(); + nextTick(() => { + waiting.value = false; }); - - if (canceled) return; - - let targetHost: string | null = hostTemp; - - // ドメイン部分だけを取り出す - targetHost = extractDomain(targetHost); - if (targetHost == null) { - os.alert({ - type: 'error', - title: i18n.ts.invalidValue, - text: i18n.ts.tryAgain, - }); - return; - } - openRemote(options, targetHost); } + +onBeforeUnmount(() => { + password.value = ''; + needCaptcha.value = false; + userInfo.value = null; +}); </script> <style lang="scss" module> -.avatar { - margin: 0 auto 0 auto; - width: 64px; - height: 64px; - background: #ddd; - background-position: center; - background-size: cover; - border-radius: var(--radius-full); +.transition_enterActive, +.transition_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); } - -.instanceManualSelectButton { - display: block; - text-align: center; - opacity: .7; - font-size: .8em; - - &:hover { - text-decoration: underline; - } +.transition_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_leaveTo { + opacity: 0; + transform: translateX(-50px); } -.orHr { +.signinRoot { + overflow-x: hidden; + overflow-x: clip; + position: relative; - margin: .4em auto; - width: 100%; - height: 1px; - background: var(--divider); } -.orMsg { +.waitingRoot { position: absolute; - top: -.6em; - display: inline-block; - padding: 0 1em; - background: var(--panel); - font-size: 0.8em; - color: var(--fgOnPanel); - margin: 0; - left: 50%; - transform: translateX(-50%); + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%); + display: flex; + justify-content: center; + align-items: center; + z-index: 1; } </style> diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index d48780e9de..676a336ec7 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -4,26 +4,30 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModalWindow - ref="dialog" - :width="400" - :height="450" - @close="onClose" +<MkModal + ref="modal" + :preferType="'dialog'" + @click="onClose" @closed="emit('closed')" > - <template #header>{{ i18n.ts.login }}</template> - - <MkSpacer :marginMin="20" :marginMax="28"> - <MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/> - </MkSpacer> -</MkModalWindow> + <div :class="$style.root"> + <div :class="$style.header"> + <div :class="$style.headerText"><i class="ti ti-login-2"></i> {{ i18n.ts.login }}</div> + <button :class="$style.closeButton" class="_button" @click="onClose"><i class="ti ti-x"></i></button> + </div> + <div :class="$style.content"> + <MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/> + </div> + </div> +</MkModal> </template> <script lang="ts" setup> +import * as Misskey from 'misskey-js'; import { shallowRef } from 'vue'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import MkSignin from '@/components/MkSignin.vue'; -import MkModalWindow from '@/components/MkModalWindow.vue'; +import MkModal from '@/components/MkModal.vue'; import { i18n } from '@/i18n.js'; withDefaults(defineProps<{ @@ -37,20 +41,67 @@ withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'done', v: any): void; + (ev: 'done', v: Misskey.entities.SigninFlowResponse & { finished: true }): void; (ev: 'closed'): void; (ev: 'cancelled'): void; }>(); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const modal = shallowRef<InstanceType<typeof MkModal>>(); function onClose() { emit('cancelled'); - if (dialog.value) dialog.value.close(); + if (modal.value) modal.value.close(); } -function onLogin(res) { +function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true }) { emit('done', res); - if (dialog.value) dialog.value.close(); + if (modal.value) modal.value.close(); } </script> + +<style lang="scss" module> +.root { + overflow: auto; + margin: auto; + position: relative; + width: 100%; + max-width: 400px; + height: 100%; + max-height: 450px; + box-sizing: border-box; + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); +} + +.header { + position: sticky; + top: 0; + left: 0; + width: 100%; + height: 50px; + box-sizing: border-box; + display: flex; + align-items: center; + font-weight: bold; + backdrop-filter: var(--MI-blur, blur(15px)); + background: var(--MI_THEME-acrylicBg); + z-index: 1; +} + +.headerText { + padding: 0 20px; + box-sizing: border-box; +} + +.closeButton { + margin-left: auto; + padding: 16px; + font-size: 16px; + line-height: 16px; +} + +.content { + padding: 32px; + box-sizing: border-box; +} +</style> diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 4c55831a3a..e636712389 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -21,12 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption> <div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div> <span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> - <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> - <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> - <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> - <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span> - <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span> - <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span> + <span v-else-if="usernameState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="usernameState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="usernameState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> + <span v-else-if="usernameState === 'invalid-format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span> + <span v-else-if="usernameState === 'min-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span> + <span v-else-if="usernameState === 'max-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span> </template> </MkInput> <MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail"> @@ -34,32 +34,32 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix><i class="ti ti-mail"></i></template> <template #caption> <span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> - <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> - <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span> - <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span> - <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span> - <span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span> - <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span> - <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span> - <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> - <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> + <span v-else-if="emailState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="emailState === 'unavailable:used'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span> + <span v-else-if="emailState === 'unavailable:format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span> + <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span> + <span v-else-if="emailState === 'unavailable:banned'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span> + <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span> + <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span> + <span v-else-if="emailState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="emailState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> </template> </MkInput> <MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword"> <template #label>{{ i18n.ts.password }}</template> <template #prefix><i class="ti ti-lock"></i></template> <template #caption> - <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span> - <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span> - <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span> + <span v-if="passwordStrength == 'low'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span> + <span v-if="passwordStrength == 'medium'" style="color: var(--MI_THEME-warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span> + <span v-if="passwordStrength == 'high'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span> </template> </MkInput> <MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype"> <template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template> <template #prefix><i class="ti ti-lock"></i></template> <template #caption> - <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span> - <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span> + <span v-if="passwordRetypeState == 'match'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span> + <span v-if="passwordRetypeState == 'not-match'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span> </template> </MkInput> <MkInput v-if="instance.approvalRequiredForSignup" v-model="reason" type="text" :spellcheck="false" required data-cy-signup-reason> @@ -71,6 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> <MkCaptcha v-if="instance.enableFC" ref="fc" v-model="fcResponse" :class="$style.captcha" provider="fc" :sitekey="instance.fcSiteKey"/> + <MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/> <MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;"> <template v-if="submitting"> <MkLoading :em="true" :colored="false"/> @@ -86,10 +87,10 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed } from 'vue'; import { toUnicode } from 'punycode/'; import * as Misskey from 'misskey-js'; +import * as config from '@@/js/config.js'; import MkButton from './MkButton.vue'; import MkInput from './MkInput.vue'; import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; -import * as config from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; @@ -103,7 +104,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'signup', user: Misskey.entities.SigninResponse): void; + (ev: 'signup', user: Misskey.entities.SignupResponse): void; (ev: 'signupEmailPending'): void; (ev: 'approvalPending'): void; }>(); @@ -111,9 +112,11 @@ const emit = defineEmits<{ const host = toUnicode(config.host); const hcaptcha = ref<Captcha | undefined>(); +const mcaptcha = ref<Captcha | undefined>(); const recaptcha = ref<Captcha | undefined>(); const turnstile = ref<Captcha | undefined>(); const fc = ref<Captcha | undefined>(); +const testcaptcha = ref<Captcha | undefined>(); const username = ref<string>(''); const password = ref<string>(''); @@ -131,6 +134,7 @@ const mCaptchaResponse = ref<string | null>(null); const reCaptchaResponse = ref<string | null>(null); const turnstileResponse = ref<string | null>(null); const fcResponse = ref<string | null>(null); +const testcaptchaResponse = ref<string | null>(null); const usernameAbortController = ref<null | AbortController>(null); const emailAbortController = ref<null | AbortController>(null); @@ -141,6 +145,7 @@ const shouldDisableSubmitting = computed((): boolean => { instance.enableRecaptcha && !reCaptchaResponse.value || instance.enableTurnstile && !turnstileResponse.value || instance.enableFC && !fcResponse.value || + instance.enableTestcaptcha && !testcaptchaResponse.value || instance.emailRequiredForSignup && emailState.value !== 'ok' || usernameState.value !== 'ok' || passwordRetypeState.value !== 'match'; @@ -259,20 +264,33 @@ async function onSubmit(): Promise<void> { if (submitting.value) return; submitting.value = true; - try { - await misskeyApi('signup', { - username: username.value, - password: password.value, - emailAddress: email.value, - invitationCode: invitationCode.value, - reason: reason.value, - 'hcaptcha-response': hCaptchaResponse.value, - 'm-captcha-response': mCaptchaResponse.value, - 'g-recaptcha-response': reCaptchaResponse.value, - 'turnstile-response': turnstileResponse.value, - 'frc-captcha-solution': fcResponse.value, - }); - if (instance.emailRequiredForSignup) { + const signupPayload: Misskey.entities.SignupRequest = { + username: username.value, + password: password.value, + emailAddress: email.value, + invitationCode: invitationCode.value, + reason: reason.value, + 'hcaptcha-response': hCaptchaResponse.value, + 'm-captcha-response': mCaptchaResponse.value, + 'g-recaptcha-response': reCaptchaResponse.value, + 'turnstile-response': turnstileResponse.value, + 'frc-captcha-solution': fcResponse.value, + 'testcaptcha-response': testcaptchaResponse.value, + }; + + const res = await fetch(`${config.apiUrl}/signup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(signupPayload), + }).catch(() => { + onSignupApiError(); + return null; + }); + + if (res && res.ok) { + if (res.status === 204 || instance.emailRequiredForSignup) { os.alert({ type: 'success', title: i18n.ts._signup.almostThere, @@ -287,28 +305,35 @@ async function onSubmit(): Promise<void> { }); emit('approvalPending'); } else { - const res = await misskeyApi('signin', { - username: username.value, - password: password.value, - }); - emit('signup', res); + const resJson = (await res.json()) as Misskey.entities.SignupResponse; + if (_DEV_) console.log(resJson); + + emit('signup', resJson); if (props.autoSet) { - return login(res.i); + await login(resJson.token); } } - } catch { - submitting.value = false; - hcaptcha.value?.reset?.(); - recaptcha.value?.reset?.(); - turnstile.value?.reset?.(); - fc.value?.reset?.(); - - os.alert({ - type: 'error', - text: i18n.ts.somethingHappened, - }); + } else { + onSignupApiError(); } + + submitting.value = false; +} + +function onSignupApiError() { + submitting.value = false; + hcaptcha.value?.reset?.(); + mcaptcha.value?.reset?.(); + recaptcha.value?.reset?.(); + turnstile.value?.reset?.(); + fc.value?.reset?.(); + testcaptcha.value?.reset?.(); + + os.alert({ + type: 'error', + text: i18n.ts.somethingHappened, + }); } </script> @@ -317,8 +342,8 @@ async function onSubmit(): Promise<void> { padding: 16px; text-align: center; font-size: 26px; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); } .captcha { diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index 251c805401..06481b808c 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-if="availableServerRules" :defaultOpen="true"> <template #label>{{ i18n.ts.serverRules }}</template> - <template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template> + <template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template> <ol class="_gaps_s" :class="$style.rules"> <li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="sanitizeHtml(item)"></div></li> @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-if="availableTos || availablePrivacyPolicy" :defaultOpen="true"> <template #label>{{ tosPrivacyPolicyLabel }}</template> - <template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template> + <template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template> <div class="_gaps_s"> <div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div> <div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div> @@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template> - <template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template> + <template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template> <a href="https://activitypub.software/TransFem-org/Sharkey/-/blob/stable/IMPORTANT_NOTES.md" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a> @@ -151,8 +151,8 @@ async function updateAgreeNote(v: boolean) { padding: 16px; text-align: center; font-size: 26px; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); } .rules { @@ -171,19 +171,19 @@ async function updateAgreeNote(v: boolean) { flex-shrink: 0; display: flex; position: sticky; - top: calc(var(--stickyTop, 0px) + 8px); + top: calc(var(--MI-stickyTop, 0px) + 8px); counter-increment: item; content: counter(item); width: 32px; height: 32px; line-height: 32px; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); font-size: 13px; font-weight: bold; align-items: center; justify-content: center; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); } } diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue index 91e7d5dd53..291c3ecc2f 100644 --- a/packages/frontend/src/components/MkSignupDialog.vue +++ b/packages/frontend/src/components/MkSignupDialog.vue @@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only ref="dialog" :width="500" :height="600" - @close="dialog?.close()" - @closed="$emit('closed')" + @close="onClose" + @closed="emit('closed')" > <template #header>{{ i18n.ts.signup }}</template> @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveToClass="$style.transition_x_leaveTo" > <template v-if="!isAcceptedServerRule"> - <XServerRules @done="isAcceptedServerRule = true" @cancel="dialog?.close()"/> + <XServerRules @done="isAcceptedServerRule = true" @cancel="onClose"/> </template> <template v-else> <XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending" @approvalPending="onApprovalPending"/> @@ -47,7 +47,8 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'done', res: Misskey.entities.SigninResponse): void; + (ev: 'done', res: Misskey.entities.SignupResponse): void; + (ev: 'cancelled'): void; (ev: 'closed'): void; }>(); @@ -55,7 +56,12 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); const isAcceptedServerRule = ref(false); -function onSignup(res: Misskey.entities.SigninResponse) { +function onClose() { + emit('cancelled'); + dialog.value?.close(); +} + +function onSignup(res: Misskey.entities.SignupResponse) { emit('done', res); dialog.value?.close(); } diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue index 7743a89242..84dc244b23 100644 --- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue +++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue @@ -63,22 +63,22 @@ function close() { .root { position: fixed; z-index: v-bind(zIndex); - bottom: var(--margin); + bottom: var(--MI-margin); left: 0; right: 0; margin: auto; box-sizing: border-box; - width: calc(100% - (var(--margin) * 2)); + width: calc(100% - (var(--MI-margin) * 2)); max-width: 500px; display: flex; - backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } .icon { text-align: center; padding-top: 25px; width: 100px; - color: var(--accent); + color: var(--MI_THEME-accent); } @media (max-width: 500px) { .icon { diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 6bd00fcc2a..a32fd53c51 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -110,20 +110,20 @@ watch(() => props.expandAllCws, (expandAllCws) => { left: 0; width: 100%; height: 64px; - // background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + // background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; - background: var(--panel); + background: var(--MI_THEME-panel); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } &:hover { > .fadeLabel { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } } } @@ -132,18 +132,18 @@ watch(() => props.expandAllCws, (expandAllCws) => { .reply { margin-right: 6px; - color: var(--accent); + color: var(--MI_THEME-accent); } .rp { margin-left: 4px; font-style: oblique; - color: var(--renote); + color: var(--MI_THEME-renote); } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -152,7 +152,7 @@ watch(() => props.expandAllCws, (expandAllCws) => { width: 100%; margin-top: 14px; position: sticky; - bottom: calc(var(--stickyBottom, 0px) - 100px); + bottom: calc(var(--MI-stickyBottom, 0px) - 100px); } .playMFMButton { @@ -161,10 +161,10 @@ watch(() => props.expandAllCws, (expandAllCws) => { .showLessLabel { display: inline-block; - background: var(--popup); + background: var(--MI_THEME-popup); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 430e3c7958..c9c173aa35 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -28,11 +28,38 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> -<script lang="ts" setup> -import { } from 'vue'; +<script lang="ts"> +export type SuperMenuDef = { + title?: string; + items: ({ + type: 'a'; + href: string; + target?: string; + icon?: string; + text: string; + danger?: boolean; + active?: boolean; + } | { + type: 'button'; + icon?: string; + text: string; + danger?: boolean; + active?: boolean; + action: (ev: MouseEvent) => void; + } | { + type: 'link'; + to: string; + icon?: string; + text: string; + danger?: boolean; + active?: boolean; + })[]; +}; +</script> +<script lang="ts" setup> defineProps<{ - def: any[]; + def: SuperMenuDef[]; grid?: boolean; }>(); </script> @@ -43,7 +70,7 @@ defineProps<{ & + .group { margin-top: 16px; padding-top: 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } > .title { @@ -59,12 +86,12 @@ defineProps<{ width: 100%; box-sizing: border-box; padding: 9px 16px 9px 8px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); font-size: 0.9em; &:hover { text-decoration: none; - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } &:focus-visible { @@ -72,12 +99,12 @@ defineProps<{ } &.active { - color: var(--accent); - background: var(--accentedBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-accentedBg); } &.danger { - color: var(--error); + color: var(--MI_THEME-error); } > .icon { @@ -128,10 +155,10 @@ defineProps<{ &:hover { text-decoration: none; background: none; - color: var(--accent); + color: var(--MI_THEME-accent); > .icon { - background: var(--accentedBg); + background: var(--MI_THEME-accentedBg); } } @@ -144,8 +171,8 @@ defineProps<{ width: 60px; height: 60px; aspect-ratio: 1; - background: var(--panel); - border-radius: var(--radius-full); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-full); } > .text { diff --git a/packages/frontend/src/components/MkSwitch.button.vue b/packages/frontend/src/components/MkSwitch.button.vue index f7c413e1d3..581aa4e644 100644 --- a/packages/frontend/src/components/MkSwitch.button.vue +++ b/packages/frontend/src/components/MkSwitch.button.vue @@ -51,18 +51,18 @@ const toggle = () => { width: calc(var(--height) * 1.6); height: calc(var(--height) + 2px); // 枠線 outline: none; - background: var(--switchOffBg); + background: var(--MI_THEME-switchOffBg); background-clip: content-box; - border: solid 1px var(--switchOffBg); - border-radius: var(--radius-ellipse); + border: solid 1px var(--MI_THEME-switchOffBg); + border-radius: var(--MI-radius-ellipse); cursor: pointer; transition: inherit; user-select: none; } .buttonChecked { - background-color: var(--switchOnBg) !important; - border-color: var(--switchOnBg) !important; + background-color: var(--MI_THEME-switchOnBg) !important; + border-color: var(--MI_THEME-switchOnBg) !important; } .buttonDisabled { @@ -75,17 +75,17 @@ const toggle = () => { top: 3px; width: calc(var(--height) - 6px); height: calc(var(--height) - 6px); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); transition: all 0.2s ease; &:not(.knobChecked) { left: 3px; - background: var(--switchOffFg); + background: var(--MI_THEME-switchOffFg); } } .knobChecked { left: calc(calc(100% - var(--height)) + 3px); - background: var(--switchOnFg); + background: var(--MI_THEME-switchOnFg); } </style> diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue index a0994d9cc9..5e6029ee40 100644 --- a/packages/frontend/src/components/MkSwitch.vue +++ b/packages/frontend/src/components/MkSwitch.vue @@ -59,7 +59,7 @@ const toggle = () => { &:hover { > .button { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } @@ -77,7 +77,7 @@ const toggle = () => { margin: 0; &:focus-visible ~ .toggle { - outline: 2px solid var(--focus); + outline: 2px solid var(--MI_THEME-focus); outline-offset: 2px; } } @@ -87,7 +87,7 @@ const toggle = () => { margin-top: 2px; display: block; transition: inherit; - color: var(--fg); + color: var(--MI_THEME-fg); } .label { @@ -99,7 +99,7 @@ const toggle = () => { .caption { margin: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); font-size: 0.85em; &:empty { diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue index ec3b1c90ca..485d003f93 100644 --- a/packages/frontend/src/components/MkSystemWebhookEditor.vue +++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue @@ -55,6 +55,18 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.userCreated)" @click="test('userCreated')"><i class="ti ti-send"></i></MkButton> </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="events.inactiveModeratorsWarning" :disabled="disabledEvents.inactiveModeratorsWarning"> + <template #label>{{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsWarning }}</template> + </MkSwitch> + <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsWarning)" @click="test('inactiveModeratorsWarning')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="events.inactiveModeratorsInvitationOnlyChanged" :disabled="disabledEvents.inactiveModeratorsInvitationOnlyChanged"> + <template #label>{{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsInvitationOnlyChanged }}</template> + </MkSwitch> + <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsInvitationOnlyChanged)" @click="test('inactiveModeratorsInvitationOnlyChanged')"><i class="ti ti-send"></i></MkButton> + </div> </div> <div v-show="mode === 'edit'" :class="$style.description"> @@ -100,6 +112,8 @@ type EventType = { abuseReport: boolean; abuseReportResolved: boolean; userCreated: boolean; + inactiveModeratorsWarning: boolean; + inactiveModeratorsInvitationOnlyChanged: boolean; } const emit = defineEmits<{ @@ -123,6 +137,8 @@ const events = ref<EventType>({ abuseReport: true, abuseReportResolved: true, userCreated: true, + inactiveModeratorsWarning: true, + inactiveModeratorsInvitationOnlyChanged: true, }); const isActive = ref<boolean>(true); @@ -130,6 +146,8 @@ const disabledEvents = ref<EventType>({ abuseReport: false, abuseReportResolved: false, userCreated: false, + inactiveModeratorsWarning: false, + inactiveModeratorsInvitationOnlyChanged: false, }); const disableSubmitButton = computed(() => { @@ -261,10 +279,10 @@ onMounted(async () => { bottom: 0; left: 0; padding: 12px; - border-top: solid 0.5px var(--divider); - background: var(--acrylicBg); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + border-top: solid 0.5px var(--MI_THEME-divider); + background: var(--MI_THEME-acrylicBg); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } .switchBox { @@ -289,6 +307,6 @@ onMounted(async () => { .description { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } </style> diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue index 54ab8fc663..52e4d304bc 100644 --- a/packages/frontend/src/components/MkTab.vue +++ b/packages/frontend/src/components/MkTab.vue @@ -39,7 +39,7 @@ export default defineComponent({ > button { flex: 1; padding: 10px 8px; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); &:disabled { opacity: 1 !important; @@ -47,13 +47,13 @@ export default defineComponent({ } &.active { - color: var(--accent); - background: var(--accentedBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-accentedBg); } &:not(.active):hover { - color: var(--fgHighlighted); - background: var(--panelHighlight); + color: var(--MI_THEME-fgHighlighted); + background: var(--MI_THEME-panelHighlight); } &:not(:first-child) { diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue index 6b9c181597..87aa046963 100644 --- a/packages/frontend/src/components/MkTagCloud.vue +++ b/packages/frontend/src/components/MkTagCloud.vue @@ -33,7 +33,7 @@ watch(available, () => { try { window.TagCanvas.Start(idForCanvas, idForTags, { textColour: '#ffffff', - outlineColour: tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(), + outlineColour: tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(), outlineRadius: 10, initial: [-0.030, -0.010], frontSelect: true, diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue index 72d6e12656..9deb6528d1 100644 --- a/packages/frontend/src/components/MkTextarea.vue +++ b/packages/frontend/src/components/MkTextarea.vue @@ -159,7 +159,7 @@ onUnmounted(() => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -179,23 +179,23 @@ onUnmounted(() => { font: inherit; font-weight: normal; font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + color: var(--MI_THEME-fg); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); outline: none; box-shadow: none; box-sizing: border-box; transition: border-color 0.1s ease-out; &:hover { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } .focused { > .textarea { - border-color: var(--accent) !important; + border-color: var(--MI_THEME-accent) !important; } } @@ -226,7 +226,7 @@ onUnmounted(() => { .mfmPreview { padding: 12px; - border-radius: var(--radius); + border-radius: var(--MI-radius); box-sizing: border-box; min-height: 130px; pointer-events: none; diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index b69c19eb9e..7a9abab62e 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -39,10 +39,12 @@ const props = withDefaults(defineProps<{ withRenotes?: boolean; withReplies?: boolean; withBots?: boolean; + withSensitive?: boolean; onlyFiles?: boolean; }>(), { withRenotes: true, withReplies: false, + withSensitive: true, onlyFiles: false, withBots: true, }); @@ -53,6 +55,7 @@ const emit = defineEmits<{ }>(); provide('inTimeline', true); +provide('tl_withSensitive', computed(() => props.withSensitive)); provide('inChannel', computed(() => props.src === 'channel')); type TimelineQueryType = { @@ -275,6 +278,9 @@ function refreshEndpointAndChannel() { // IDが切り替わったら切り替え先のTLを表示させたい watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel); +// withSensitiveはクライアントで完結する処理のため、単にリロードするだけでOK +watch(() => props.withSensitive, reloadTimeline); + // 初回表示用 refreshEndpointAndChannel(); diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue index f731b3264f..38b537cbc9 100644 --- a/packages/frontend/src/components/MkToast.vue +++ b/packages/frontend/src/components/MkToast.vue @@ -70,7 +70,7 @@ onMounted(() => { max-width: calc(100% - 32px); width: min-content; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); overflow: clip; text-align: center; pointer-events: none; diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue index b32066c950..73aef68964 100644 --- a/packages/frontend/src/components/MkTokenGenerateWindow.vue +++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only :okButtonDisabled="false" :canClose="false" @close="dialog?.close()" - @closed="$emit('closed')" + @closed="emit('closed')" @ok="ok()" > <template #header>{{ title || i18n.ts.generateAccessToken }}</template> @@ -136,15 +136,15 @@ function enableAll(): void { .adminPermissions { margin: 8px -6px 0; padding: 24px 6px 6px; - border: 2px solid var(--error); - border-radius: calc(var(--radius) / 2); + border: 2px solid var(--MI_THEME-error); + border-radius: calc(var(--MI-radius) / 2); } .adminPermissionsHeader { margin: -34px 0 6px 12px; padding: 0 4px; width: fit-content; - color: var(--error); - background: var(--panel); + color: var(--MI_THEME-error); + background: var(--MI_THEME-panel); } </style> diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue index aac07008a4..22e74aa6d1 100644 --- a/packages/frontend/src/components/MkTooltip.vue +++ b/packages/frontend/src/components/MkTooltip.vue @@ -109,8 +109,8 @@ onUnmounted(() => { padding: 8px 12px; box-sizing: border-box; text-align: center; - border-radius: var(--radius-xs); - border: solid 0.5px var(--divider); + border-radius: var(--MI-radius-xs); + border: solid 0.5px var(--MI_THEME-divider); pointer-events: none; transform-origin: center center; } diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue index cec7d69943..53b8db38b2 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Note.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only </I18n> <MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/> <div v-if="onceReacted"> - <b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br> + <b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br> <I18n :src="i18n.ts._initialTutorial._reaction.reactDone"> <template #undo> <i class="ph-minus ph-bold ph-lg"></i> @@ -116,13 +116,13 @@ function removeReaction(emoji) { <style lang="scss" module> .exampleNoteRoot { - border-radius: var(--radius); - border: var(--panelBorder); - background: var(--panel); + border-radius: var(--MI-radius); + border: var(--MI_THEME-panelBorder); + background: var(--MI_THEME-panel); } .divider { height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue index a9014d4202..367c573617 100644 --- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue +++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue @@ -81,14 +81,14 @@ const exampleCWNote = reactive<Misskey.entities.Note>({ <style lang="scss" module> .exampleRoot { max-width: none!important; - border-radius: var(--radius); - border: var(--panelBorder); - background: var(--panel); + border-radius: var(--MI-radius); + border: var(--MI_THEME-panelBorder); + background: var(--MI_THEME-panel); } .divider { height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .image { @@ -101,7 +101,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({ display: block; width: 100%; height: 40px; - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); font-weight: bold; text-align: left; @@ -116,8 +116,8 @@ const exampleCWNote = reactive<Misskey.entities.Note>({ left: 0; right: 0; bottom: 0; - border-radius: var(--radius-ellipse); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + border-radius: var(--MI-radius-ellipse); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); } } diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue index 322082f5a0..e1fc3e4f26 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only :initialNote="exampleNote" @fileChangeSensitive="doSucceeded" ></MkPostForm> - <div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div> + <div v-if="onceSucceeded"><b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div> <MkFolder> <template #label>{{ i18n.ts.previewNoteText }}</template> <MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote> @@ -91,14 +91,14 @@ const exampleNote = reactive<Misskey.entities.Note>({ <style lang="scss" module> .exampleRoot { - border-radius: var(--radius); - border: var(--panelBorder); - background: var(--panel); + border-radius: var(--MI-radius); + border: var(--MI_THEME-panelBorder); + background: var(--MI_THEME-panel); } .divider { height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .image { @@ -111,7 +111,7 @@ const exampleNote = reactive<Misskey.entities.Note>({ display: block; width: 100%; height: 40px; - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); font-weight: bold; text-align: left; @@ -126,8 +126,8 @@ const exampleNote = reactive<Misskey.entities.Note>({ left: 0; right: 0; bottom: 0; - border-radius: var(--radius-ellipse); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + border-radius: var(--MI-radius-ellipse); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); } } diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue index b900a30c85..931b7343c5 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue @@ -31,14 +31,14 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js'; <style lang="scss" module> .exampleNoteRoot { - border-radius: var(--radius); - border: var(--panelBorder); - background: var(--panel); + border-radius: var(--MI-radius); + border: var(--MI_THEME-panelBorder); + background: var(--MI_THEME-panel); } .divider { height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .image { @@ -51,7 +51,7 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js'; display: block; width: 100%; height: 40px; - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); font-weight: bold; text-align: left; @@ -66,8 +66,8 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js'; left: 0; right: 0; bottom: 0; - border-radius: var(--radius-ellipse); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + border-radius: var(--MI-radius-ellipse); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); } } diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue index 1f5a2b9381..11d7c8dc4d 100644 --- a/packages/frontend/src/components/MkTutorialDialog.vue +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialTutorial._landing.title }}</div> <div>{{ i18n.ts._initialTutorial._landing.description }}</div> <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton> @@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div> <I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;"> <template #link> @@ -223,7 +223,7 @@ async function close(skip: boolean) { .progressBarValue { height: 100%; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); transition: all 0.5s cubic-bezier(0,.5,.5,1); } @@ -253,7 +253,7 @@ async function close(skip: boolean) { left: 0; flex-shrink: 0; padding: 12px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); -webkit-backdrop-filter: blur(15px); backdrop-filter: blur(15px); } diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index 91f5b86c2d..7cafb1b0af 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -46,8 +46,8 @@ onMounted(() => { max-width: 480px; box-sizing: border-box; text-align: center; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); } .title { diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 04f5314463..0808a052cd 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -84,13 +84,13 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue'; -import type { summaly } from '@misskey-dev/summaly'; import { url as local } from '@@/js/config.js'; +import { versatileLang } from '@@/js/intl-const.js'; +import type { summaly } from '@misskey-dev/summaly'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { deviceKind } from '@/scripts/device-kind.js'; import MkButton from '@/components/MkButton.vue'; -import { versatileLang } from '@@/js/intl-const.js'; import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; import { defaultStore } from '@/store.js'; @@ -180,7 +180,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa sensitive.value = info.sensitive ?? false; }); -function adjustTweetHeight(message: any) { +function adjustTweetHeight(message: MessageEvent) { if (message.origin !== 'https://platform.twitter.com') return; const embed = message.data?.['twttr.embed']; if (embed?.method !== 'twttr.private.resize') return; @@ -193,14 +193,16 @@ function openPlayer(): void { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), { url: requestUrl.href, }, { - // TODO + closed: () => { + dispose(); + }, }); } -(window as any).addEventListener('message', adjustTweetHeight); +window.addEventListener('message', adjustTweetHeight); onUnmounted(() => { - (window as any).removeEventListener('message', adjustTweetHeight); + window.removeEventListener('message', adjustTweetHeight); }); </script> @@ -219,7 +221,7 @@ onUnmounted(() => { height: 1.5em; padding: 0; margin: 0; - color: var(--fg); + color: var(--MI_THEME-fg); background: rgba(128, 128, 128, 0.2); opacity: 0.7; @@ -240,8 +242,8 @@ onUnmounted(() => { position: relative; display: block; font-size: 14px; - box-shadow: 0 0 0 1px var(--divider); - border-radius: var(--radius-sm); + box-shadow: 0 0 0 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-sm); overflow: clip; &:hover { @@ -270,7 +272,7 @@ onUnmounted(() => { height: 100%; background-position: center; background-size: cover; - background-color: var(--bg); + background-color: var(--MI_THEME-bg); display: flex; justify-content: center; align-items: center; @@ -317,7 +319,6 @@ onUnmounted(() => { .siteName { display: inline-block; margin: 0; - color: var(--urlPreviewInfo); font-size: 0.8em; line-height: 16px; vertical-align: top; diff --git a/packages/frontend/src/components/MkUrlWarningDialog.vue b/packages/frontend/src/components/MkUrlWarningDialog.vue index f2e9b11fd9..3bec6eecdd 100644 --- a/packages/frontend/src/components/MkUrlWarningDialog.vue +++ b/packages/frontend/src/components/MkUrlWarningDialog.vue @@ -94,7 +94,7 @@ onBeforeUnmount(() => { min-width: 320px; max-width: 480px; box-sizing: border-box; - background: var(--panel); + background: var(--MI_THEME-panel); border-radius: 16px; } @@ -106,7 +106,7 @@ onBeforeUnmount(() => { .icon { font-size: 18px; - color: var(--warn); + color: var(--MI_THEME-warn); } .title { @@ -117,7 +117,7 @@ onBeforeUnmount(() => { .urlAddress { padding: 10px 14px; border-radius: 8px; - border: 1px solid var(--divider); + border: 1px solid var(--MI_THEME-divider); overflow-x: auto; white-space: nowrap; } diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index 3c5f563aa0..fe499fabbf 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="dialog" :width="400" @close="dialog?.close()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template v-if="announcement" #header>:{{ announcement.title }}:</template> <template v-else #header>New announcement</template> @@ -25,9 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkRadios v-model="icon"> <template #label>{{ i18n.ts.icon }}</template> <option value="info"><i class="ti ti-info-circle"></i></option> - <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option> - <option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option> - <option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option> + <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option> + <option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option> + <option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option> </MkRadios> <MkRadios v-model="display"> <template #label>{{ i18n.ts.display }}</template> @@ -62,9 +62,16 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkRadios from '@/components/MkRadios.vue'; +type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; } + const props = defineProps<{ user: Misskey.entities.User, - announcement?: Misskey.entities.Announcement, + announcement?: Required<AdminAnnouncementType>, +}>(); + +const emit = defineEmits<{ + (ev: 'done', v: { deleted?: boolean; updated?: AdminAnnouncementType; created?: AdminAnnouncementType; }): void, + (ev: 'closed'): void }>(); const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null); @@ -74,11 +81,6 @@ const icon = ref(props.announcement ? props.announcement.icon : 'info'); const display = ref(props.announcement ? props.announcement.display : 'dialog'); const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false); -const emit = defineEmits<{ - (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, - (ev: 'closed'): void -}>(); - async function done() { const params = { title: title.value, @@ -88,7 +90,7 @@ async function done() { display: display.value, needConfirmationToRead: needConfirmationToRead.value, userId: props.user.id, - }; + } satisfies Misskey.entities.AdminAnnouncementsCreateRequest; if (props.announcement) { await os.apiWithDialog('admin/announcements/update', { @@ -141,8 +143,8 @@ async function del() { bottom: 0; left: 0; padding: 12px; - border-top: solid 0.5px var(--divider); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + border-top: solid 0.5px var(--MI_THEME-divider); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } </style> diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue index 603f9f2435..ce28f6ec5e 100644 --- a/packages/frontend/src/components/MkUserCardMini.vue +++ b/packages/frontend/src/components/MkUserCardMini.vue @@ -23,7 +23,7 @@ import { acct } from '@/filters/user.js'; const props = withDefaults(defineProps<{ user: Misskey.entities.User; - withChart: boolean; + withChart?: boolean; }>(), { withChart: true, }); @@ -49,8 +49,8 @@ $bodyInfoHieght: 16px; display: flex; align-items: center; padding: 16px; - background: var(--panel); - border-radius: var(--radius-sm); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); } .avatar { @@ -64,7 +64,7 @@ $bodyInfoHieght: 16px; flex: 1; overflow: hidden; font-size: 0.9em; - color: var(--fg); + color: var(--MI_THEME-fg); padding-right: 8px; } diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index 73cdd9ce00..a6bbacacee 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -75,9 +75,9 @@ defineProps<{ top: 62px; left: 13px; z-index: 2; - width: var(--avatar); - height: var(--avatar); - border: solid 4px var(--panel); + width: var(--MI-avatar); + height: var(--MI-avatar); + border: solid 4px var(--MI_THEME-panel); } .title { @@ -98,7 +98,7 @@ defineProps<{ margin: 0; line-height: 16px; font-size: 0.8em; - color: var(--fg); + color: var(--MI_THEME-fg); opacity: 0.7; } @@ -110,13 +110,13 @@ defineProps<{ color: #fff; background: rgba(0, 0, 0, 0.7); font-size: 0.7em; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .description { padding: 16px; font-size: 0.8em; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .mfm { @@ -128,7 +128,7 @@ defineProps<{ .status { padding: 10px 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .statusItem { @@ -139,12 +139,12 @@ defineProps<{ .statusItemLabel { margin: 0; font-size: 0.7em; - color: var(--fg); + color: var(--MI_THEME-fg); } .statusItemValue { font-size: 1em; - color: var(--accent); + color: var(--MI_THEME-accent); } .follow { @@ -169,7 +169,7 @@ defineProps<{ color: #fff; background: rgba(0, 0, 0, 0.7); font-size: 0.7em; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); list-style-type: none; margin-left: 0; } diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue index ac82ecc3d6..8dc01a08ab 100644 --- a/packages/frontend/src/components/MkUserList.vue +++ b/packages/frontend/src/components/MkUserList.vue @@ -39,6 +39,6 @@ const props = withDefaults(defineProps<{ .root { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - grid-gap: var(--margin); + grid-gap: var(--MI-margin); } </style> diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue index 9f04353f62..7a6ded110a 100644 --- a/packages/frontend/src/components/MkUserOnlineIndicator.vue +++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue @@ -36,11 +36,11 @@ const text = computed(() => { <style lang="scss" module> .root { - box-shadow: 0 0 0 3px var(--panel); + box-shadow: 0 0 0 3px var(--MI_THEME-panel); // sharkey: the comment mentions something about 100% radius not behaving correctly on blink. // couldn't reproduce, assuming the 120% here was just an old workaround - border-radius: var(--radius-full); // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる + border-radius: var(--MI-radius-full); // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる &.status_online { background: #58d4c9; diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index 40199c12ff..73e38bef09 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <svg viewBox="0 0 128 128" :class="$style.avatarBack"> <g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)"> - <path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--popup);"/> + <path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--MI_THEME-popup);"/> </g> </svg> <MkAvatar :class="$style.avatar" :user="user" indicator/> @@ -162,7 +162,7 @@ onMounted(() => { color: #fff; background: rgba(0, 0, 0, 0.7); font-size: 0.7em; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .locked:first-child { @@ -173,7 +173,7 @@ onMounted(() => { color: #fff; background: rgba(0, 0, 0, 0.7); font-size: 0.7em; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); } .locked:not(:first-child) { @@ -184,7 +184,7 @@ onMounted(() => { color: #fff; background: rgba(0, 0, 0, 0.7); font-size: 0.7em; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); } .avatarBack { @@ -204,8 +204,8 @@ onMounted(() => { right: 0; margin: 0 auto; z-index: 2; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); } .title { @@ -233,15 +233,15 @@ onMounted(() => { padding: 16px 26px; font-size: 0.8em; text-align: center; - border-top: solid 1px var(--divider); - border-bottom: solid 1px var(--divider); + border-top: solid 1px var(--MI_THEME-divider); + border-bottom: solid 1px var(--MI_THEME-divider); } .fields { font-size: 0.8em; padding: 16px; - border-top: solid 1px var(--divider); - border-bottom: solid 1px var(--divider); + border-top: solid 1px var(--MI_THEME-divider); + border-bottom: solid 1px var(--MI_THEME-divider); } .field { @@ -298,7 +298,7 @@ onMounted(() => { .statusItemLabel { font-size: 0.7em; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } .menu { @@ -306,8 +306,8 @@ onMounted(() => { top: 8px; right: 44px; padding: 6px; - background: var(--panel); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-ellipse); } .follow { diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index a5b48c8ce2..85d4666172 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only @click="cancel()" @close="cancel()" @ok="ok()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header>{{ i18n.ts.selectUser }}</template> <div> @@ -196,11 +196,11 @@ onMounted(() => { font-size: 14px; &:hover { - background: var(--X7); + background: var(--MI_THEME-X7); } &.selected { - background: var(--accent); + background: var(--MI_THEME-accent); color: #fff; } } diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue index 1524ea0ec9..5153c06139 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue @@ -62,7 +62,7 @@ const popularUsers: Paging = { .users { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); - grid-gap: var(--margin); + grid-gap: var(--MI-margin); justify-content: center; } </style> diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index 3194641cdb..7cb48f6afb 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -51,6 +51,11 @@ watch(name, () => { // 空文字列をnullにしたいので??は使うな // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing name: name.value || null, + }, undefined, { + '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': { + title: i18n.ts.yourNameContainsProhibitedWords, + text: i18n.ts.yourNameContainsProhibitedWordsDescription, + }, }); }); diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue index c80349d034..4c4f4989c5 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.User.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue @@ -59,9 +59,9 @@ async function follow() { top: 30px; left: 13px; z-index: 2; - width: var(--avatar); - height: var(--avatar); - border: solid 4px var(--panel); + width: var(--MI-avatar); + height: var(--MI-avatar); + border: solid 4px var(--MI_THEME-panel); } .title { @@ -82,7 +82,7 @@ async function follow() { margin: 0; line-height: 16px; font-size: 0.8em; - color: var(--fg); + color: var(--MI_THEME-fg); opacity: 0.7; } @@ -99,7 +99,7 @@ async function follow() { } .footer { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); padding: 16px; } </style> diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index 1fb1eda039..b7261129ef 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div> <div>{{ i18n.ts._initialAccountSetting.letsStartAccountSetup }}</div> <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton> @@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.centerPage"> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i> <div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div> <div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div> <MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/> @@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div> <div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div> <div class="_buttonsCenter" style="margin-top: 16px;"> @@ -223,7 +223,7 @@ async function later(later: boolean) { .progressBarValue { height: 100%; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); transition: all 0.5s cubic-bezier(0,.5,.5,1); } @@ -252,7 +252,7 @@ async function later(later: boolean) { left: 0; flex-shrink: 0; padding: 12px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); -webkit-backdrop-filter: blur(15px); backdrop-filter: blur(15px); } diff --git a/packages/frontend/src/components/MkUsersTooltip.vue b/packages/frontend/src/components/MkUsersTooltip.vue index 054a503257..0cb7f22e93 100644 --- a/packages/frontend/src/components/MkUsersTooltip.vue +++ b/packages/frontend/src/components/MkUsersTooltip.vue @@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import * as Misskey from 'misskey-js'; import MkTooltip from './MkTooltip.vue'; defineProps<{ showing: boolean; - users: any[]; // TODO + users: Misskey.entities.UserLite[]; count: number; targetElement: HTMLElement; }>(); diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue index 3c3f9e94b6..5624abbd33 100644 --- a/packages/frontend/src/components/MkVisibilityPicker.vue +++ b/packages/frontend/src/components/MkVisibilityPicker.vue @@ -82,7 +82,7 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void { &.asDrawer { padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; width: 100%; - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); border-bottom-right-radius: 0; border-bottom-left-radius: 0; @@ -124,7 +124,7 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void { } &.active { - color: var(--accent); + color: var(--MI_THEME-accent); } } diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue index cab42cd59d..d098dad9a1 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue @@ -62,7 +62,7 @@ async function renderChart() { const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const computedStyle = getComputedStyle(document.documentElement); - const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(); + const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(); const colorRead = accent; const colorWrite = '#2ecc71'; diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index 874eff6c79..54f2ee655c 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -110,8 +110,8 @@ function showMenu(ev: MouseEvent) { .panel { position: relative; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); box-shadow: 0 12px 32px rgb(0 0 0 / 25%); } @@ -140,7 +140,7 @@ function showMenu(ev: MouseEvent) { right: 16px; width: 32px; height: 32px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); font-size: 18px; z-index: 50; } @@ -191,14 +191,14 @@ function showMenu(ev: MouseEvent) { } .statsItemLabel { - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); font-size: 0.9em; } .statsItemCount { font-weight: bold; font-size: 1.2em; - color: var(--accent); + color: var(--MI_THEME-accent); } .tl { @@ -207,7 +207,7 @@ function showMenu(ev: MouseEvent) { .tlHeader { padding: 12px 16px; - border-bottom: solid 1px var(--divider); + border-bottom: solid 1px var(--MI_THEME-divider); } .tlBody { diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue index 60b75b6d30..34fa6b0723 100644 --- a/packages/frontend/src/components/MkWaitingDialog.vue +++ b/packages/frontend/src/components/MkWaitingDialog.vue @@ -47,8 +47,8 @@ watch(() => props.showing, () => { padding: 32px; box-sizing: border-box; text-align: center; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); width: 250px; &.iconOnly { @@ -65,7 +65,7 @@ watch(() => props.showing, () => { font-size: 32px; &.success { - color: var(--accent); + color: var(--MI_THEME-accent); } &.waiting { diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index 99840bf8d7..b987283a65 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -7,12 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.root"> <template v-if="edit"> <header :class="$style.editHeader"> - <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select> + <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select> <template #label>{{ i18n.ts.selectWidget }}</template> <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option> </MkSelect> <MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> - <MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton> + <MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton> </header> <Sortable :modelValue="props.widgets" @@ -123,7 +123,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { .widget { contain: content; - margin: var(--margin) 0; + margin: var(--MI-margin) 0; &:first-of-type { margin-top: 0; @@ -158,7 +158,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { height: 32px; color: #fff; background: rgba(#000, 0.7); - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); } &Config { diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 08906a1205..2953f656d4 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only :enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''" :leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''" appear - @afterLeave="$emit('closed')" + @afterLeave="emit('closed')" > <div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]"> <div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown"> @@ -54,12 +54,19 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue'; +import type { MenuItem } from '@/types/menu.js'; import contains from '@/scripts/contains.js'; import * as os from '@/os.js'; -import type { MenuItem } from '@/types/menu.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; +type WindowButton = { + title: string; + icon: string; + onClick: () => void; + highlighted?: boolean; +}; + const minHeight = 50; const minWidth = 250; @@ -87,8 +94,8 @@ const props = withDefaults(defineProps<{ mini?: boolean; front?: boolean; contextmenu?: MenuItem[] | null; - buttonsLeft?: any[]; - buttonsRight?: any[]; + buttonsLeft?: WindowButton[]; + buttonsRight?: WindowButton[]; }>(), { initialWidth: 400, initialHeight: null, @@ -484,6 +491,10 @@ defineExpose({ } .root { + // universal.vueとかで直接--MI-stickyBottomが定義されていたりするのでリセット + --MI-stickyTop: 0; + --MI-stickyBottom: 0; + position: fixed; top: 0; left: 0; @@ -502,7 +513,7 @@ defineExpose({ contain: content; width: 100%; height: 100%; - border-radius: var(--radius); + border-radius: var(--MI-radius); } .header { @@ -514,10 +525,10 @@ defineExpose({ flex-shrink: 0; user-select: none; height: var(--height); - background: var(--windowHeader); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - //border-bottom: solid 1px var(--divider); + background: var(--MI_THEME-windowHeader); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + //border-bottom: solid 1px var(--MI_THEME-divider); font-size: 90%; font-weight: bold; @@ -531,11 +542,11 @@ defineExpose({ width: var(--height); &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } &.highlighted { - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -560,7 +571,7 @@ defineExpose({ .content { flex: 1; overflow: auto; - background: var(--panel); + background: var(--MI_THEME-panel); container-type: size; } diff --git a/packages/frontend/src/components/SkFlashPlayer.vue b/packages/frontend/src/components/SkFlashPlayer.vue index 739a7bc74c..2b61974ef7 100644 --- a/packages/frontend/src/components/SkFlashPlayer.vue +++ b/packages/frontend/src/components/SkFlashPlayer.vue @@ -244,9 +244,9 @@ onDeactivated(() => { } .hide { - border-radius: var(--radius-sm) !important; + border-radius: var(--MI-radius-sm) !important; background-color: black !important; - color: var(--accentLighten) !important; + color: var(--MI_THEME-accentLighten) !important; font-size: 12px !important; } @@ -260,9 +260,9 @@ onDeactivated(() => { > i { display: block; position: absolute; - border-radius: var(--radius-sm); - background-color: var(--fg); - color: var(--accentLighten); + border-radius: var(--MI-radius-sm); + background-color: var(--MI_THEME-fg); + color: var(--MI_THEME-accentLighten); font-size: 14px; opacity: .5; padding: 3px 6px; @@ -276,9 +276,9 @@ onDeactivated(() => { > .alt { display: block; position: absolute; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); background-color: black; - color: var(--accentLighten); + color: var(--MI_THEME-accentLighten); font-size: 0.8em; font-weight: bold; opacity: .5; @@ -311,10 +311,10 @@ onDeactivated(() => { justify-content: center; align-items: center; background: rgba(64, 64, 64, 0.3); - backdrop-filter: var(--modalBgFilter); + backdrop-filter: var(--MI-modalBgFilter); color: #fff; font-size: 12px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); position: absolute; z-index: 4; @@ -334,7 +334,7 @@ onDeactivated(() => { > .controls { display: flex; width: 100%; - background-color: var(--bg); + background-color: var(--MI_THEME-bg); z-index: 5; > * { @@ -344,12 +344,12 @@ onDeactivated(() => { > button, a { border: none; background-color: transparent; - color: var(--accent); + color: var(--MI_THEME-accent); text-decoration: none; cursor: pointer; &:hover { - background-color: var(--fg); + background-color: var(--MI_THEME-fg); } &:disabled { @@ -385,11 +385,11 @@ onDeactivated(() => { outline: none; &::-webkit-slider-runnable-track { - background: var(--bg); + background: var(--MI_THEME-bg); } &::-ms-fill-lower, &::-ms-fill-upper { - background: var(--bg); + background: var(--MI_THEME-bg); } } @@ -398,8 +398,8 @@ onDeactivated(() => { height: 100%; border-radius: 0; animate: 0.2s; - background: var(--bg); - border: 1px solid var(--fg); + background: var(--MI_THEME-bg); + border: 1px solid var(--MI_THEME-fg); overflow-x: hidden; } @@ -408,9 +408,9 @@ onDeactivated(() => { height: 100%; width: 14px; border-radius: 0; - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); -webkit-appearance: none; - box-shadow: calc(-100vw - 14px) 0 0 100vw var(--accent); + box-shadow: calc(-100vw - 14px) 0 0 100vw var(--MI_THEME-accent); clip-path: polygon(1px 0, 100% 0, 100% 100%, 1px 100%, 1px calc(50% + 10.5px), -100vw calc(50% + 10.5px), -100vw calc(50% - 10.5px), 0 calc(50% - 10.5px)); z-index: 1; } @@ -420,13 +420,13 @@ onDeactivated(() => { height: 100%; border-radius: 0; animate: 0.2s; - background: var(--bg); - border: 1px solid var(--fg); + background: var(--MI_THEME-bg); + border: 1px solid var(--MI_THEME-fg); } &::-moz-range-progress { height: 100%; - background: var(--accent); + background: var(--MI_THEME-accent); } &::-moz-range-thumb { @@ -434,7 +434,7 @@ onDeactivated(() => { height: 100%; border-radius: 0; width: 14px; - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); } &::-ms-track { @@ -448,14 +448,14 @@ onDeactivated(() => { } &::-ms-fill-lower { - background: var(--accent); - border: 1px solid var(--fg); + background: var(--MI_THEME-accent); + border: 1px solid var(--MI_THEME-fg); border-radius: 0; } &::-ms-fill-upper { - background: var(--bg); - border: 1px solid var(--fg); + background: var(--MI_THEME-bg); + border: 1px solid var(--MI_THEME-fg); border-radius: 0; } @@ -465,7 +465,7 @@ onDeactivated(() => { height: 100%; width: 14px; border-radius: 0; - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); } } } diff --git a/packages/frontend/src/components/SkFollowingRecentNotes.vue b/packages/frontend/src/components/SkFollowingRecentNotes.vue index 6daa8feba5..7e71065082 100644 --- a/packages/frontend/src/components/SkFollowingRecentNotes.vue +++ b/packages/frontend/src/components/SkFollowingRecentNotes.vue @@ -118,15 +118,15 @@ function checkMute(note: Misskey.entities.Note | undefined | null, mutes: Mutes) <style module lang="scss"> .panel { - background: var(--panel); + background: var(--MI_THEME-panel); } @keyframes border { from { - border-left: 0 solid var(--accent); + border-left: 0 solid var(--MI_THEME-accent); } to { - border-left: 6px solid var(--accent); + border-left: 6px solid var(--MI_THEME-accent); } } diff --git a/packages/frontend/src/components/SkInstanceTicker.vue b/packages/frontend/src/components/SkInstanceTicker.vue index eb987d9c77..2bfe5cc157 100644 --- a/packages/frontend/src/components/SkInstanceTicker.vue +++ b/packages/frontend/src/components/SkInstanceTicker.vue @@ -45,7 +45,7 @@ const bg = { display: flex; align-items: center; height: 1.5ex; - border-radius: var(--radius-xl); + border-radius: var(--MI-radius-xl); padding: 4px; overflow: clip; color: #fff; diff --git a/packages/frontend/src/components/SkMfmWindow.vue b/packages/frontend/src/components/SkMfmWindow.vue index c599531ec5..58ca3d38ab 100644 --- a/packages/frontend/src/components/SkMfmWindow.vue +++ b/packages/frontend/src/components/SkMfmWindow.vue @@ -491,12 +491,12 @@ const preview_fade = ref(`$[fade 🍮] $[fade.out 🍮] $[fade.speed=3s 🍮] $[ > .title { position: sticky; z-index: 1; - top: var(--stickyTop, 0px); + top: var(--MI-stickyTop, 0px); padding: 16px; font-weight: bold; - -webkit-backdrop-filter: var(--blur, blur(10px)); - backdrop-filter: var(--blur, blur(10px)); - background-color: var(--X16); + -webkit-backdrop-filter: var(--MI-blur, blur(10px)); + backdrop-filter: var(--MI-blur, blur(10px)); + background-color: var(--MI_THEME-X16); } > .content { @@ -507,7 +507,7 @@ const preview_fade = ref(`$[fade 🍮] $[fade.out 🍮] $[fade.speed=3s 🍮] $[ } > .preview { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); padding: 16px; } } diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index dc8c32a7d9..528e86646b 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-show="!isDeleted" ref="rootEl" v-hotkey="keymap" - :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]" + :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]" :tabindex="isDeleted ? '-1' : '0'" > <SkNoteSub v-if="appearNote.reply" v-show="!renoteCollapsed && !inReplyToCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> @@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" :class="$style.footerButton" class="_button" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @click.stop @mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()" > @@ -157,8 +157,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> - <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i> + <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -202,6 +202,9 @@ import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } fro import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; +import { shouldCollapsed } from '@@/js/collapsed.js'; +import { host } from '@@/js/config.js'; +import type { MenuItem } from '@/types/menu.js'; import SkNoteSub from '@/components/SkNoteSub.vue'; import SkNoteHeader from '@/components/SkNoteHeader.vue'; import SkNoteSimple from '@/components/SkNoteSimple.vue'; @@ -215,6 +218,7 @@ import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkButton from '@/components/MkButton.vue'; import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; +import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; import number from '@/filters/number.js'; import * as os from '@/os.js'; @@ -233,13 +237,10 @@ import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; -import type { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { useRouter } from '@/router/supplier.js'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; -import { shouldCollapsed } from '@@/js/collapsed.js'; -import { host } from '@@/js/config.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { type Keymap } from '@/scripts/hotkey.js'; import { focusPrev, focusNext } from '@/scripts/focus.js'; @@ -264,6 +265,7 @@ const emit = defineEmits<{ const router = useRouter(); const inTimeline = inject<boolean>('inTimeline', false); +const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true)); const inChannel = inject('inChannel', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); @@ -343,15 +345,18 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute'; */ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' { - if (mutedWords == null) return false; - - if (checkWordMute(noteToCheck, $i, mutedWords)) return true; - if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; - if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; + if (mutedWords != null) { + if (checkWordMute(noteToCheck, $i, mutedWords)) return true; + if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; + if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; + } if (checkOnly) return false; - if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute'; + if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) { + return 'sensitiveMute'; + } + return false; } @@ -514,7 +519,7 @@ function boostVisibility() { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); renoting = true; @@ -564,7 +569,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (props.mock) { return; @@ -625,7 +630,7 @@ function quote() { } function reply(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); if (props.mock) { return; } @@ -638,7 +643,7 @@ function reply(): void { } function like(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); if (props.mock) { @@ -660,7 +665,7 @@ function like(): void { } function react(viaKeyboard = false): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -808,15 +813,24 @@ function showRenoteMenu(): void { }; } + const renoteDetailsMenu: MenuItem = { + type: 'link', + text: i18n.ts.renoteDetails, + icon: 'ti ti-info-circle', + to: notePage(note.value), + }; + if (isMyRenote) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); os.popupMenu([ + renoteDetailsMenu, getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getUnrenote(), ], renoteTime.value); } else { os.popupMenu([ + renoteDetailsMenu, getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote), @@ -901,8 +915,8 @@ function emitUpdReaction(emoji: string, delta: number) { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: solid 2px var(--focus); - border-radius: var(--radius); + border: solid 2px var(--MI_THEME-focus); + border-radius: var(--MI-radius); box-sizing: border-box; } } @@ -929,9 +943,9 @@ function emitUpdReaction(emoji: string, delta: number) { right: 12px; padding: 0 4px; margin-bottom: 0 !important; - background: var(--popup); - border-radius: var(--radius-sm); - box-shadow: 0px 4px 32px var(--shadow); + background: var(--MI_THEME-popup); + border-radius: var(--MI-radius-sm); + box-shadow: 0px 4px 32px var(--MI_THEME-shadow); } .footerButton { @@ -950,6 +964,11 @@ function emitUpdReaction(emoji: string, delta: number) { } } +.skipRender { + content-visibility: auto; + contain-intrinsic-size: 0 150px; +} + .tip { display: flex; align-items: center; @@ -972,18 +991,18 @@ function emitUpdReaction(emoji: string, delta: number) { position: relative; display: flex; align-items: center; - padding: 24px 32px 0 calc(32px + var(--avatar) + 14px); + padding: 24px 32px 0 calc(32px + var(--MI-avatar) + 14px); line-height: 28px; white-space: pre; - color: var(--renote); + color: var(--MI_THEME-renote); &::before { content: ''; position: absolute; top: 0; - left: calc(32px + .5 * var(--avatar)); + left: calc(32px + .5 * var(--MI-avatar)); bottom: -8px; - border-left: var(--thread-width) solid var(--thread); + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); } &:first-child { @@ -1072,9 +1091,9 @@ function emitUpdReaction(emoji: string, delta: number) { .collapsedInReplyToLine { position: absolute; - left: calc(32px + .5 * var(--avatar)); + left: calc(32px + .5 * var(--MI-avatar)); // using solid instead of dotted, stylelistic choice - border-left: var(--thread-width) solid var(--thread); + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); top: calc(28px + 28px); // 28px of .root padding, plus 28px of avatar height (see SkNote) height: 28px; } @@ -1090,7 +1109,7 @@ function emitUpdReaction(emoji: string, delta: number) { left: 8px; width: 5px; height: calc(100% - 16px); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); pointer-events: none; } @@ -1099,10 +1118,10 @@ function emitUpdReaction(emoji: string, delta: number) { display: block !important; position: sticky !important; margin: 0 14px 0 0; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); position: sticky !important; - top: calc(22px + var(--stickyTop, 0px)); + top: calc(22px + var(--MI-stickyTop, 0px)); left: 0; transition: top 0.5s; @@ -1128,15 +1147,15 @@ function emitUpdReaction(emoji: string, delta: number) { width: 100%; margin-top: 14px; position: sticky; - bottom: calc(var(--stickyBottom, 0px) - 100px); + bottom: calc(var(--MI-stickyBottom, 0px) - 100px); } .showLessLabel { display: inline-block; - background: var(--popup); + background: var(--MI_THEME-popup); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } @@ -1154,19 +1173,19 @@ function emitUpdReaction(emoji: string, delta: number) { z-index: 2; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + //background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); &:hover > .collapsedLabel { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } } .collapsedLabel { display: inline-block; - background: var(--panel); + background: var(--MI_THEME-panel); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } @@ -1175,13 +1194,13 @@ function emitUpdReaction(emoji: string, delta: number) { } .replyIcon { - color: var(--accent); + color: var(--MI_THEME-accent); margin-right: 0.5em; } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -1205,8 +1224,8 @@ function emitUpdReaction(emoji: string, delta: number) { .quoteNote { padding: 16px; // Made border solid, stylistic choice - border: solid 1px var(--renote); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-renote); + border-radius: var(--MI-radius-sm); overflow: clip; } @@ -1229,7 +1248,7 @@ function emitUpdReaction(emoji: string, delta: number) { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -1242,14 +1261,14 @@ function emitUpdReaction(emoji: string, delta: number) { @container (max-width: 580px) { .root { font-size: 0.95em; - --avatar: 46px; + --MI-avatar: 46px; } .renote { - padding: 24px 26px 0 calc(26px + var(--avatar) + 14px); + padding: 24px 26px 0 calc(26px + var(--MI-avatar) + 14px); &::before { - left: calc(26px + .5 * var(--avatar)); + left: calc(26px + .5 * var(--MI-avatar)); } } @@ -1262,7 +1281,7 @@ function emitUpdReaction(emoji: string, delta: number) { } .collapsedInReplyToLine { - left: calc(26px + .5 * var(--avatar)); + left: calc(26px + .5 * var(--MI-avatar)); } .article { @@ -1284,26 +1303,26 @@ function emitUpdReaction(emoji: string, delta: number) { } .collapsedInReplyToLine { - left: calc(25px + .5 * var(--avatar)); + left: calc(25px + .5 * var(--MI-avatar)); } } @container (max-width: 500px) { .renote { - padding: 23px 25px 0 calc(25px + var(--avatar) + 14px); + padding: 23px 25px 0 calc(25px + var(--MI-avatar) + 14px); &::before { - left: calc(25px + .5 * var(--avatar)); + left: calc(25px + .5 * var(--MI-avatar)); } } } @container (max-width: 480px) { .renote { - padding: 22px 24px 0 calc(24px + var(--avatar) + 14px); + padding: 22px 24px 0 calc(24px + var(--MI-avatar) + 14px); &::before { - left: calc(24px + .5 * var(--avatar)); + left: calc(24px + .5 * var(--MI-avatar)); } } @@ -1321,7 +1340,7 @@ function emitUpdReaction(emoji: string, delta: number) { } .collapsedInReplyToLine { - left: calc(24px + .5 * var(--avatar)); + left: calc(24px + .5 * var(--MI-avatar)); top: calc(22px + 28px); // 22px of .root padding, plus 28px of avatar height } @@ -1332,12 +1351,12 @@ function emitUpdReaction(emoji: string, delta: number) { @container (max-width: 450px) { .root { - --avatar: 44px; + --MI-avatar: 44px; } .avatar { margin: 0 10px 0 0; - top: calc(14px + var(--stickyTop, 0px)); + top: calc(14px + var(--MI-stickyTop, 0px)); } } diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index 7907da6c94..6c14c6627c 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -58,7 +58,14 @@ SPDX-License-Identifier: AGPL-3.0-only <img v-for="role in appearNote.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/> </span> </div> - <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> + <div :class="$style.noteHeaderUsernameAndBadgeRoles"> + <div :class="$style.noteHeaderUsername"> + <MkAcct :user="appearNote.user"/> + </div> + <div v-if="appearNote.user.badgeRoles" :class="$style.noteHeaderBadgeRoles"> + <img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/> + </div> + </div> </div> </div> <div style="display: flex; align-items: flex-end; margin-left: auto;"> @@ -143,7 +150,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" class="_button" :class="$style.noteFooterButton" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @mousedown.prevent="renoted ? undoRenote() : boostVisibility()" > <i class="ti ti-repeat"></i> @@ -165,8 +172,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> - <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i> + <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -244,6 +251,7 @@ import { computed, inject, onMounted, onUnmounted, onUpdated, provide, ref, shal import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; +import { host } from '@@/js/config.js'; import SkNoteSub from '@/components/SkNoteSub.vue'; import SkNoteSimple from '@/components/SkNoteSimple.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; @@ -267,7 +275,6 @@ import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { host } from '@@/js/config.js'; import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; @@ -515,7 +522,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); renoting = true; @@ -561,7 +568,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -619,7 +626,7 @@ function quote() { } function reply(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); os.post({ reply: appearNote.value, @@ -630,7 +637,7 @@ function reply(): void { } function react(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -667,7 +674,7 @@ function react(): void { } function like(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -749,7 +756,7 @@ async function clip(): Promise<void> { function showRenoteMenu(): void { if (!isMyRenote) return; - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); os.popupMenu([{ text: i18n.ts.unrenote, icon: 'ti ti-trash', @@ -877,8 +884,8 @@ onUnmounted(() => { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: dashed 2px var(--focus); - border-radius: var(--radius); + border: dashed 2px var(--MI_THEME-focus); + border-radius: var(--MI-radius); box-sizing: border-box; } } @@ -904,7 +911,7 @@ onUnmounted(() => { padding: 16px 32px 8px 32px; line-height: 28px; white-space: pre; - color: var(--renote); + color: var(--MI_THEME-renote); } .renoteAvatar { @@ -913,7 +920,7 @@ onUnmounted(() => { width: 28px; height: 28px; margin: 0 8px 0 0; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .renoteText { @@ -967,8 +974,8 @@ onUnmounted(() => { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: solid 1px var(--focus); - border-radius: var(--radius); + border: solid 1px var(--MI_THEME-focus); + border-radius: var(--MI-radius); box-sizing: border-box; } } @@ -985,8 +992,8 @@ onUnmounted(() => { .noteHeaderAvatar { display: block; flex-shrink: 0; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); } .noteHeaderBody { @@ -1009,8 +1016,8 @@ onUnmounted(() => { padding: 4px 6px; font-size: 80%; line-height: 1; - border: solid 0.5px var(--divider); - border-radius: var(--radius-xs); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs); } .noteHeaderInfo { @@ -1018,8 +1025,13 @@ onUnmounted(() => { text-align: right; } +.noteHeaderUsernameAndBadgeRoles { + display: flex; +} + .noteHeaderUsername { margin-bottom: 2px; + margin-right: 0.5em; line-height: 1.3; word-wrap: anywhere; text-overflow: ellipsis; @@ -1034,6 +1046,19 @@ onUnmounted(() => { margin-top: 5px; } +.noteHeaderBadgeRoles { + margin: 0 .5em 0 0; +} + +.noteHeaderBadgeRole { + height: 1.3em; + vertical-align: -20%; + + & + .noteHeaderBadgeRole { + margin-left: 0.2em; + } +} + .noteContent { container-type: inline-size; overflow-wrap: break-word; @@ -1049,19 +1074,19 @@ onUnmounted(() => { } .noteReplyTarget { - color: var(--accent); + color: var(--MI_THEME-accent); margin-right: 0.5em; } .rn { margin-left: 4px; font-style: oblique; - color: var(--renote); + color: var(--MI_THEME-renote); } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -1076,8 +1101,8 @@ onUnmounted(() => { .quoteNote { padding: 16px; - border: solid 1px var(--renote); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-renote); + border-radius: var(--MI-radius-sm); overflow: clip; } @@ -1102,7 +1127,7 @@ onUnmounted(() => { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -1112,17 +1137,17 @@ onUnmounted(() => { opacity: 0.7; &.reacted { - color: var(--accent); + color: var(--MI_THEME-accent); } } .reply:not(:first-child) { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .tabs { - border-top: solid 0.5px var(--divider); - border-bottom: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); display: flex; } @@ -1141,7 +1166,7 @@ onUnmounted(() => { } .tabActive { - border-bottom: solid 2px var(--accent); + border-bottom: solid 2px var(--MI_THEME-accent); } .tab_renotes { @@ -1161,12 +1186,12 @@ onUnmounted(() => { .reactionTab { padding: 4px 6px; - border: solid 1px var(--divider); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-sm); } .reactionTabActive { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); } @container (max-width: 500px) { @@ -1221,7 +1246,7 @@ onUnmounted(() => { margin: 0 10px 0 0 !important; width: 40px !important; height: 40px !important; - border-radius: var(--radius-sm) !important; + border-radius: var(--MI-radius-sm) !important; } .muted { diff --git a/packages/frontend/src/components/SkNoteHeader.vue b/packages/frontend/src/components/SkNoteHeader.vue index 45218fafb6..6bcc30f6cb 100644 --- a/packages/frontend/src/components/SkNoteHeader.vue +++ b/packages/frontend/src/components/SkNoteHeader.vue @@ -161,7 +161,7 @@ const mock = inject<boolean>('mock', false); } &:hover { - color: var(--nameHover); + color: var(--MI_THEME-nameHover); text-decoration: none; } } @@ -188,8 +188,8 @@ const mock = inject<boolean>('mock', false); margin: 0 .5em 0 0; padding: 1px 6px; font-size: 80%; - border: solid 0.5px var(--divider); - border-radius: var(--radius-xs); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs); } .username { @@ -255,7 +255,7 @@ const mock = inject<boolean>('mock', false); } .danger { - color: var(--accent); + color: var(--MI_THEME-accent); } @container (max-width: 500px) { diff --git a/packages/frontend/src/components/SkNoteSimple.vue b/packages/frontend/src/components/SkNoteSimple.vue index b31e337a99..b9895305f2 100644 --- a/packages/frontend/src/components/SkNoteSimple.vue +++ b/packages/frontend/src/components/SkNoteSimple.vue @@ -50,7 +50,7 @@ watch(() => props.expandAllCws, (expandAllCws) => { font-size: 0.95em; &:hover, &:focus-within { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); transition: background .2s; } } @@ -61,9 +61,9 @@ watch(() => props.expandAllCws, (expandAllCws) => { margin: 0 10px 0 0; width: 34px; height: 34px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); position: sticky !important; - top: calc(16px + var(--stickyTop, 0px)); + top: calc(16px + var(--MI-stickyTop, 0px)); left: 0; } diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue index fac35191b9..bd25e1e3ad 100644 --- a/packages/frontend/src/components/SkNoteSub.vue +++ b/packages/frontend/src/components/SkNoteSub.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" class="_button" :class="$style.noteFooterButton" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @mousedown="renoted ? undoRenote() : boostVisibility()" > <i class="ph-rocket-launch ph-bold ph-lg"></i> @@ -106,7 +106,8 @@ import { $i } from '@/account.js'; import { userPage } from '@/filters/user.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { defaultStore } from '@/store.js'; -import { pleaseLogin } from '@/scripts/please-login.js'; +import { host } from '@@/js/config.js'; +import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { reactionPicker } from '@/scripts/reaction-picker.js'; @@ -159,6 +160,11 @@ const isRenote = ( props.note.poll == null ); +const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ + type: 'lookup', + url: `https://${host}/notes/${appearNote.value.id}`, +})); + async function addReplyTo(replyNote: Misskey.entities.Note) { replies.value.unshift(replyNote); appearNote.value.repliesCount += 1; @@ -196,7 +202,7 @@ function focus() { } function reply(viaKeyboard = false): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); os.post({ reply: props.note, @@ -208,7 +214,7 @@ function reply(viaKeyboard = false): void { } function react(viaKeyboard = false): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); if (props.note.reactionAcceptance === 'likeOnly') { @@ -242,7 +248,7 @@ function react(viaKeyboard = false): void { } function like(): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -302,7 +308,7 @@ function boostVisibility() { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -346,7 +352,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -426,7 +432,7 @@ if (props.detail) { padding: 28px 32px; position: relative; - --reply-indent: calc(.5 * var(--avatar)); + --reply-indent: calc(.5 * var(--MI-avatar)); &.children { padding: 10px 0 0 8px; @@ -434,16 +440,16 @@ if (props.detail) { &.isReply { /* @link https://utopia.fyi/clamp/calculator?a=450,580,26—36 */ - --avatar: clamp(26px, -8.6154px + 7.6923cqi, 36px); + --MI-avatar: clamp(26px, -8.6154px + 7.6923cqi, 36px); } } .line { position: absolute; - left: calc(32px + .5 * var(--avatar)); + left: calc(32px + .5 * var(--MI-avatar)); // using solid instead of dotted, stylelistic choice - border-left: var(--thread-width) solid var(--thread); - top: calc(28px + var(--avatar)); // 28px of .root padding, plus 58px of avatar height (see SkNote) + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); + top: calc(28px + var(--MI-avatar)); // 28px of .root padding, plus 58px of avatar height (see SkNote) bottom: -28px; } @@ -468,8 +474,8 @@ if (props.detail) { right: -12px; left: -12px; bottom: -12px; - background: var(--panelHighlight); - border-radius: var(--radius); + background: var(--MI_THEME-panelHighlight); + border-radius: var(--MI-radius); opacity: 0; transition: opacity .2s, background .2s; z-index: -1; @@ -487,7 +493,7 @@ if (props.detail) { left: 8px; width: 5px; height: calc(100% - 8px); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); pointer-events: none; } @@ -495,9 +501,9 @@ if (props.detail) { flex-shrink: 0; display: block; margin: 0 14px 0 0; - width: var(--avatar); - height: var(--avatar); - border-radius: var(--radius-sm); + width: var(--MI-avatar); + height: var(--MI-avatar); + border-radius: var(--MI-radius-sm); } .body { @@ -525,12 +531,12 @@ if (props.detail) { opacity: 0.7; &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } // Responsible for Reply borders 448 and 508 .reply, .more { - //border-left: solid 0.5px var(--divider); + //border-left: solid 0.5px var(--MI_THEME-divider); margin-top: 10px; } @@ -541,11 +547,11 @@ if (props.detail) { @container (max-width: 580px) { .root { padding: 28px 26px 0; - --avatar: 46px; + --MI-avatar: 46px; } .line { - left: calc(26px + .5 * var(--avatar)); + left: calc(26px + .5 * var(--MI-avatar)); } } @@ -555,8 +561,8 @@ if (props.detail) { } .line { - top: calc(23px + var(--avatar)); - left: calc(25px + .5 * var(--avatar)); + top: calc(23px + var(--MI-avatar)); + left: calc(25px + .5 * var(--MI-avatar)); } } @@ -574,7 +580,7 @@ if (props.detail) { opacity: 0.7; &.reacted { - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -591,7 +597,7 @@ if (props.detail) { } .reply, .more { - //border-left: solid 0.5px var(--divider); + //border-left: solid 0.5px var(--MI_THEME-divider); margin-top: 10px; } @@ -605,23 +611,23 @@ if (props.detail) { } .line { - top: calc(22px + var(--avatar)); - left: calc(24px + .5 * var(--avatar)); + top: calc(22px + var(--MI-avatar)); + left: calc(24px + .5 * var(--MI-avatar)); } } @container (max-width: 450px) { .root { - --avatar: 44px; + --MI-avatar: 44px; } } .muted { text-align: center; padding: 8px !important; - border: 1px solid var(--divider); + border: 1px solid var(--MI_THEME-divider); margin: 8px 8px 0 8px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } // avatar container with line @@ -633,7 +639,7 @@ if (props.detail) { .threadLine { width: 0; flex-grow: 1; - border-left: var(--thread-width) solid var(--thread); + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); margin-left: var(--reply-indent); } @@ -642,10 +648,10 @@ if (props.detail) { } .reply:not(:last-child) { - border-left: var(--thread-width) solid var(--thread); + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); &::before { - left: calc(-1 * var(--thread-width)); + left: calc(-1 * var(--MI-thread-width)); } } @@ -654,10 +660,10 @@ if (props.detail) { content: ''; left: 0px; top: -10px; - height: calc(10px + 10px + .5 * var(--avatar)); + height: calc(10px + 10px + .5 * var(--MI-avatar)); width: 15px; - border-left: var(--thread-width) solid var(--thread); - border-bottom: var(--thread-width) solid var(--thread); + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); + border-bottom: var(--MI-thread-width) solid var(--MI_THEME-thread); border-bottom-left-radius: 15px; } diff --git a/packages/frontend/src/components/SkOldNoteWindow.vue b/packages/frontend/src/components/SkOldNoteWindow.vue index 3810b62366..48b9020402 100644 --- a/packages/frontend/src/components/SkOldNoteWindow.vue +++ b/packages/frontend/src/components/SkOldNoteWindow.vue @@ -182,8 +182,8 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS .noteHeaderAvatar { display: block; flex-shrink: 0; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); } .noteHeaderBody { @@ -206,8 +206,8 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS padding: 4px 6px; font-size: 80%; line-height: 1; - border: solid 0.5px var(--divider); - border-radius: var(--radius-xs); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs); } .noteHeaderInfo { @@ -240,19 +240,19 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS } .noteReplyTarget { - color: var(--accent); + color: var(--MI_THEME-accent); margin-right: 0.5em; } .rn { margin-left: 4px; font-style: oblique; - color: var(--renote); + color: var(--MI_THEME-renote); } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -267,8 +267,8 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS .quoteNote { padding: 16px; - border: dashed 1px var(--renote); - border-radius: var(--radius-xs); + border: dashed 1px var(--MI_THEME-renote); + border-radius: var(--MI-radius-xs); overflow: clip; } @@ -287,7 +287,7 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } diff --git a/packages/frontend/src/components/SkOneko.vue b/packages/frontend/src/components/SkOneko.vue index 24bb392335..ef7bdd74f0 100644 --- a/packages/frontend/src/components/SkOneko.vue +++ b/packages/frontend/src/components/SkOneko.vue @@ -240,6 +240,6 @@ onMounted(init); pointer-events: none; image-rendering: pixelated; z-index: 2147483647; - background-image: var(--oneko-image, url(/client-assets/oneko.gif)); + background-image: var(--MI_THEME-oneko-image, url(/client-assets/oneko.gif)); } </style> diff --git a/packages/frontend/src/components/SkUserRecentNotes.vue b/packages/frontend/src/components/SkUserRecentNotes.vue index 7cfa0fb3f6..908affcdaf 100644 --- a/packages/frontend/src/components/SkUserRecentNotes.vue +++ b/packages/frontend/src/components/SkUserRecentNotes.vue @@ -94,7 +94,7 @@ onMounted(async () => { <style lang="scss" module> .panel { - background: var(--panel); + background: var(--MI_THEME-panel); } .userInfo { diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue index f5546edf1e..b8837d7133 100644 --- a/packages/frontend/src/components/form/link.vue +++ b/packages/frontend/src/components/form/link.vue @@ -60,18 +60,18 @@ const props = defineProps<{ width: 100%; box-sizing: border-box; padding: 10px 14px; - background: var(--folderHeaderBg); - border-radius: var(--radius-sm); + background: var(--MI_THEME-folderHeaderBg); + border-radius: var(--MI-radius-sm); font-size: 0.9em; &:hover { text-decoration: none; - background: var(--folderHeaderHoverBg); + background: var(--MI_THEME-folderHeaderHoverBg); } &.active { - color: var(--accent); - background: var(--folderHeaderHoverBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-folderHeaderHoverBg); } } @@ -79,7 +79,7 @@ const props = defineProps<{ margin-right: 0.75em; flex-shrink: 0; text-align: center; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue index ad37daa265..5fca3acc31 100644 --- a/packages/frontend/src/components/form/section.vue +++ b/packages/frontend/src/components/form/section.vue @@ -21,8 +21,8 @@ defineProps<{ <style lang="scss" module> .root { - border-top: solid 0.5px var(--divider); - //border-bottom: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); + //border-bottom: solid 0.5px var(--MI_THEME-divider); } .rootFirst { @@ -49,7 +49,7 @@ defineProps<{ .description { font-size: 0.85em; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); margin: 0 0 8px 0; } </style> diff --git a/packages/frontend/src/components/form/slot.vue b/packages/frontend/src/components/form/slot.vue index f54db0ca82..da94b7abbb 100644 --- a/packages/frontend/src/components/form/slot.vue +++ b/packages/frontend/src/components/form/slot.vue @@ -35,7 +35,7 @@ function focus() { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue index 5226c61d68..821f07510b 100644 --- a/packages/frontend/src/components/form/suspense.vue +++ b/packages/frontend/src/components/form/suspense.vue @@ -18,19 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> -<script lang="ts" setup> +<script lang="ts" setup generic="T extends unknown"> import { ref, watch } from 'vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; const props = defineProps<{ - p: () => Promise<any>; + p: () => Promise<T>; }>(); const pending = ref(true); const resolved = ref(false); const rejected = ref(false); -const result = ref<any>(null); +const result = ref<T | null>(null); const process = () => { if (props.p == null) { diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index 1238e6ef23..fc6c64d2aa 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div v-if="chosen && !shouldHide" :class="$style.root"> +<div v-if="chosen && !shouldHide"> <div v-if="!showMenu" :class="[$style.main, { @@ -30,12 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only </component> </div> <div v-else :class="$style.menu"> - <div :class="$style.menuContainer"> - <div>Ads by {{ host }}</div> - <!--<MkButton class="button" primary>{{ i18n.ts._ad.like }}</MkButton>--> - <MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ i18n.ts._ad.reduceFrequencyOfThisAd }}</MkButton> - <button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button> - </div> + <div>Ads by {{ host }}</div> + <!--<MkButton class="button" primary>{{ i18n.ts._ad.like }}</MkButton>--> + <MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ i18n.ts._ad.reduceFrequencyOfThisAd }}</MkButton> + <button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button> </div> </div> <div v-else></div> @@ -43,9 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed } from 'vue'; +import { url as local, host } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { url as local, host } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; @@ -122,11 +120,6 @@ function reduceFrequency(): void { </script> <style lang="scss" module> -.root { - background-size: auto auto; - background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px ); -} - .main { text-align: center; @@ -139,8 +132,6 @@ function reduceFrequency(): void { } &.form_horizontal { - padding: 8px; - > .link, > .link > .img { max-width: min(600px, 100%); @@ -149,8 +140,6 @@ function reduceFrequency(): void { } &.form_horizontalBig { - padding: 8px; - > .link, > .link > .img { max-width: min(600px, 100%); @@ -182,7 +171,7 @@ function reduceFrequency(): void { display: block; object-fit: contain; margin: auto; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); } .i { @@ -191,8 +180,8 @@ function reduceFrequency(): void { right: 1px; display: grid; place-content: center; - background: var(--panel); - border-radius: var(--radius-full); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-full); padding: 2px; } @@ -202,15 +191,12 @@ function reduceFrequency(): void { } .menu { - padding: 8px; text-align: center; -} - -.menuContainer { padding: 8px; margin: 0 auto; max-width: 400px; - border: solid 1px var(--divider); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-divider); } .menuButton { diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index fbc716016c..90fa522f3d 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -25,17 +25,18 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, inject, ref } from 'vue'; +import { computed, defineAsyncComponent, inject, ref } from 'vue'; +import type { MenuItem } from '@/types/menu.js'; import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js'; import { defaultStore } from '@/store.js'; import { customEmojisMap } from '@/custom-emojis.js'; import * as os from '@/os.js'; -import { misskeyApiGet } from '@/scripts/misskey-api.js'; +import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as sound from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; -import type { MenuItem } from '@/types/menu.js'; +import { $i } from '@/account.js'; const props = defineProps<{ name: string; @@ -127,9 +128,31 @@ function onClick(ev: MouseEvent) { }, }); + if ($i?.isModerator ?? $i?.isAdmin) { + menuItems.push({ + text: i18n.ts.edit, + icon: 'ti ti-pencil', + action: async () => { + await edit(props.name); + }, + }); + } + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } } + +async function edit(name: string) { + const emoji = await misskeyApi('emoji', { + name: name, + }); + const { dispose } = os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), { + emoji: emoji, + }, { + closed: () => dispose(), + }); +} + </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue index 64c828ba4a..77dddaff89 100644 --- a/packages/frontend/src/components/global/MkError.vue +++ b/packages/frontend/src/components/global/MkError.vue @@ -44,6 +44,6 @@ const emit = defineEmits<{ width: 128px; height: 128px; margin-bottom: 16px; - border-radius: var(--radius-md); + border-radius: var(--MI-radius-md); } </style> diff --git a/packages/frontend/src/components/global/MkLoading.vue b/packages/frontend/src/components/global/MkLoading.vue index 49d8ace37b..47d797606b 100644 --- a/packages/frontend/src/components/global/MkLoading.vue +++ b/packages/frontend/src/components/global/MkLoading.vue @@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{ --size: 38px; &.colored { - color: var(--accent); + color: var(--MI_THEME-accent); } &.inline { diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts index 9bf9f4a872..1039572a06 100644 --- a/packages/frontend/src/components/global/MkMfm.ts +++ b/packages/frontend/src/components/global/MkMfm.ts @@ -7,6 +7,7 @@ import { VNode, h, defineAsyncComponent, SetupContext, provide } from 'vue'; import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import CkFollowMouse from '../CkFollowMouse.vue'; +import { host } from '@@/js/config.js'; import MkUrl from '@/components/global/MkUrl.vue'; import MkTime from '@/components/global/MkTime.vue'; import MkLink from '@/components/MkLink.vue'; @@ -18,7 +19,6 @@ import MkCodeInline from '@/components/MkCodeInline.vue'; import MkGoogle from '@/components/MkGoogle.vue'; import MkSparkle from '@/components/MkSparkle.vue'; import MkA, { MkABehavior } from '@/components/global/MkA.vue'; -import { host } from '@@/js/config.js'; import { defaultStore } from '@/store.js'; function safeParseFloat(str: unknown): number | null { @@ -32,8 +32,8 @@ const QUOTE_STYLE = ` display: block; margin: 8px; padding: 6px 0 6px 12px; -color: var(--fg); -border-left: solid 3px var(--fg); +color: var(--MI_THEME-fg); +border-left: solid 3px var(--MI_THEME-fg); opacity: 0.7; `.split('\n').join(' '); @@ -60,7 +60,8 @@ type MfmEvents = { // eslint-disable-next-line import/no-default-export export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) { - provide('linkNavigationBehavior', props.linkNavigationBehavior); + // こうしたいところだけど functional component 内では provide は使えない + //provide('linkNavigationBehavior', props.linkNavigationBehavior); const isNote = props.isNote ?? true; const shouldNyaize = props.nyaize === 'respect' && props.author?.isCat && props.author?.speakAsCat && !defaultStore.state.disableCatSpeak; @@ -328,7 +329,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'border': { let color = validColor(token.props.args.color); - color = color ? `#${color}` : 'var(--accent)'; + color = color ? `#${color}` : 'var(--MI_THEME-accent)'; let b_style = token.props.args.style; if ( typeof b_style !== 'string' || @@ -361,7 +362,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven const child = token.children[0]; const unixtime = parseInt(child.type === 'text' ? child.props.text : ''); return h('span', { - style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: var(--radius-ellipse); padding: 4px 10px 4px 6px;', + style: 'display: inline-block; font-size: 90%; border: solid 1px var(--MI_THEME-divider); border-radius: var(--MI-radius-ellipse); padding: 4px 10px 4px 6px;', }, [ h('i', { class: 'ti ti-clock', @@ -409,6 +410,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven key: Math.random(), url: token.props.url, rel: 'nofollow noopener', + navigationBehavior: props.linkNavigationBehavior, }))]; } @@ -417,6 +419,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven key: Math.random(), url: token.props.url, rel: 'nofollow noopener', + navigationBehavior: props.linkNavigationBehavior, }, genEl(token.children, scale, true)))]; } @@ -425,6 +428,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven key: Math.random(), host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host, username: token.props.username, + navigationBehavior: props.linkNavigationBehavior, }))]; } @@ -432,7 +436,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven return [h('bdi', h(MkA, { key: Math.random(), to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`, - style: 'color:var(--hashtag);', + style: 'color:var(--MI_THEME-hashtag);', + behavior: props.linkNavigationBehavior, }, `#${token.props.hashtag}`))]; } @@ -527,8 +532,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } default: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - console.error('unrecognized ast type:', (token as any).type); + // @ts-expect-error 存在しないASTタイプ + console.error('unrecognized ast type:', token.type); return []; } diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index 7d13fb9279..ffa6f13ff6 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -53,7 +53,7 @@ export type Tab = { </script> <script lang="ts" setup> -import { onMounted, onUnmounted, watch, nextTick, shallowRef } from 'vue'; +import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue'; import { defaultStore } from '@/store.js'; const props = withDefaults(defineProps<{ @@ -120,14 +120,14 @@ function onTabWheel(ev: WheelEvent) { let entering = false; -async function enter(element: Element) { +async function enter(el: Element) { + if (!(el instanceof HTMLElement)) return; entering = true; - const el = element as HTMLElement; const elementWidth = el.getBoundingClientRect().width; el.style.width = '0'; el.style.paddingLeft = '0'; - el.offsetWidth; // force reflow - el.style.width = elementWidth + 'px'; + el.offsetWidth; // reflow + el.style.width = `${elementWidth}px`; el.style.paddingLeft = ''; nextTick(() => { entering = false; @@ -136,22 +136,23 @@ async function enter(element: Element) { setTimeout(renderTab, 170); } -function afterEnter(element: Element) { - //el.style.width = ''; +function afterEnter(el: Element) { + if (!(el instanceof HTMLElement)) return; + // element.style.width = ''; } -async function leave(element: Element) { - const el = element as HTMLElement; +async function leave(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementWidth = el.getBoundingClientRect().width; - el.style.width = elementWidth + 'px'; + el.style.width = `${elementWidth}px`; el.style.paddingLeft = ''; - el.offsetWidth; // force reflow + el.offsetWidth; // reflow el.style.width = '0'; el.style.paddingLeft = '0'; } -function afterLeave(element: Element) { - const el = element as HTMLElement; +function afterLeave(el: Element) { + if (!(el instanceof HTMLElement)) return; el.style.width = ''; } @@ -219,7 +220,7 @@ onUnmounted(() => { &.active { opacity: 1; - color: var(--accent); + color: var(--MI_THEME-accent); } &.animate { @@ -248,8 +249,8 @@ onUnmounted(() => { position: absolute; bottom: 0; height: 3px; - background: var(--accent); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-accent); + border-radius: var(--MI-radius-ellipse); transition: none; pointer-events: none; diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index bb20f54fa4..18c97b1bdb 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -115,7 +115,7 @@ function goBack(): void { } const calcBg = () => { - const rawBg = 'var(--bg)'; + const rawBg = 'var(--MI_THEME-bg)'; const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); tinyBg.setAlpha(0.85); bg.value = tinyBg.toRgbString(); @@ -146,9 +146,9 @@ onUnmounted(() => { <style lang="scss" module> .root { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - border-bottom: solid 0.5px var(--divider); + -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%; } @@ -161,7 +161,7 @@ onUnmounted(() => { .upper { --height: 50px; display: flex; - gap: var(--margin); + gap: var(--MI-margin); height: var(--height); .tabs:first-child { @@ -239,14 +239,14 @@ onUnmounted(() => { width: calc(var(--height) - (var(--margin))); box-sizing: border-box; position: relative; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); &:hover { background: rgba(0, 0, 0, 0.05); } &.highlighted { - color: var(--accent); + color: var(--MI_THEME-accent); } } diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue index 72993991ce..1aebf487bb 100644 --- a/packages/frontend/src/components/global/MkStickyContainer.vue +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -5,32 +5,30 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div ref="rootEl"> - <div ref="headerEl"> + <div ref="headerEl" :class="$style.header"> <slot name="header"></slot> </div> <div - ref="bodyEl" + :class="$style.body" :data-sticky-container-header-height="headerHeight" :data-sticky-container-footer-height="footerHeight" - style="position: relative; z-index: 0;" > <slot></slot> </div> - <div ref="footerEl"> + <div ref="footerEl" :class="$style.footer"> <slot name="footer"></slot> </div> </div> </template> <script lang="ts" setup> -import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue'; +import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, useTemplateRef } from 'vue'; import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js'; -const rootEl = shallowRef<HTMLElement>(); -const headerEl = shallowRef<HTMLElement>(); -const footerEl = shallowRef<HTMLElement>(); -const bodyEl = shallowRef<HTMLElement>(); +const rootEl = useTemplateRef('rootEl'); +const headerEl = useTemplateRef('headerEl'); +const footerEl = useTemplateRef('footerEl'); const headerHeight = ref<string | undefined>(); const childStickyTop = ref(0); @@ -67,31 +65,11 @@ onMounted(() => { watch([parentStickyTop, parentStickyBottom], calc); - watch(childStickyTop, () => { - if (bodyEl.value == null) return; - bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`); - }, { - immediate: true, - }); - - watch(childStickyBottom, () => { - if (bodyEl.value == null) return; - bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`); - }, { - immediate: true, - }); - if (headerEl.value != null) { - headerEl.value.style.position = 'sticky'; - headerEl.value.style.top = 'var(--stickyTop, 0)'; - headerEl.value.style.zIndex = '1'; observer.observe(headerEl.value); } if (footerEl.value != null) { - footerEl.value.style.position = 'sticky'; - footerEl.value.style.bottom = 'var(--stickyBottom, 0)'; - footerEl.value.style.zIndex = '1'; observer.observe(footerEl.value); } }); @@ -101,6 +79,27 @@ onUnmounted(() => { }); defineExpose({ - rootEl: rootEl, + rootEl, }); </script> + +<style lang='scss' module> +.body { + position: relative; + z-index: 0; + --MI-stickyTop: v-bind("childStickyTop + 'px'"); + --MI-stickyBottom: v-bind("childStickyBottom + 'px'"); +} + +.header { + position: sticky; + top: var(--MI-stickyTop, 0); + z-index: 1; +} + +.footer { + position: sticky; + bottom: var(--MI-stickyBottom, 0); + z-index: 1; +} +</style> diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 50bec990a1..f600f7eed2 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -99,10 +99,10 @@ if (!invalid && props.origin === null && (props.mode === 'relative' || props.mod <style lang="scss" module> .old1 { - color: var(--warn); + color: var(--MI_THEME-warn); } .old1.old2 { - color: var(--error); + color: var(--MI_THEME-error); } </style> diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 19bd794a5d..38bdfc52d4 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -27,6 +27,7 @@ import MkLoadingPage from '@/pages/_loading_.vue'; const props = defineProps<{ router?: IRouter; + nested?: boolean; }>(); const router = props.router ?? inject('router'); @@ -39,6 +40,8 @@ const currentDepth = inject('routerCurrentDepth', 0); provide('routerCurrentDepth', currentDepth + 1); function resolveNested(current: Resolved, d = 0): Resolved | null { + if (!props.nested) return current; + if (d === currentDepth) { return current; } else { diff --git a/packages/frontend/src/components/page/page.dynamic.vue b/packages/frontend/src/components/page/page.dynamic.vue index 8c511a690d..c2449931c1 100644 --- a/packages/frontend/src/components/page/page.dynamic.vue +++ b/packages/frontend/src/components/page/page.dynamic.vue @@ -27,9 +27,9 @@ const props = defineProps<{ <style lang="scss" module> .root { - border: 1px solid var(--divider); - border-radius: var(--radius); - padding: var(--margin); + border: 1px solid var(--MI_THEME-divider); + border-radius: var(--MI-radius); + padding: var(--MI-margin); text-align: center; } diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue index fc1ce9fc7b..69443ce7dd 100644 --- a/packages/frontend/src/components/page/page.image.vue +++ b/packages/frontend/src/components/page/page.image.vue @@ -28,8 +28,8 @@ onMounted(() => { <style lang="scss" module> .root { - border: 1px solid var(--divider); - border-radius: var(--radius); + border: 1px solid var(--MI_THEME-divider); + border-radius: var(--MI-radius); overflow: hidden; } .mediaList { diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue index b5ba407806..84436e7adb 100644 --- a/packages/frontend/src/components/page/page.note.vue +++ b/packages/frontend/src/components/page/page.note.vue @@ -35,7 +35,7 @@ onMounted(() => { <style lang="scss" module> .root { - border: 1px solid var(--divider); - border-radius: var(--radius); + border: 1px solid var(--MI_THEME-divider); + border-radius: var(--MI-radius); } </style> |