mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2025-03-04 07:18:50 -07:00
Bad UX when a post is muted and it just says "Some chick said something". Now provide some context too to help people decide if they want to view something potentially triggering.
730 lines
17 KiB
Vue
730 lines
17 KiB
Vue
<template>
|
|
<div
|
|
v-if="!muted.muted"
|
|
v-show="!isDeleted"
|
|
ref="el"
|
|
v-hotkey="keymap"
|
|
v-size="{ max: [500, 450, 350, 300] }"
|
|
class="lxwezrsl _block"
|
|
:tabindex="!isDeleted ? '-1' : null"
|
|
:class="{ renote: isRenote }"
|
|
>
|
|
<MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
|
|
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
|
|
<div v-if="isRenote" class="renote">
|
|
<MkAvatar class="avatar" :user="note.user"/>
|
|
<i class="ph-repeat ph-bold ph-lg"></i>
|
|
<I18n :src="i18n.ts.renotedBy" tag="span">
|
|
<template #user>
|
|
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
|
|
<MkUserName :user="note.user"/>
|
|
</MkA>
|
|
</template>
|
|
</I18n>
|
|
<div class="info">
|
|
<button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
|
|
<i v-if="isMyRenote" class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"></i>
|
|
<MkTime :time="note.createdAt"/>
|
|
</button>
|
|
<MkVisibility :note="note"/>
|
|
</div>
|
|
</div>
|
|
<article ref="noteEl" class="article" @contextmenu.stop="onContextmenu" tabindex="-1">
|
|
<header class="header">
|
|
<MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/>
|
|
<div class="body">
|
|
<div class="top">
|
|
<MkA v-user-preview="appearNote.user.id" class="name" :to="userPage(appearNote.user)">
|
|
<MkUserName :user="appearNote.user"/>
|
|
</MkA>
|
|
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
|
<div class="info">
|
|
<MkVisibility :note="appearNote"/>
|
|
</div>
|
|
</div>
|
|
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
|
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
|
|
</div>
|
|
</header>
|
|
<div class="main">
|
|
<div class="body">
|
|
<div v-if="appearNote.cw != null" class="cw">
|
|
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
|
<br/>
|
|
<XCwButton v-model="showContent" :note="appearNote"/>
|
|
</div>
|
|
<div v-show="appearNote.cw == null || showContent" class="content">
|
|
<div class="text">
|
|
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
|
<div v-if="translating || translation" class="translation">
|
|
<MkLoading v-if="translating" mini/>
|
|
<div v-else class="translated">
|
|
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
|
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="appearNote.files.length > 0" class="files">
|
|
<XMediaList :media-list="appearNote.files"/>
|
|
</div>
|
|
<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
|
|
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote" @click.stop="router.push(notePage(appearNote.renote))"/></div>
|
|
</div>
|
|
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
|
</div>
|
|
<footer class="footer">
|
|
<div class="info">
|
|
<MkA class="created-at" :to="notePage(appearNote)">
|
|
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
|
</MkA>
|
|
</div>
|
|
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
|
<button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
|
|
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left ph-bold ph-lg"></i></template>
|
|
<template v-else><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i></template>
|
|
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
|
</button>
|
|
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
|
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
|
<button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
|
|
<i class="ph-smiley ph-bold ph-lg"></i>
|
|
</button>
|
|
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
|
<i class="ph-minus ph-bold ph-lg"></i>
|
|
</button>
|
|
<XQuoteButton class="button" :note="appearNote"/>
|
|
<button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
|
|
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</article>
|
|
<MkNoteSub v-for="note in directReplies" :key="note.id" :note="note" class="reply" :conversation="replies"/>
|
|
</div>
|
|
<div v-else class="_panel muted" @click="muted.muted = false">
|
|
<I18n :src="i18n.ts.userSaysSomethingReason" tag="small">
|
|
<template #name>
|
|
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
|
|
<MkUserName :user="appearNote.user"/>
|
|
</MkA>
|
|
</template>
|
|
<template #reason>
|
|
<b>{{ muted.matched.join(", ") }}</b>
|
|
</template>
|
|
</I18n>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, inject, onMounted, onUnmounted, onUpdated, reactive, ref } from 'vue';
|
|
import * as mfm from 'mfm-js';
|
|
import type * as misskey from 'calckey-js';
|
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
|
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
|
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
|
import XMediaList from '@/components/MkMediaList.vue';
|
|
import XCwButton from '@/components/MkCwButton.vue';
|
|
import XPoll from '@/components/MkPoll.vue';
|
|
import XStarButton from '@/components/MkStarButton.vue';
|
|
import XRenoteButton from '@/components/MkRenoteButton.vue';
|
|
import XQuoteButton from '@/components/MkQuoteButton.vue';
|
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
|
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
|
import MkVisibility from '@/components/MkVisibility.vue';
|
|
import { pleaseLogin } from '@/scripts/please-login';
|
|
import { getWordMute } from '@/scripts/check-word-mute';
|
|
import { userPage } from '@/filters/user';
|
|
import { notePage } from '@/filters/note';
|
|
import { useRouter } from '@/router';
|
|
import * as os from '@/os';
|
|
import { defaultStore, noteViewInterruptors } from '@/store';
|
|
import { reactionPicker } from '@/scripts/reaction-picker';
|
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
|
import { $i } from '@/account';
|
|
import { i18n } from '@/i18n';
|
|
import { getNoteMenu } from '@/scripts/get-note-menu';
|
|
import { useNoteCapture } from '@/scripts/use-note-capture';
|
|
import { deepClone } from '@/scripts/clone';
|
|
import { stream } from '@/stream';
|
|
import { NoteUpdatedEvent } from 'calckey-js/built/streaming.types';
|
|
|
|
const router = useRouter();
|
|
|
|
const props = defineProps<{
|
|
note: misskey.entities.Note;
|
|
pinned?: boolean;
|
|
}>();
|
|
|
|
const inChannel = inject('inChannel', null);
|
|
|
|
let note = $ref(deepClone(props.note));
|
|
|
|
// plugin
|
|
if (noteViewInterruptors.length > 0) {
|
|
onMounted(async () => {
|
|
let result = deepClone(note);
|
|
for (const interruptor of noteViewInterruptors) {
|
|
result = await interruptor.handler(result);
|
|
}
|
|
note = result;
|
|
});
|
|
}
|
|
|
|
const isRenote = (
|
|
note.renote != null &&
|
|
note.text == null &&
|
|
note.fileIds.length === 0 &&
|
|
note.poll == null
|
|
);
|
|
|
|
const el = ref<HTMLElement>();
|
|
const noteEl = $ref();
|
|
const menuButton = ref<HTMLElement>();
|
|
const starButton = ref<InstanceType<typeof XStarButton>>();
|
|
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
|
const renoteTime = ref<HTMLElement>();
|
|
const reactButton = ref<HTMLElement>();
|
|
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
|
const isMyRenote = $i && ($i.id === note.userId);
|
|
const showContent = ref(false);
|
|
const isDeleted = ref(false);
|
|
const muted = ref(getWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
|
const translation = ref(null);
|
|
const translating = ref(false);
|
|
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
|
const conversation = ref<misskey.entities.Note[]>([]);
|
|
const replies = ref<misskey.entities.Note[]>([]);
|
|
const directReplies = ref<misskey.entities.Note[]>([]);
|
|
let isScrolling;
|
|
|
|
|
|
const keymap = {
|
|
'r': () => reply(true),
|
|
'e|a|plus': () => react(true),
|
|
'q': () => renoteButton.value.renote(true),
|
|
'esc': blur,
|
|
'm|o': () => menu(true),
|
|
's': () => showContent.value !== showContent.value,
|
|
};
|
|
|
|
useNoteCapture({
|
|
rootEl: el,
|
|
note: $$(appearNote),
|
|
isDeletedRef: isDeleted,
|
|
});
|
|
|
|
function reply(viaKeyboard = false): void {
|
|
pleaseLogin();
|
|
os.post({
|
|
reply: appearNote,
|
|
animation: !viaKeyboard,
|
|
}, () => {
|
|
focus();
|
|
});
|
|
}
|
|
|
|
function react(viaKeyboard = false): void {
|
|
pleaseLogin();
|
|
blur();
|
|
reactionPicker.show(reactButton.value, reaction => {
|
|
os.api('notes/reactions/create', {
|
|
noteId: appearNote.id,
|
|
reaction: reaction,
|
|
});
|
|
}, () => {
|
|
focus();
|
|
});
|
|
}
|
|
|
|
function undoReact(note): void {
|
|
const oldReaction = note.myReaction;
|
|
if (!oldReaction) return;
|
|
os.api('notes/reactions/delete', {
|
|
noteId: note.id,
|
|
});
|
|
}
|
|
|
|
function onContextmenu(ev: MouseEvent): void {
|
|
const isLink = (el: HTMLElement) => {
|
|
if (el.tagName === 'A') return true;
|
|
if (el.parentElement) {
|
|
return isLink(el.parentElement);
|
|
}
|
|
};
|
|
if (isLink(ev.target)) return;
|
|
if (window.getSelection().toString() !== '') return;
|
|
|
|
if (defaultStore.state.useReactionPickerForContextMenu) {
|
|
ev.preventDefault();
|
|
react();
|
|
} else {
|
|
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), ev).then(focus);
|
|
}
|
|
}
|
|
|
|
function menu(viaKeyboard = false): void {
|
|
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
|
|
viaKeyboard,
|
|
}).then(focus);
|
|
}
|
|
|
|
function showRenoteMenu(viaKeyboard = false): void {
|
|
if (!isMyRenote) return;
|
|
os.popupMenu([{
|
|
text: i18n.ts.unrenote,
|
|
icon: 'ph-trash ph-bold ph-lg',
|
|
danger: true,
|
|
action: () => {
|
|
os.api('notes/delete', {
|
|
noteId: note.id,
|
|
});
|
|
isDeleted.value = true;
|
|
},
|
|
}], renoteTime.value, {
|
|
viaKeyboard: viaKeyboard,
|
|
});
|
|
}
|
|
|
|
function focus() {
|
|
noteEl.focus();
|
|
}
|
|
|
|
function blur() {
|
|
noteEl.blur();
|
|
}
|
|
|
|
os.api('notes/children', {
|
|
noteId: appearNote.id,
|
|
limit: 30,
|
|
depth: 12,
|
|
}).then(res => {
|
|
replies.value = res;
|
|
directReplies.value = res.filter(note => note.replyId === appearNote.id || note.renoteId === appearNote.id).reverse();
|
|
});
|
|
|
|
if (appearNote.replyId) {
|
|
os.api('notes/conversation', {
|
|
noteId: appearNote.replyId,
|
|
limit: 30,
|
|
}).then(res => {
|
|
conversation.value = res.reverse();
|
|
focus();
|
|
});
|
|
}
|
|
|
|
function onNoteReplied(noteData: NoteUpdatedEvent): void {
|
|
const { type, id, body } = noteData;
|
|
if (type === 'replied' && id === appearNote.id) {
|
|
const { id: createdId } = body;
|
|
|
|
os.api('notes/show', {
|
|
noteId: createdId,
|
|
}).then(note => {
|
|
if (note.replyId === appearNote.id) {
|
|
replies.value.unshift(note);
|
|
directReplies.value.unshift(note);
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
document.addEventListener("wheel", () => {
|
|
isScrolling = true;
|
|
})
|
|
|
|
onMounted(() => {
|
|
stream.on("noteUpdated", onNoteReplied);
|
|
isScrolling = false;
|
|
noteEl.scrollIntoView();
|
|
});
|
|
|
|
onUpdated(() => {
|
|
if (!isScrolling) {
|
|
noteEl.scrollIntoView()
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
stream.off("noteUpdated", onNoteReplied);
|
|
});
|
|
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.lxwezrsl {
|
|
font-size: 1.05em;
|
|
position: relative;
|
|
transition: box-shadow 0.1s ease;
|
|
contain: content;
|
|
|
|
&:focus-visible {
|
|
outline: none;
|
|
|
|
&:after {
|
|
content: "";
|
|
pointer-events: none;
|
|
display: block;
|
|
position: absolute;
|
|
z-index: 10;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
margin: auto;
|
|
width: calc(100% - 8px);
|
|
height: calc(100% - 8px);
|
|
border: solid 1px var(--focus);
|
|
border-radius: var(--radius);
|
|
box-sizing: border-box;
|
|
}
|
|
}
|
|
|
|
&:hover > .article > .main > .footer > .button {
|
|
opacity: 1;
|
|
}
|
|
> .reply-to {
|
|
margin-bottom: -16px;
|
|
}
|
|
|
|
> .reply-to-more {
|
|
// opacity: 0.7;
|
|
cursor: pointer;
|
|
|
|
@media (pointer: coarse) {
|
|
cursor: default;
|
|
}
|
|
}
|
|
|
|
> .renote {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 16px 32px 8px 32px;
|
|
line-height: 28px;
|
|
white-space: pre;
|
|
color: var(--renote);
|
|
|
|
> .avatar {
|
|
flex-shrink: 0;
|
|
display: inline-block;
|
|
width: 28px;
|
|
height: 28px;
|
|
margin: 0 8px 0 0;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
> i {
|
|
margin-right: 4px;
|
|
}
|
|
|
|
> span {
|
|
overflow: hidden;
|
|
flex-shrink: 1;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
|
|
> .name {
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
|
|
> .info {
|
|
margin-left: auto;
|
|
font-size: 0.9em;
|
|
|
|
> .time {
|
|
flex-shrink: 0;
|
|
color: inherit;
|
|
|
|
> .dropdownIcon {
|
|
margin-right: 4px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
> .renote + .article {
|
|
padding-top: 8px;
|
|
}
|
|
|
|
> .article {
|
|
padding: 32px;
|
|
padding-bottom: 6px;
|
|
&:last-child {
|
|
padding-bottom: 24px;
|
|
}
|
|
font-size: 1.1em;
|
|
overflow: clip;
|
|
outline: none;
|
|
scroll-margin-top: calc(var(--stickyTop) + 20vh);
|
|
> .header {
|
|
display: flex;
|
|
position: relative;
|
|
margin-bottom: 16px;
|
|
|
|
> .avatar {
|
|
display: block;
|
|
flex-shrink: 0;
|
|
width: var(--avatarSize);
|
|
height: var(--avatarSize);
|
|
}
|
|
|
|
> .body {
|
|
width: 0;
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
padding-left: 14px;
|
|
font-size: 0.95em;
|
|
|
|
> .top {
|
|
display: flex;
|
|
align-items: center;
|
|
> .name {
|
|
font-weight: bold;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
> .is-bot {
|
|
flex-shrink: 0;
|
|
align-self: center;
|
|
margin: 0 0.5em;
|
|
padding: 4px 6px;
|
|
font-size: 80%;
|
|
border: solid 0.5px var(--divider);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
> .info {
|
|
float: right;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
> .main {
|
|
> .body {
|
|
> .cw {
|
|
cursor: default;
|
|
display: block;
|
|
margin: 0;
|
|
padding: 0;
|
|
overflow-wrap: break-word;
|
|
|
|
> .text {
|
|
margin-right: 8px;
|
|
}
|
|
}
|
|
|
|
> .content {
|
|
> .text {
|
|
overflow-wrap: break-word;
|
|
|
|
> .reply {
|
|
color: var(--accent);
|
|
margin-right: 0.5em;
|
|
}
|
|
|
|
> .rp {
|
|
margin-left: 4px;
|
|
font-style: oblique;
|
|
color: var(--renote);
|
|
}
|
|
|
|
> .translation {
|
|
border: solid 0.5px var(--divider);
|
|
border-radius: var(--radius);
|
|
padding: 12px;
|
|
margin-top: 8px;
|
|
}
|
|
}
|
|
|
|
> .url-preview {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
> .poll {
|
|
font-size: 80%;
|
|
}
|
|
|
|
> .renote {
|
|
padding: 8px 0;
|
|
|
|
> * {
|
|
padding: 16px;
|
|
border: solid 1px var(--renote);
|
|
border-radius: 8px;
|
|
transition: background .2s;
|
|
&:hover, &:focus-within {
|
|
background-color: var(--panelHighlight);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
> .channel {
|
|
opacity: 0.7;
|
|
font-size: 80%;
|
|
}
|
|
}
|
|
|
|
> .footer {
|
|
> .info {
|
|
margin: 16px 0;
|
|
opacity: 0.7;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
> .button {
|
|
margin: 0;
|
|
padding: 8px;
|
|
opacity: 0.7;
|
|
|
|
&:not(:last-child) {
|
|
margin-right: 16px;
|
|
}
|
|
|
|
&:hover {
|
|
color: var(--fgHighlighted);
|
|
}
|
|
|
|
> .count {
|
|
display: inline;
|
|
margin: 0 0 0 8px;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
&.reacted {
|
|
color: var(--accent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
> .reply {
|
|
border-top: solid 0.5px var(--divider);
|
|
cursor: pointer;
|
|
padding-top: 24px;
|
|
padding-bottom: 10px;
|
|
@media (pointer: coarse) {
|
|
cursor: default;
|
|
}
|
|
}
|
|
|
|
// Hover
|
|
.reply :deep(.main), .reply-to, .reply-to-more, :deep(.more) {
|
|
position: relative;
|
|
&::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: -12px -24px;
|
|
bottom: -0px;
|
|
background: var(--panelHighlight);
|
|
border-radius: var(--radius);
|
|
opacity: 0;
|
|
transition: opacity .2s;
|
|
z-index: -1;
|
|
}
|
|
&.reply-to, &.reply-to-more {
|
|
&::before {
|
|
inset: 0px 8px;
|
|
}
|
|
&:first-of-type::before {
|
|
top: 12px;
|
|
}
|
|
}
|
|
// &::after {
|
|
// content: "";
|
|
// position: absolute;
|
|
// inset: -9999px;
|
|
// background: var(--modalBg);
|
|
// opacity: 0;
|
|
// z-index: -2;
|
|
// pointer-events: none;
|
|
// transition: opacity .2s;
|
|
// }
|
|
&.more::before {
|
|
inset: 0 !important;
|
|
}
|
|
&:hover, &:focus-within {
|
|
&::before {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
// @media (pointer: coarse) {
|
|
// &:has(.button:focus-within) {
|
|
// z-index: 2;
|
|
// --X13: transparent;
|
|
// &::after {
|
|
// opacity: 1;
|
|
// backdrop-filter: var(--modalBgFilter);
|
|
// }
|
|
// }
|
|
// }
|
|
}
|
|
|
|
|
|
&.max-width_500px {
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
&.max-width_450px {
|
|
|
|
> .reply-to-more:first-child {
|
|
padding-top: 14px;
|
|
}
|
|
> .renote {
|
|
padding: 8px 16px 0 16px;
|
|
}
|
|
|
|
> .article {
|
|
padding: 16px;
|
|
> .header > .body {
|
|
padding-left: 10px;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.max-width_350px {
|
|
> .article {
|
|
> .main {
|
|
> .footer {
|
|
> .button {
|
|
&:not(:last-child) {
|
|
margin-right: 18px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
&.max-width_300px {
|
|
font-size: 0.825em;
|
|
|
|
> .article {
|
|
|
|
> .main {
|
|
> .footer {
|
|
> .button {
|
|
&:not(:last-child) {
|
|
margin-right: 12px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.muted {
|
|
padding: 8px;
|
|
text-align: center;
|
|
opacity: 0.7;
|
|
}
|
|
</style>
|