diff --git a/CHANGELOG.md b/CHANGELOG.md index b6fcbba1b5..cd8027b050 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ - Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに - Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように - Enhance: テーマ設定画面のデザインを改善 +- Enhance: 投稿フォームの設定メニューを改良 + - 投稿フォームをリセットできるように + - 文字数カウントを復活 - Fix: テーマ切り替え時に一部の色が変わらない問題を修正 ### Server diff --git a/locales/index.d.ts b/locales/index.d.ts index c9f67c8110..3b56563149 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5346,6 +5346,10 @@ export interface Locale extends ILocale { * 投稿フォーム */ "postForm": string; + /** + * 文字数 + */ + "textCount": string; "_emojiPalette": { /** * パレット diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 605aab919d..ad96549160 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1332,6 +1332,7 @@ preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル" paste: "ペースト" emojiPalette: "絵文字パレット" postForm: "投稿フォーム" +textCount: "文字数" _emojiPalette: palettes: "パレット" diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index aa53c19c33..954cfa58be 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -15,9 +15,6 @@ SPDX-License-Identifier: AGPL-3.0-only @focusin.passive.stop="() => {}" > <div - ref="itemsEl" - v-hotkey="keymap" - tabindex="0" class="_popup _shadow" :class="$style.menu" :style="{ @@ -27,147 +24,156 @@ SPDX-License-Identifier: AGPL-3.0-only @keydown.stop="() => {}" @contextmenu.self.prevent="() => {}" > - <template v-for="item in (items2 ?? [])"> - <div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div> - <span v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]"> - <span style="opacity: 0.7;">{{ item.text }}</span> + <slot name="header"></slot> + <div + ref="itemsEl" + v-hotkey="keymap" + tabindex="0" + :class="$style.menuItems" + > + <template v-for="item in (items2 ?? [])"> + <div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div> + <span v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]"> + <span style="opacity: 0.7;">{{ item.text }}</span> + </span> + <span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]"> + <span><MkEllipsis/></span> + </span> + <MkA + v-else-if="item.type === 'link'" + role="menuitem" + tabindex="0" + :class="['_button', $style.item]" + :to="item.to" + @click.passive="close(true)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > + <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> + <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> + <div :class="$style.item_content"> + <span :class="$style.item_content_text">{{ item.text }}</span> + <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> + </div> + </MkA> + <a + v-else-if="item.type === 'a'" + role="menuitem" + tabindex="0" + :class="['_button', $style.item]" + :href="item.href" + :target="item.target" + :rel="item.target === '_blank' ? 'noopener noreferrer' : undefined" + :download="item.download" + @click.passive="close(true)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > + <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" class="_blink"><i class="_indicatorCircle"></i></span> + </div> + </a> + <button + v-else-if="item.type === 'user'" + role="menuitem" + tabindex="0" + :class="['_button', $style.item, { [$style.active]: item.active }]" + @click.prevent="item.active ? close(false) : clicked(item.action, $event)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > + <MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/> + <div v-if="item.indicate" :class="$style.item_content"> + <span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> + </div> + </button> + <button + v-else-if="item.type === 'switch'" + role="menuitemcheckbox" + tabindex="0" + :class="['_button', $style.item]" + :disabled="unref(item.disabled)" + @click.prevent="switchItem(item)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > + <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> + <MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> + <div :class="$style.item_content"> + <span :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]">{{ item.text }}</span> + <MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> + </div> + </button> + <button + v-else-if="item.type === 'radio'" + role="menuitem" + tabindex="0" + :class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]" + :disabled="unref(item.disabled)" + @mouseenter.prevent="preferClick ? null : showRadioOptions(item, $event)" + @keydown.enter.prevent="preferClick ? null : showRadioOptions(item, $event)" + @click.prevent="!preferClick ? null : showRadioOptions(item, $event)" + > + <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> + <div :class="$style.item_content"> + <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> + <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> + </div> + </button> + <button + v-else-if="item.type === 'radioOption'" + role="menuitemradio" + tabindex="0" + :class="['_button', $style.item, $style.radio, { [$style.active]: unref(item.active) }]" + @click.prevent="unref(item.active) ? null : clicked(item.action, $event, false)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > + <div :class="$style.icon"> + <span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span> + </div> + <div :class="$style.item_content"> + <span :class="$style.item_content_text">{{ item.text }}</span> + </div> + </button> + <button + v-else-if="item.type === 'parent'" + role="menuitem" + tabindex="0" + :class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]" + @mouseenter.prevent="preferClick ? null : showChildren(item, $event)" + @keydown.enter.prevent="preferClick ? null : showChildren(item, $event)" + @click.prevent="!preferClick ? null : showChildren(item, $event)" + > + <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> + <div :class="$style.item_content"> + <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> + <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> + </div> + </button> + <button + v-else role="menuitem" + tabindex="0" + :class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]" + @click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > + <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> + <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> + <div :class="$style.item_content"> + <span :class="$style.item_content_text">{{ item.text }}</span> + <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> + </div> + </button> + </template> + <span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]"> + <span>{{ i18n.ts.none }}</span> </span> - <span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]"> - <span><MkEllipsis/></span> - </span> - <MkA - v-else-if="item.type === 'link'" - role="menuitem" - tabindex="0" - :class="['_button', $style.item]" - :to="item.to" - @click.passive="close(true)" - @mouseenter.passive="onItemMouseEnter" - @mouseleave.passive="onItemMouseLeave" - > - <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> - <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> - <div :class="$style.item_content"> - <span :class="$style.item_content_text">{{ item.text }}</span> - <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> - </div> - </MkA> - <a - v-else-if="item.type === 'a'" - role="menuitem" - tabindex="0" - :class="['_button', $style.item]" - :href="item.href" - :target="item.target" - :rel="item.target === '_blank' ? 'noopener noreferrer' : undefined" - :download="item.download" - @click.passive="close(true)" - @mouseenter.passive="onItemMouseEnter" - @mouseleave.passive="onItemMouseLeave" - > - <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" class="_blink"><i class="_indicatorCircle"></i></span> - </div> - </a> - <button - v-else-if="item.type === 'user'" - role="menuitem" - tabindex="0" - :class="['_button', $style.item, { [$style.active]: item.active }]" - @click.prevent="item.active ? close(false) : clicked(item.action, $event)" - @mouseenter.passive="onItemMouseEnter" - @mouseleave.passive="onItemMouseLeave" - > - <MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/> - <div v-if="item.indicate" :class="$style.item_content"> - <span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> - </div> - </button> - <button - v-else-if="item.type === 'switch'" - role="menuitemcheckbox" - tabindex="0" - :class="['_button', $style.item]" - :disabled="unref(item.disabled)" - @click.prevent="switchItem(item)" - @mouseenter.passive="onItemMouseEnter" - @mouseleave.passive="onItemMouseLeave" - > - <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> - <MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> - <div :class="$style.item_content"> - <span :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]">{{ item.text }}</span> - <MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> - </div> - </button> - <button - v-else-if="item.type === 'radio'" - role="menuitem" - tabindex="0" - :class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]" - :disabled="unref(item.disabled)" - @mouseenter.prevent="preferClick ? null : showRadioOptions(item, $event)" - @keydown.enter.prevent="preferClick ? null : showRadioOptions(item, $event)" - @click.prevent="!preferClick ? null : showRadioOptions(item, $event)" - > - <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> - <div :class="$style.item_content"> - <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> - <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> - </div> - </button> - <button - v-else-if="item.type === 'radioOption'" - role="menuitemradio" - tabindex="0" - :class="['_button', $style.item, $style.radio, { [$style.active]: unref(item.active) }]" - @click.prevent="unref(item.active) ? null : clicked(item.action, $event, false)" - @mouseenter.passive="onItemMouseEnter" - @mouseleave.passive="onItemMouseLeave" - > - <div :class="$style.icon"> - <span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span> - </div> - <div :class="$style.item_content"> - <span :class="$style.item_content_text">{{ item.text }}</span> - </div> - </button> - <button - v-else-if="item.type === 'parent'" - role="menuitem" - tabindex="0" - :class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]" - @mouseenter.prevent="preferClick ? null : showChildren(item, $event)" - @keydown.enter.prevent="preferClick ? null : showChildren(item, $event)" - @click.prevent="!preferClick ? null : showChildren(item, $event)" - > - <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> - <div :class="$style.item_content"> - <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> - <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> - </div> - </button> - <button - v-else role="menuitem" - tabindex="0" - :class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]" - @click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)" - @mouseenter.passive="onItemMouseEnter" - @mouseleave.passive="onItemMouseLeave" - > - <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> - <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> - <div :class="$style.item_content"> - <span :class="$style.item_content_text">{{ item.text }}</span> - <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> - </div> - </button> - </template> - <span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]"> - <span>{{ i18n.ts.none }}</span> - </span> + </div> + <slot name="footer"></slot> </div> <div v-if="childMenu"> <XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" @actioned="childActioned" @closed="closeChild"/> @@ -429,7 +435,7 @@ onBeforeUnmount(() => { .root { &.center { > .menu { - > .item { + .item { text-align: center; } } @@ -439,7 +445,7 @@ onBeforeUnmount(() => { > .menu { min-width: 230px; - > .item { + .item { padding: 6px 20px; font-size: 0.95em; line-height: 24px; @@ -452,36 +458,38 @@ onBeforeUnmount(() => { margin: auto; > .menu { - padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; width: 100%; border-radius: 24px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; - > .item { - font-size: 1em; - padding: 12px 24px; + > .menuItems { + padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; - &::before { - width: calc(100% - 24px); - border-radius: 12px; + > .item { + font-size: 1em; + padding: 12px 24px; + + &::before { + width: calc(100% - 24px); + border-radius: 12px; + } + + > .icon { + margin-right: 14px; + width: 24px; + } } - > .icon { - margin-right: 14px; - width: 24px; + > .divider { + margin: 12px 0; } } - - > .divider { - margin: 12px 0; - } } } } .menu { - padding: 8px 0; box-sizing: border-box; max-width: 100vw; min-width: 200px; @@ -493,6 +501,11 @@ onBeforeUnmount(() => { } } +.menuItems { + padding: 8px 0; + box-sizing: border-box; +} + .item { display: flex; align-items: center; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index d57300f647..e31c33149f 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.headerRight"> <template v-if="!(channel != null && fixed)"> - <button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility"> + <button v-if="channel == null" ref="visibilityButton" v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility"> <span v-if="visibility === 'public'"><i class="ti ti-world"></i></span> <span v-if="visibility === 'home'"><i class="ti ti-home"></i></span> <span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span> @@ -32,15 +32,11 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="$style.headerRightButtonText">{{ channel.name }}</span> </button> </template> - <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> + <button v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> <span v-if="!localOnly"><i class="ti ti-rocket"></i></span> <span v-else><i class="ti ti-rocket-off"></i></span> </button> - <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance"> - <span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span> - <span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span> - <span v-else><i class="ti ti-icons"></i></span> - </button> + <button ref="otherSettingsButton" v-tooltip="i18n.ts.other" class="_button" :class="$style.headerRightItem" @click="showOtherSettings"><i class="ti ti-dots"></i></button> <button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> <div :class="$style.submitInner"> <template v-if="posted"></template> @@ -111,6 +107,7 @@ import { toASCII } from 'punycode.js'; import { host, url } from '@@/js/config.js'; import type { ShallowRef } from 'vue'; import type { PostFormProps } from '@/types/post-form.js'; +import type { MenuItem } from '@/types/menu.js'; import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; import MkNotePreview from '@/components/MkNotePreview.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; @@ -171,6 +168,7 @@ const textareaEl = shallowRef<HTMLTextAreaElement | null>(null); const cwInputEl = shallowRef<HTMLInputElement | null>(null); const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null); const visibilityButton = shallowRef<HTMLElement>(); +const otherSettingsButton = shallowRef<HTMLElement>(); const posting = ref(false); const posted = ref(false); @@ -556,6 +554,47 @@ async function toggleReactionAcceptance() { reactionAcceptance.value = select.result; } +//#region その他の設定メニューpopup +function showOtherSettings() { + let reactionAcceptanceIcon = 'ti ti-icons'; + + if (reactionAcceptance.value === 'likeOnly') { + reactionAcceptanceIcon = 'ti ti-heart _love'; + } else if (reactionAcceptance.value === 'likeOnlyForRemote') { + reactionAcceptanceIcon = 'ti ti-heart-plus'; + } + + const menuDef = [{ + icon: reactionAcceptanceIcon, + text: i18n.ts.reactionAcceptance, + action: () => { + toggleReactionAcceptance(); + }, + }, { type: 'divider' }, { + icon: 'ti ti-trash', + text: i18n.ts.reset, + danger: true, + action: async () => { + if (props.mock) return; + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.ts.resetAreYouSure, + }); + if (canceled) return; + clear(); + }, + }] satisfies MenuItem[]; + + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkPostFormOtherMenu.vue')), { + items: menuDef, + textLength: textLength.value, + src: otherSettingsButton.value, + }, { + closed: () => dispose(), + }); +} +//#endregion + function pushVisibleUser(user: Misskey.entities.UserDetailed) { if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) { visibleUsers.value.push(user); diff --git a/packages/frontend/src/components/MkPostFormOtherMenu.vue b/packages/frontend/src/components/MkPostFormOtherMenu.vue new file mode 100644 index 0000000000..7b3e6625f1 --- /dev/null +++ b/packages/frontend/src/components/MkPostFormOtherMenu.vue @@ -0,0 +1,128 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModal ref="modal" v-slot="{ type, maxHeight }" :zPriority="'high'" :src="src" :transparentBg="true" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()"> + <MkMenu + :items="items" + :align="align" + :width="width" + :maxHeight="maxHeight" + :asDrawer="type === 'drawer'" + @close="modal?.close()" + > + <template #header> + <div :class="[$style.textCountRoot, { [$style.asDrawer]: type === 'drawer' }]"> + <div :class="$style.textCountLabel">{{ i18n.ts.textCount }}</div> + <div + :class="[$style.textCount, + { [$style.danger]: textCountPercentage > 100 }, + { [$style.warning]: textCountPercentage > 90 && textCountPercentage <= 100 }, + ]" + > + <div :class="$style.textCountGraph"></div> + <div><span :class="$style.textCountCurrent">{{ number(textLength) }}</span> / {{ number(maxTextLength) }}</div> + </div> + </div> + </template> + </MkMenu> +</MkModal> +</template> + +<script lang="ts" setup> +import { computed, useTemplateRef } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkModal from '@/components/MkModal.vue'; +import MkMenu from '@/components/MkMenu.vue'; +import { instance } from '@/instance.js'; +import { i18n } from '@/i18n.js'; +import number from '@/filters/number.js'; +import type { MenuItem } from '@/types/menu.js'; + +const modal = useTemplateRef('modal'); + +const props = defineProps<{ + items: MenuItem[]; + textLength: number; + align?: 'center' | string; + width?: number; + src?: HTMLElement; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; +}>(); + +const maxTextLength = computed(() => { + return instance ? instance.maxNoteTextLength : 1000; +}); + +const textCountPercentage = computed(() => { + return props.textLength / maxTextLength.value * 100; +}); +</script> + +<style lang="scss" module> +.textCountRoot { + --textCountBg: color-mix(in srgb, var(--MI_THEME-panel), var(--MI_THEME-fg) 15%); + background-color: var(--textCountBg); + padding: 10px 14px; + + &.asDrawer { + padding: 12px 24px; + } +} + +.textCountLabel { + font-size: 11px; + opacity: 0.8; + margin-bottom: 4px; +} + +.textCount { + display: flex; + gap: var(--MI-marginHalf); + align-items: center; + font-size: 12px; + --countColor: var(--MI_THEME-accent); + + &.danger { + --countColor: var(--MI_THEME-error); + } + + &.warning { + --countColor: var(--MI_THEME-warn); + } + + .textCountGraph { + position: relative; + width: 24px; + height: 24px; + border-radius: 50%; + background-image: conic-gradient( + var(--countColor) 0% v-bind("Math.min(100, textCountPercentage) + '%'"), + rgba(0, 0, 0, .2) v-bind("Math.min(100, textCountPercentage) + '%'") 100% + ); + + &::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + border-radius: 50%; + background-color: var(--textCountBg); + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } + + .textCountCurrent { + color: var(--countColor); + font-weight: 700; + font-size: 18px; + } +} +</style> diff --git a/packages/frontend/src/filters/number.ts b/packages/frontend/src/filters/number.ts index 10fb64deb4..479afd58d4 100644 --- a/packages/frontend/src/filters/number.ts +++ b/packages/frontend/src/filters/number.ts @@ -5,4 +5,4 @@ import { numberFormat } from '@@/js/intl-const.js'; -export default n => n == null ? 'N/A' : numberFormat.format(n); +export default (n?: number) => n == null ? 'N/A' : numberFormat.format(n); diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 43a97d49eb..c449b0e956 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -436,6 +436,10 @@ rt { color: var(--MI_THEME-link); } +._love { + color: var(--MI_THEME-love); +} + ._caption { font-size: 0.8em; opacity: 0.7;