summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-05-12 12:41:53 +0900
committerGitHub <noreply@github.com>2023-05-12 12:41:53 +0900
commitde6348e8a00a72c2410a8907d59a9bd29142a200 (patch)
treef163329a67ac5c6dc1c33845b4fc131a8c59a39b /packages/frontend/src/components
parentMerge pull request #10814 from misskey-dev/develop (diff)
parentfix(frontend): fix retention rate heatmap rendering (diff)
downloadmisskey-de6348e8a00a72c2410a8907d59a9bd29142a200.tar.gz
misskey-de6348e8a00a72c2410a8907d59a9bd29142a200.tar.bz2
misskey-de6348e8a00a72c2410a8907d59a9bd29142a200.zip
Merge pull request #10833 from misskey-dev/develop
* refactor(frontend): use css modules * feat: 投稿したコンテンツのAIによる学習を軽減するオプションを追加 Resolve #10819 * enhance(backend): publicReactionsをデフォルトtrueに * 念のためnoimageaiもつける * add X-Robots-Tag: noai * Update ja-JP.yml * fix(frontend): ブラーエフェクトを有効にしている状態で高負荷になる問題を修正 * enhance(backend): graceful shutdown for job queue and refactor * fix(backend): テスト時は一部のサービスを停止 * fix test * New Crowdin updates (#10815) * New translations ja-JP.yml (English) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (Chinese Traditional) * refactor * bump * refactor(frontend): use css module * refactor(frontend): use css module * delete unused component * センシティブワードを正規表現、CWにも適用するように (#10688) * cwにセンシティブが効いてない * CWが無いときにTextを見るように * 比較演算子間違えた * とりあえずチェック * 正規表現対応 * /test/giにも対応 * matchでしなくてもいいのでは感 * レビュー修正 * Update packages/backend/src/core/NoteCreateService.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * Update packages/backend/src/core/NoteCreateService.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * 修正 * wipかも * wordsでスペース区切りのものできたかも * なんか動いたかも * test作成 * 文言の修正 * 修正 * note参照 --------- Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * Update CHANGELOG.md * New Crowdin updates (#10823) * New translations ja-JP.yml (English) * New translations ja-JP.yml (German) * ci: fix typo * fix(frontend): より明確な説明にしたのとtypo修正 * fix typo * fix(frontend): カラーバーがリプライには表示されないのを修正 * fix(frontend): チャンネル内の検索ボックスが挙動不審な問題を修正 Fix #10793 * enhance(backend): ノートのハッシュタグもMeilisearchに突っ込むように 今後ハッシュタグ検索とか実装するときのため * feat(frontend): ユーザー指定ノート検索 * fix(frontend): fix retention chart rendering * Update about-misskey.vue * meta: Remove @rinsuki from reviewer-lottery (#10830) * New Crowdin updates (#10824) * New translations ja-JP.yml (English) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (German) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (French) * New translations ja-JP.yml (German) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Italian) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Norwegian) * New translations ja-JP.yml (Russian) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Indonesian) * New translations ja-JP.yml (Thai) * enhance(frontend): アカウント初期設定ウィザードにプライバシー設定を追加 * Update CHANGELOG.md * fix(backend): ひとつのMeilisearchサーバーを複数のMisskeyサーバーで使えない問題を修正 * fix MkUserSetupDialog.Privacy.vue * ci: skip non-Japanese locale on TurboSnap * ci: notify on changes for push events * ci: fix missing branch * Update basic.cy.js * [ci skip] New Crowdin updates (#10834) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Arabic) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (Arabic) * :art: * :art: * enhance(frontend): add retention line chart * update deps * refactor * fix(frontend): Pageにおいて画像ブロックに画像を設定できない問題を修正 Fix #10837 --------- Co-authored-by: nenohi <kimutipartylove@gmail.com> Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> Co-authored-by: rinsuki <428rinsuki+git@gmail.com>
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkCheckbox.vue144
-rw-r--r--packages/frontend/src/components/MkInstanceStats.vue13
-rw-r--r--packages/frontend/src/components/MkModalWindow.vue97
-rw-r--r--packages/frontend/src/components/MkNoteSub.vue12
-rw-r--r--packages/frontend/src/components/MkRadio.vue93
-rw-r--r--packages/frontend/src/components/MkRetentionHeatmap.vue22
-rw-r--r--packages/frontend/src/components/MkRetentionLineChart.vue130
-rw-r--r--packages/frontend/src/components/MkSelect.vue197
-rw-r--r--packages/frontend/src/components/MkSwitch.vue149
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Follow.vue4
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts31
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Privacy.vue64
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Profile.vue4
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.vue50
-rw-r--r--packages/frontend/src/components/MkWidgets.vue2
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue20
-rw-r--r--packages/frontend/src/components/page/page.image.vue12
17 files changed, 577 insertions, 467 deletions
diff --git a/packages/frontend/src/components/MkCheckbox.vue b/packages/frontend/src/components/MkCheckbox.vue
deleted file mode 100644
index a8e24dd839..0000000000
--- a/packages/frontend/src/components/MkCheckbox.vue
+++ /dev/null
@@ -1,144 +0,0 @@
-<template>
-<div
- class="ziffeoms"
- :class="{ disabled, checked }"
->
- <input
- ref="input"
- type="checkbox"
- :disabled="disabled"
- @keydown.enter="toggle"
- >
- <span ref="button" v-adaptive-border v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle">
- <i class="check ti ti-check"></i>
- </span>
- <span class="label">
- <!-- TODO: 無名slotの方は廃止 -->
- <span @click="toggle"><slot name="label"></slot><slot></slot></span>
- <p class="caption"><slot name="caption"></slot></p>
- </span>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { toRefs, Ref } from 'vue';
-import * as os from '@/os';
-import MkRippleEffect from '@/components/MkRippleEffect.vue';
-import { i18n } from '@/i18n';
-
-const props = defineProps<{
- modelValue: boolean | Ref<boolean>;
- disabled?: boolean;
-}>();
-
-const emit = defineEmits<{
- (ev: 'update:modelValue', v: boolean): void;
-}>();
-
-let button = $shallowRef<HTMLElement>();
-const checked = toRefs(props).modelValue;
-const toggle = () => {
- if (props.disabled) return;
- emit('update:modelValue', !checked.value);
-
- if (!checked.value) {
- const rect = button.getBoundingClientRect();
- const x = rect.left + (button.offsetWidth / 2);
- const y = rect.top + (button.offsetHeight / 2);
- os.popup(MkRippleEffect, { x, y, particle: false }, {}, 'end');
- }
-};
-</script>
-
-<style lang="scss" scoped>
-.ziffeoms {
- position: relative;
- display: flex;
- transition: all 0.2s ease;
-
- > * {
- user-select: none;
- }
-
- > input {
- position: absolute;
- width: 0;
- height: 0;
- opacity: 0;
- margin: 0;
- }
-
- > .button {
- position: relative;
- display: inline-flex;
- flex-shrink: 0;
- margin: 0;
- box-sizing: border-box;
- width: 23px;
- height: 23px;
- outline: none;
- background: var(--panel);
- border: solid 1px var(--panel);
- border-radius: 4px;
- cursor: pointer;
- transition: inherit;
-
- > .check {
- margin: auto;
- opacity: 0;
- color: var(--fgOnAccent);
- font-size: 13px;
- transform: scale(0.5);
- transition: all 0.2s ease;
- }
- }
-
- &:hover {
- > .button {
- border-color: var(--inputBorderHover) !important;
- }
- }
-
- > .label {
- margin-left: 12px;
- margin-top: 2px;
- display: block;
- transition: inherit;
- color: var(--fg);
-
- > span {
- display: block;
- line-height: 20px;
- cursor: pointer;
- transition: inherit;
- }
-
- > .caption {
- margin: 8px 0 0 0;
- color: var(--fgTransparentWeak);
- font-size: 0.85em;
-
- &:empty {
- display: none;
- }
- }
- }
-
- &.disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
-
- &.checked {
- > .button {
- background-color: var(--accent) !important;
- border-color: var(--accent) !important;
-
- > .check {
- opacity: 1;
- transform: scale(1);
- }
- }
- }
-}
-</style>
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 0f87fef6b1..6fcd8f7811 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -52,9 +52,12 @@
<MkFoldableSection class="item">
<template #header>Retention rate</template>
- <div class="_panel" :class="$style.retention">
+ <div class="_panel" :class="$style.retentionHeatmap">
<MkRetentionHeatmap/>
</div>
+ <div class="_panel" :class="$style.retentionLine">
+ <MkRetentionLineChart/>
+ </div>
</MkFoldableSection>
<MkFoldableSection class="item">
@@ -86,6 +89,7 @@ import { i18n } from '@/i18n';
import MkHeatmap from '@/components/MkHeatmap.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
+import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
import { initChart } from '@/scripts/init-chart';
initChart();
@@ -202,7 +206,12 @@ onMounted(() => {
margin-bottom: 16px;
}
-.retention {
+.retentionHeatmap {
+ padding: 16px;
+ margin-bottom: 16px;
+}
+
+.retentionLine {
padding: 16px;
margin-bottom: 16px;
}
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index ad7dc4da11..63c55b904a 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -1,15 +1,15 @@
<template>
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
- <div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
- <div ref="headerEl" class="header">
- <button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
- <span class="title">
+ <div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
+ <div ref="headerEl" :class="$style.header">
+ <button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
+ <span :class="$style.title">
<slot name="header"></slot>
</span>
- <button v-if="!withOkButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button>
- <button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button>
+ <button v-if="!withOkButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button>
+ <button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button>
</div>
- <div class="body">
+ <div :class="$style.body">
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
@@ -81,8 +81,8 @@ defineExpose({
});
</script>
-<style lang="scss" scoped>
-.ebkgoccj {
+<style lang="scss" module>
+.root {
margin: auto;
overflow: hidden;
display: flex;
@@ -96,51 +96,52 @@ defineExpose({
--root-margin: 16px;
}
- > .header {
- $height: 46px;
- $height-narrow: 42px;
- display: flex;
- flex-shrink: 0;
- background: var(--windowHeader);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ --headerHeight: 46px;
+ --headerHeightNarrow: 42px;
+}
- > button {
- height: $height;
- width: $height;
+.header {
+ display: flex;
+ flex-shrink: 0;
+ background: var(--windowHeader);
+ -webkit-backdrop-filter: var(--blur, blur(15px));
+ backdrop-filter: var(--blur, blur(15px));
+}
- @media (max-width: 500px) {
- height: $height-narrow;
- width: $height-narrow;
- }
- }
+.headerButton {
+ height: var(--headerHeight);
+ width: var(--headerHeight);
- > .title {
- flex: 1;
- line-height: $height;
- padding-left: 32px;
- font-weight: bold;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- pointer-events: none;
+ @media (max-width: 500px) {
+ height: var(--headerHeightNarrow);
+ width: var(--headerHeightNarrow);
+ }
+}
- @media (max-width: 500px) {
- line-height: $height-narrow;
- padding-left: 16px;
- }
- }
+.title {
+ flex: 1;
+ line-height: var(--headerHeight);
+ padding-left: 32px;
+ font-weight: bold;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ pointer-events: none;
- > button + .title {
- padding-left: 0;
- }
+ @media (max-width: 500px) {
+ line-height: var(--headerHeightNarrow);
+ padding-left: 16px;
}
+}
- > .body {
- flex: 1;
- overflow: auto;
- background: var(--panel);
- container-type: size;
- }
+.headerButton + .title {
+ padding-left: 0;
+}
+
+.body {
+ flex: 1;
+ overflow: auto;
+ background: var(--panel);
+ container-type: size;
}
</style>
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index c293641355..9ac0b7858f 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -1,6 +1,7 @@
<template>
<div :class="[$style.root, { [$style.children]: depth > 1 }]">
<div :class="$style.main">
+ <div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
<div :class="$style.body">
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
@@ -62,6 +63,7 @@ if (props.detail) {
.root {
padding: 16px 32px;
font-size: 0.9em;
+ position: relative;
&.children {
padding: 10px 0 0 16px;
@@ -73,6 +75,16 @@ if (props.detail) {
display: flex;
}
+.colorBar {
+ position: absolute;
+ top: 8px;
+ left: 8px;
+ width: 5px;
+ height: calc(100% - 8px);
+ border-radius: 999px;
+ pointer-events: none;
+}
+
.avatar {
flex-shrink: 0;
display: block;
diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue
index 5db2f5ee6d..eea94d4692 100644
--- a/packages/frontend/src/components/MkRadio.vue
+++ b/packages/frontend/src/components/MkRadio.vue
@@ -1,8 +1,7 @@
<template>
<div
v-adaptive-border
- class="novjtctn"
- :class="{ disabled, checked }"
+ :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
:aria-checked="checked"
:aria-disabled="disabled"
@click="toggle"
@@ -10,11 +9,12 @@
<input
type="radio"
:disabled="disabled"
+ :class="$style.input"
>
- <span class="button">
+ <span :class="$style.button">
<span></span>
</span>
- <span class="label"><slot></slot></span>
+ <span :class="$style.label"><slot></slot></span>
</div>
</template>
@@ -39,8 +39,8 @@ function toggle(): void {
}
</script>
-<style lang="scss" scoped>
-.novjtctn {
+<style lang="scss" module>
+.root {
position: relative;
display: inline-block;
text-align: left;
@@ -53,17 +53,11 @@ function toggle(): void {
border-radius: 6px;
font-size: 90%;
transition: all 0.2s;
-
- > * {
- user-select: none;
- }
+ user-select: none;
&.disabled {
opacity: 0.6;
-
- &, * {
- cursor: not-allowed !important;
- }
+ cursor: not-allowed !important;
}
&:hover {
@@ -74,10 +68,7 @@ function toggle(): void {
background-color: var(--accentedBg) !important;
border-color: var(--accentedBg) !important;
color: var(--accent);
-
- &, * {
- cursor: default !important;
- }
+ cursor: default !important;
> .button {
border-color: var(--accent);
@@ -89,44 +80,44 @@ function toggle(): void {
}
}
}
+}
- > input {
- position: absolute;
- width: 0;
- height: 0;
- opacity: 0;
- margin: 0;
- }
+.input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ margin: 0;
+}
+
+.button {
+ position: absolute;
+ width: 14px;
+ height: 14px;
+ background: none;
+ border: solid 2px var(--inputBorder);
+ border-radius: 100%;
+ transition: inherit;
- > .button {
+ &:after {
+ content: '';
+ display: block;
position: absolute;
- width: 14px;
- height: 14px;
- background: none;
- border: solid 2px var(--inputBorder);
+ top: 3px;
+ right: 3px;
+ bottom: 3px;
+ left: 3px;
border-radius: 100%;
- transition: inherit;
-
- &:after {
- content: '';
- display: block;
- position: absolute;
- top: 3px;
- right: 3px;
- bottom: 3px;
- left: 3px;
- border-radius: 100%;
- opacity: 0;
- transform: scale(0);
- transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
- }
+ opacity: 0;
+ transform: scale(0);
+ transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
+}
- > .label {
- margin-left: 28px;
- display: block;
- line-height: 20px;
- cursor: pointer;
- }
+.label {
+ margin-left: 28px;
+ display: block;
+ line-height: 20px;
+ cursor: pointer;
}
</style>
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index f33f68cab7..311d5c425c 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -40,7 +40,7 @@ async function renderChart() {
let raw = await os.api('retention', { });
- raw = raw.slice(0, maxDays);
+ raw = raw.slice(0, maxDays + 1);
const data = [];
for (const record of raw) {
@@ -90,8 +90,13 @@ async function renderChart() {
borderRadius: 3,
backgroundColor(c) {
const value = c.dataset.data[c.dataIndex].v;
- const a = value / max(c.dataset.data[c.dataIndex].y);
- return alpha(color, a);
+ const m = max(c.dataset.data[c.dataIndex].y);
+ if (m === 0) {
+ return alpha(color, 0);
+ } else {
+ const a = value / m;
+ return alpha(color, a);
+ }
},
fill: true,
width(c) {
@@ -129,6 +134,10 @@ async function renderChart() {
autoSkip: false,
callback: (value, index, values) => value,
},
+ title: {
+ display: true,
+ text: 'Days later',
+ },
},
y: {
type: 'time',
@@ -166,7 +175,12 @@ async function renderChart() {
},
label(context) {
const v = context.dataset.data[context.dataIndex];
- return [`Active: ${v.v} (${Math.round((v.v / max(v.y)) * 100)}%)`];
+ const m = max(v.y);
+ if (m === 0) {
+ return [`Active: ${v.v} (-%)`];
+ } else {
+ return [`Active: ${v.v} (${Math.round((v.v / m) * 100)}%)`];
+ }
},
},
//mode: 'index',
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
new file mode 100644
index 0000000000..8bd0279806
--- /dev/null
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -0,0 +1,130 @@
+<template>
+<canvas ref="chartEl"></canvas>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, shallowRef } from 'vue';
+import { Chart } from 'chart.js';
+import tinycolor from 'tinycolor2';
+import { defaultStore } from '@/store';
+import { useChartTooltip } from '@/scripts/use-chart-tooltip';
+import { chartVLine } from '@/scripts/chart-vline';
+import { alpha } from '@/scripts/color';
+import { initChart } from '@/scripts/init-chart';
+import * as os from '@/os';
+
+initChart();
+
+const chartEl = shallowRef<HTMLCanvasElement>(null);
+
+const { handler: externalTooltipHandler } = useChartTooltip();
+
+let chartInstance: Chart;
+
+const getYYYYMMDD = (date: Date) => {
+ const y = date.getFullYear().toString().padStart(2, '0');
+ const m = (date.getMonth() + 1).toString().padStart(2, '0');
+ const d = date.getDate().toString().padStart(2, '0');
+ return `${y}/${m}/${d}`;
+};
+
+const getDate = (ymd: string) => {
+ const [y, m, d] = ymd.split('-').map(x => parseInt(x, 10));
+ const date = new Date(y, m + 1, d, 0, 0, 0, 0);
+ return date;
+};
+
+onMounted(async () => {
+ let raw = await os.api('retention', { });
+
+ 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 color = accent.toHex();
+
+ chartInstance = new Chart(chartEl.value, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: raw.map((record, i) => ({
+ label: getYYYYMMDD(new Date(record.createdAt)),
+ pointRadius: 0,
+ borderWidth: 2,
+ borderJoinStyle: 'round',
+ borderColor: alpha(color, Math.min(1, (raw.length - (i - 1)) / raw.length)),
+ fill: false,
+ tension: 0.4,
+ data: [{
+ x: '0',
+ y: 100,
+ d: getYYYYMMDD(new Date(record.createdAt)),
+ }, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({
+ x: (i + 1).toString(),
+ y: (v / record.users) * 100,
+ d: getYYYYMMDD(new Date(record.createdAt)),
+ }))],
+ })),
+ },
+ options: {
+ aspectRatio: 2.5,
+ layout: {
+ padding: {
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ },
+ },
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: 'Days later',
+ },
+ },
+ y: {
+ title: {
+ display: true,
+ text: 'Rate (%)',
+ },
+ ticks: {
+ callback: (value, index, values) => value + '%',
+ },
+ },
+ },
+ interaction: {
+ intersect: false,
+ },
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ enabled: false,
+ callbacks: {
+ title(context) {
+ const v = context[0].dataset.data[context[0].dataIndex];
+ return `${v.x} days later`;
+ },
+ label(context) {
+ const v = context.dataset.data[context.dataIndex];
+ const p = Math.round(v.y) + '%';
+ return `${v.d} ${p}`;
+ },
+ },
+ mode: 'index',
+ animation: {
+ duration: 0,
+ },
+ external: externalTooltipHandler,
+ },
+ },
+ },
+ plugins: [chartVLine(vLineColor)],
+ });
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 2de890186a..4efb65c287 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -1,13 +1,13 @@
<template>
-<div class="vblkjoeq">
- <div class="label" @click="focus"><slot name="label"></slot></div>
- <div ref="container" class="input" :class="{ inline, disabled, focused }" @mousedown.prevent="show">
- <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
+<div>
+ <div :class="$style.label" @click="focus"><slot name="label"></slot></div>
+ <div ref="container" :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]" @mousedown.prevent="show">
+ <div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
<select
ref="inputEl"
v-model="v"
v-adaptive-border
- class="select"
+ :class="$style.inputCore"
:disabled="disabled"
:required="required"
:readonly="readonly"
@@ -18,9 +18,9 @@
>
<slot></slot>
</select>
- <div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
+ <div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
</div>
- <div class="caption"><slot name="caption"></slot></div>
+ <div :class="$style.caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
@@ -169,121 +169,116 @@ function show(ev: MouseEvent) {
}
</script>
-<style lang="scss" scoped>
-.vblkjoeq {
- > .label {
- font-size: 0.85em;
- padding: 0 0 8px 0;
- user-select: none;
+<style lang="scss" module>
+.label {
+ font-size: 0.85em;
+ padding: 0 0 8px 0;
+ user-select: none;
- &:empty {
- display: none;
- }
+ &:empty {
+ display: none;
}
+}
- > .caption {
- font-size: 0.85em;
- padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+.caption {
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
- &:empty {
- display: none;
- }
+ &:empty {
+ display: none;
}
+}
- > .input {
- position: relative;
- cursor: pointer;
+.input {
+ position: relative;
+ cursor: pointer;
- &:hover {
- > .select {
- border-color: var(--inputBorderHover) !important;
- }
- }
+ &.inline {
+ display: inline-block;
+ margin: 0;
+ }
- > .select {
- appearance: none;
- -webkit-appearance: none;
- display: block;
- height: v-bind("height + 'px'");
- width: 100%;
- margin: 0;
- padding: 0 12px;
- font: inherit;
- font-weight: normal;
- font-size: 1em;
- color: var(--fg);
- background: var(--panel);
- border: solid 1px var(--panel);
- border-radius: 6px;
- outline: none;
- box-shadow: none;
- box-sizing: border-box;
- cursor: pointer;
- transition: border-color 0.1s ease-out;
- pointer-events: none;
- user-select: none;
+ &.focused {
+ > .inputCore {
+ border-color: var(--accent) !important;
+ //box-shadow: 0 0 0 4px var(--focus);
}
+ }
- > .prefix,
- > .suffix {
- display: flex;
- align-items: center;
- position: absolute;
- z-index: 1;
- top: 0;
- padding: 0 12px;
- font-size: 1em;
- height: v-bind("height + 'px'");
- pointer-events: none;
-
- &:empty {
- display: none;
- }
+ &.disabled {
+ opacity: 0.7;
- > * {
- display: inline-block;
- min-width: 16px;
- max-width: 150px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
+ &,
+ > .inputCore {
+ cursor: not-allowed !important;
}
+ }
- > .prefix {
- left: 0;
- padding-right: 6px;
+ &:hover {
+ > .inputCore {
+ border-color: var(--inputBorderHover) !important;
}
+ }
+}
- > .suffix {
- right: 0;
- padding-left: 6px;
- }
+.inputCore {
+ appearance: none;
+ -webkit-appearance: none;
+ display: block;
+ height: v-bind("height + 'px'");
+ width: 100%;
+ margin: 0;
+ padding: 0 12px;
+ font: inherit;
+ font-weight: normal;
+ font-size: 1em;
+ color: var(--fg);
+ background: var(--panel);
+ border: solid 1px var(--panel);
+ border-radius: 6px;
+ outline: none;
+ box-shadow: none;
+ box-sizing: border-box;
+ transition: border-color 0.1s ease-out;
+ cursor: pointer;
+ pointer-events: none;
+ user-select: none;
+}
- &.inline {
- display: inline-block;
- margin: 0;
- }
+.prefix,
+.suffix {
+ display: flex;
+ align-items: center;
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ padding: 0 12px;
+ font-size: 1em;
+ height: v-bind("height + 'px'");
+ min-width: 16px;
+ max-width: 150px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ box-sizing: border-box;
+ pointer-events: none;
- &.focused {
- > select {
- border-color: var(--accent) !important;
- }
- }
+ &:empty {
+ display: none;
+ }
+}
- &.disabled {
- opacity: 0.7;
+.prefix {
+ left: 0;
+ padding-right: 6px;
+}
- &, * {
- cursor: not-allowed !important;
- }
- }
- }
+.suffix {
+ right: 0;
+ padding-left: 6px;
}
-</style>
-<style lang="scss" module>
.chevron {
transition: transform 0.1s ease-out;
}
diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue
index d9f6716f92..63738b6a44 100644
--- a/packages/frontend/src/components/MkSwitch.vue
+++ b/packages/frontend/src/components/MkSwitch.vue
@@ -1,21 +1,19 @@
<template>
-<div
- class="ziffeomt"
- :class="{ disabled, checked }"
->
+<div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]">
<input
ref="input"
type="checkbox"
:disabled="disabled"
+ :class="$style.input"
@keydown.enter="toggle"
>
- <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle">
- <div class="knob"></div>
+ <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle">
+ <div :class="$style.knob"></div>
</span>
- <span class="label">
+ <span :class="$style.body">
<!-- TODO: 無名slotの方は廃止 -->
- <span @click="toggle"><slot name="label"></slot><slot></slot></span>
- <p class="caption"><slot name="caption"></slot></p>
+ <span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
+ <p :class="$style.caption"><slot name="caption"></slot></p>
</span>
</div>
</template>
@@ -45,52 +43,12 @@ const toggle = () => {
};
</script>
-<style lang="scss" scoped>
-.ziffeomt {
+<style lang="scss" module>
+.root {
position: relative;
display: flex;
transition: all 0.2s ease;
-
- > * {
- user-select: none;
- }
-
- > input {
- position: absolute;
- width: 0;
- height: 0;
- opacity: 0;
- margin: 0;
- }
-
- > .button {
- position: relative;
- display: inline-flex;
- flex-shrink: 0;
- margin: 0;
- box-sizing: border-box;
- width: 32px;
- height: 23px;
- outline: none;
- background: var(--switchOffBg);
- background-clip: content-box;
- border: solid 1px var(--switchOffBg);
- border-radius: 999px;
- cursor: pointer;
- transition: inherit;
- user-select: none;
-
- > .knob {
- position: absolute;
- top: 3px;
- left: 3px;
- width: 15px;
- height: 15px;
- background: var(--switchOffFg);
- border-radius: 999px;
- transition: all 0.2s ease;
- }
- }
+ user-select: none;
&:hover {
> .button {
@@ -98,31 +56,6 @@ const toggle = () => {
}
}
- > .label {
- margin-left: 12px;
- margin-top: 2px;
- display: block;
- transition: inherit;
- color: var(--fg);
-
- > span {
- display: block;
- line-height: 20px;
- cursor: pointer;
- transition: inherit;
- }
-
- > .caption {
- margin: 8px 0 0 0;
- color: var(--fgTransparentWeak);
- font-size: 0.85em;
-
- &:empty {
- display: none;
- }
- }
- }
-
&.disabled {
opacity: 0.6;
cursor: not-allowed;
@@ -140,4 +73,66 @@ const toggle = () => {
}
}
}
+
+.input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ margin: 0;
+}
+
+.button {
+ position: relative;
+ display: inline-flex;
+ flex-shrink: 0;
+ margin: 0;
+ box-sizing: border-box;
+ width: 32px;
+ height: 23px;
+ outline: none;
+ background: var(--switchOffBg);
+ background-clip: content-box;
+ border: solid 1px var(--switchOffBg);
+ border-radius: 999px;
+ cursor: pointer;
+ transition: inherit;
+ user-select: none;
+}
+
+.knob {
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ width: 15px;
+ height: 15px;
+ background: var(--switchOffFg);
+ border-radius: 999px;
+ transition: all 0.2s ease;
+}
+
+.body {
+ margin-left: 12px;
+ margin-top: 2px;
+ display: block;
+ transition: inherit;
+ color: var(--fg);
+}
+
+.label {
+ display: block;
+ line-height: 20px;
+ cursor: pointer;
+ transition: inherit;
+}
+
+.caption {
+ margin: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
+ font-size: 0.85em;
+
+ &:empty {
+ display: none;
+ }
+}
</style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index b89e3e4c9d..a2a195cb09 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -40,10 +40,6 @@ import * as os from '@/os';
import { $i } from '@/account';
import MkPagination from '@/components/MkPagination.vue';
-const emit = defineEmits<{
- (ev: 'done'): void;
-}>();
-
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts
new file mode 100644
index 0000000000..70817d83c3
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts
@@ -0,0 +1,31 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import MkUserSetupDialog_Privacy from './MkUserSetupDialog.Privacy.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkUserSetupDialog_Privacy,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkUserSetupDialog_Privacy v-bind="props" />',
+ };
+ },
+ args: {
+
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkUserSetupDialog_Privacy>;
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
new file mode 100644
index 0000000000..e9f4f68df8
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
@@ -0,0 +1,64 @@
+<template>
+<div class="_gaps">
+ <MkInfo>{{ i18n.ts._initialAccountSetting.theseSettingsCanEditLater }}</MkInfo>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template>
+ <template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template>
+
+ <MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts.hideOnlineStatus }}</template>
+ <template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template>
+
+ <MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts.noCrawle }}</template>
+ <template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template>
+
+ <MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts.preventAiLearning }}</template>
+ <template #suffix>{{ preventAiLearning ? i18n.ts.on : i18n.ts.off }}</template>
+
+ <MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>
+ </MkFolder>
+
+ <MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from 'vue';
+import { instance } from '@/instance';
+import { i18n } from '@/i18n';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import * as os from '@/os';
+import { $i } from '@/account';
+
+let isLocked = ref(false);
+let hideOnlineStatus = ref(false);
+let noCrawle = ref(false);
+let preventAiLearning = ref(true);
+
+watch([isLocked, hideOnlineStatus, noCrawle, preventAiLearning], () => {
+ os.api('i/update', {
+ isLocked: !!isLocked.value,
+ hideOnlineStatus: !!hideOnlineStatus.value,
+ noCrawle: !!noCrawle.value,
+ preventAiLearning: !!preventAiLearning.value,
+ });
+});
+</script>
+
+<style lang="scss" module>
+
+</style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index adb8d43349..f26ea11214 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -37,10 +37,6 @@ import { chooseFileFromPc } from '@/scripts/select-file';
import * as os from '@/os';
import { $i } from '@/account';
-const emit = defineEmits<{
- (ev: 'done'): void;
-}>();
-
const name = ref('');
const description = ref('');
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index 096b88c309..4e80a5c0fb 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -7,9 +7,17 @@
@close="close(true)"
@closed="emit('closed')"
>
- <template #header>{{ i18n.ts.initialAccountSetting }}</template>
+ <template v-if="page === 1" #header>{{ i18n.ts._initialAccountSetting.profileSetting }}</template>
+ <template v-else-if="page === 2" #header>{{ i18n.ts._initialAccountSetting.privacySetting }}</template>
+ <template v-else-if="page === 3" #header>{{ i18n.ts.follow }}</template>
+ <template v-else-if="page === 4" #header>{{ i18n.ts.pushNotification }}</template>
+ <template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template>
+ <template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
<div style="overflow-x: clip;">
+ <div :class="$style.progressBar">
+ <div :class="$style.progressBarValue" :style="{ width: `${(page / 5) * 100}%` }"></div>
+ </div>
<Transition
mode="out-in"
:enter-active-class="$style.transition_x_enterActive"
@@ -40,12 +48,22 @@
<template v-else-if="page === 2">
<div style="height: 100cqh; overflow: auto;">
<MkSpacer :margin-min="20" :margin-max="28">
- <XFollow/>
+ <XPrivacy/>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 3">
+ <div style="height: 100cqh; overflow: auto;">
+ <MkSpacer :margin-min="20" :margin-max="28">
+ <XFollow/>
+ </MkSpacer>
+ <div :class="$style.pageFooter">
+ <MkButton primary rounded gradate style="margin: 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </div>
+ </div>
+ </template>
+ <template v-else-if="page === 4">
<div :class="$style.centerPage">
<MkSpacer :margin-min="20" :margin-max="28">
<div class="_gaps" style="text-align: center;">
@@ -58,7 +76,7 @@
</MkSpacer>
</div>
</template>
- <template v-else-if="page === 4">
+ <template v-else-if="page === 5">
<div :class="$style.centerPage">
<MkSpacer :margin-min="20" :margin-max="28">
<div class="_gaps" style="text-align: center;">
@@ -87,6 +105,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
+import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
import { host } from '@/config';
@@ -134,6 +153,21 @@ async function close(skip: boolean) {
transform: translateX(-50px);
}
+.progressBar {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 10;
+ width: 100%;
+ height: 4px;
+}
+
+.progressBarValue {
+ height: 100%;
+ background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ transition: all 0.5s cubic-bezier(0,.5,.5,1);
+}
+
.centerPage {
display: flex;
justify-content: center;
@@ -142,4 +176,14 @@ async function close(skip: boolean) {
padding-bottom: 30px;
box-sizing: border-box;
}
+
+.pageFooter {
+ position: sticky;
+ 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));
+}
</style>
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index 33e594acd8..ad1c02a488 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -32,6 +32,7 @@
<component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
</div>
</template>
+
<script lang="ts">
export type Widget = {
name: string;
@@ -42,6 +43,7 @@ export type DefaultStoredWidget = {
place: string | null;
} & Widget;
</script>
+
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import { v4 as uuid } from 'uuid';
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index ad36dcabe4..42abdcbdcc 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -2,7 +2,7 @@
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
<img :class="$style.inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
- <div v-if="user.isCat" :class="[$style.ears, { [$style.mask]: useBlurEffect }]">
+ <div v-if="user.isCat" :class="[$style.ears]">
<div :class="$style.earLeft">
<div v-if="false" :class="$style.layer">
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
@@ -154,24 +154,6 @@ watch(() => props.user.avatarBlurhash, () => {
padding: 50%;
pointer-events: none;
- &.mask {
- -webkit-mask:
- url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') center / 50% 50%,
- linear-gradient(#fff, #fff);
- -webkit-mask-composite: destination-out, source-over;
- mask:
- url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') exclude center / 50% 50%,
- linear-gradient(#fff, #fff); // polyfill of `image(#fff)`
-
- > .earLeft {
- animation: eartightleft 6s infinite;
- }
-
- > .earRight {
- animation: eartightright 6s infinite;
- }
- }
-
> .earLeft,
> .earRight {
contain: strict;
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
index 0237644d29..6ea81d257f 100644
--- a/packages/frontend/src/components/page/page.image.vue
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -1,6 +1,6 @@
<template>
-<div class="lzyxtsnt">
- <ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/>
+<div>
+ <ImgWithBlurhash v-if="image" style="max-width: 100%;" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
</div>
</template>
@@ -17,11 +17,3 @@ const props = defineProps<{
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
</script>
-
-<style lang="scss" scoped>
-.lzyxtsnt {
- > img {
- max-width: 100%;
- }
-}
-</style>