Page MenuHomePhorge

No OneTemporary

Size
80 KB
Referenced Files
None
Subscribers
None
diff --git a/src/App.scss b/src/App.scss
index aeae45a69b..d0a4062a6f 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1,1112 +1,1112 @@
@use "panel";
@import '@fortawesome/fontawesome-svg-core/styles.css';
@import '@kazvmoe-infra/pinch-zoom-element/dist/pinch-zoom.css';
// stylelint-disable rscss/class-format
/* stylelint-disable no-descending-specificity */
:root {
--status-margin: 0.75em;
--post-line-height: 1.4;
// Z-Index stuff
--ZI_media_modal: 9000;
--ZI_modals_popovers: 8500;
--ZI_modals: 8000;
--ZI_navbar_popovers: 7500;
--ZI_navbar: 7000;
--ZI_popovers: 6000;
// Fallback for when stuff is loading
--background: var(--bg);
}
html {
font-size: var(--textSize, 1rem);
--navbar-height: var(--navbarSize, 3.5rem);
--emoji-size: var(--emojiSize, 32px);
--panel-header-height: var(--panelHeaderSize, 3.2rem);
// overflow-x: clip causes my browser's tab to crash with SIGILL lul
}
body {
font-family: sans-serif;
font-family: var(--font);
margin: 0;
padding: 0;
color: var(--text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overscroll-behavior-y: none;
overflow: clip scroll;
&.hidden {
display: none;
}
}
// ## Custom scrollbars
// Only show custom scrollbars on devices which
// have a cursor/pointer to operate them
@media (any-pointer: fine) {
* {
scrollbar-color: var(--icon) transparent;
&::-webkit-scrollbar {
background: transparent;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-resizer {
/* stylelint-disable-next-line declaration-no-important */
background-color: transparent !important;
background-image:
linear-gradient(
135deg,
transparent calc(50% - 1px),
var(--textFaint) 50%,
transparent calc(50% + 1px),
transparent calc(75% - 1px),
var(--textFaint) 75%,
transparent calc(75% + 1px),
);
}
&::-webkit-scrollbar-button,
&::-webkit-scrollbar-thumb {
box-shadow: var(--shadow);
border-radius: var(--roundness);
}
// horizontal/vertical/increment/decrement are webkit-specific stuff
// that indicates whether we're affecting vertical scrollbar, increase button etc
// stylelint-disable selector-pseudo-class-no-unknown
&::-webkit-scrollbar-button {
--___bgPadding: 2px;
color: var(--text);
background-repeat: no-repeat, no-repeat;
&:horizontal {
background-size: 50% calc(50% - var(--___bgPadding)), 50% calc(50% - var(--___bgPadding));
&:increment {
background-image:
linear-gradient(45deg, var(--text) 50%, transparent 51%),
linear-gradient(-45deg, transparent 50%, var(--text) 51%);
background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding);
}
&:decrement {
background-image:
linear-gradient(45deg, transparent 50%, var(--text) calc(50% + 1px)),
linear-gradient(-45deg, var(--text) 50%, transparent 51%);
background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding);
}
}
&:vertical {
background-size: calc(50% - var(--___bgPadding)) 50%, calc(50% - var(--___bgPadding)) 50%;
&:increment {
background-image:
linear-gradient(-45deg, transparent 50%, var(--text) 51%),
linear-gradient(45deg, transparent 50%, var(--text) 51%);
background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%;
}
&:decrement {
background-image:
linear-gradient(-45deg, var(--text) 50%, transparent 51%),
linear-gradient(45deg, var(--text) 50%, transparent 51%);
background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%;
}
}
}
// stylelint-enable selector-pseudo-class-no-unknown
}
// Body should have background to scrollbar otherwise it will use white (body color?)
html {
scrollbar-color: var(--icon) var(--wallpaper);
background: var(--wallpaper);
}
}
a {
text-decoration: none;
color: var(--link);
&:focus,
&:hover {
text-decoration: underline;
}
- &:focus-visible {
+ &:not(.button-unstyled):focus-visible {
border-radius: var(--roundness);
outline: 2px solid var(--link);
}
}
.underline-links a {
text-decoration: underline;
}
h4 {
margin: 0;
}
code {
background: var(--background);
border: 1px solid var(--border);
border-radius: var(--roundness);
padding: 0 0.2em;
& pre,
pre & {
display: block;
overflow-x: auto;
padding: 0.2em;
}
&.pre {
white-space: pre;
display: block;
}
}
.iconLetter {
display: inline-block;
text-align: center;
font-weight: 1000;
}
i[class*="icon-"],
.svg-inline--fa,
.iconLetter {
color: var(--icon);
}
header {
z-index: var(--ZI_navbar);
box-shadow: var(--shadow);
box-sizing: border-box;
height: var(--navbar-height);
font-size: calc(var(--navbar-height) / 3.5);
position: fixed;
}
#sidebar {
grid-area: sidebar;
}
#modal {
position: absolute;
z-index: var(--ZI_modals);
}
.column.-scrollable {
top: var(--navbar-height);
position: sticky;
}
#main-scroller {
grid-area: content;
position: relative;
}
#notifs-column {
grid-area: notifs;
}
.app-bg-wrapper {
position: fixed;
height: 100%;
top: var(--navbar-height);
z-index: -1000;
left: 0;
right: -20px;
background-size: cover;
background-repeat: no-repeat;
background-color: var(--wallpaper);
background-image: var(--body-background-image);
background-position: 50%;
transition: background-image 1s;
}
.underlay {
grid-column: 1 / span 3;
grid-row: 1 / 1;
pointer-events: none;
background-color: var(--underlay);
z-index: -1000;
}
.app-layout {
--miniColumn: 25rem;
--maxiColumn: 45rem;
--columnGap: 1rem;
--effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)));
--effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn)));
--effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn)));
position: relative;
display: grid;
grid-template-columns:
var(--effectiveSidebarColumnWidth)
var(--effectiveContentColumnWidth);
grid-template-areas: "sidebar content";
grid-template-rows: 1fr;
box-sizing: border-box;
margin: 0 auto;
place-content: flex-start center;
flex-wrap: wrap;
min-height: 100vh;
overflow-x: clip;
.column {
--___columnMargin: var(--columnGap);
display: grid;
grid-template-columns: 100%;
box-sizing: border-box;
grid-row: 1 / 1;
margin: 0 calc(var(--___columnMargin) / 2);
padding: calc(var(--___columnMargin)) 0;
row-gap: var(--___columnMargin);
align-content: start;
&:not(.-scrollable) {
margin-top: var(--navbar-height);
}
&:hover {
z-index: 2;
}
&.-full-height {
margin-bottom: 0;
padding-top: 0;
padding-bottom: 0;
}
&.-scrollable {
--___paddingIncrease: calc(var(--columnGap) / 2);
position: sticky;
top: var(--navbar-height);
max-height: calc(100vh - var(--navbar-height));
overflow: hidden auto;
margin-left: calc(var(--___paddingIncrease) * -1);
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
// On browsers that don't support hiding scrollbars we enforce "show scrolbars" mode
// might implement old style of hiding scrollbars later if there's demand
@supports (scrollbar-width: none) or (-webkit-text-fill-color: initial) {
&:not(.-show-scrollbar) {
scrollbar-width: none;
margin-right: calc(var(--___paddingIncrease) * -1);
padding-right: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
&::-webkit-scrollbar {
display: block;
width: 0;
}
}
}
.panel-heading.-sticky {
top: calc(var(--columnGap) / -1);
}
}
}
&.-has-new-post-button {
.column {
padding-bottom: 10rem;
}
}
&.-no-sticky-headers {
.column {
.panel-heading.-sticky {
position: relative;
top: 0;
}
}
}
.column-inner {
display: grid;
grid-template-columns: 100%;
box-sizing: border-box;
row-gap: 1em;
align-content: start;
}
&.-reverse:not(.-wide, .-mobile) {
grid-template-columns:
var(--effectiveContentColumnWidth)
var(--effectiveSidebarColumnWidth);
grid-template-areas: "content sidebar";
}
&.-wide {
grid-template-columns:
var(--effectiveSidebarColumnWidth)
var(--effectiveContentColumnWidth)
var(--effectiveNotifsColumnWidth);
grid-template-areas: "sidebar content notifs";
&.-reverse {
grid-template-columns:
var(--effectiveNotifsColumnWidth)
var(--effectiveContentColumnWidth)
var(--effectiveSidebarColumnWidth);
grid-template-areas: "notifs content sidebar";
}
}
&.-mobile {
grid-template-columns: 100vw;
grid-template-areas: "content";
padding: 0;
.column {
padding-top: 0;
margin: var(--navbar-height) 0 0 0;
}
.panel-heading,
.panel-heading::after,
.panel-heading::before,
.panel,
.panel::after {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
#sidebar,
#notifs-column {
display: none;
}
}
&.-normal {
#notifs-column {
display: none;
}
}
}
.text-center {
text-align: center;
}
.button-default {
user-select: none;
color: var(--text);
border: none;
cursor: pointer;
background-color: var(--background);
box-shadow: var(--shadow);
font-size: 1em;
font-family: sans-serif;
font-family: var(--font);
&.-transparent {
backdrop-filter: blur(0.125em) contrast(60%);
}
&::-moz-focus-inner {
border: none;
}
&:disabled {
cursor: not-allowed;
}
&:active {
transform: translate(1px, 1px);
}
}
.menu-item {
line-height: var(--__line-height);
font-family: inherit;
font-weight: 400;
font-size: 100%;
cursor: pointer;
a,
button:not(.button-default) {
color: var(--text);
font-size: 100%;
text-align: initial;
padding: 0;
background: none;
border: none;
outline: none;
display: inline;
font-family: inherit;
line-height: unset;
}
&.disabled {
cursor: not-allowed;
}
}
.menu-item,
.list-item {
display: block;
box-sizing: border-box;
border: none;
outline: none;
text-align: initial;
color: inherit;
clear: both;
position: relative;
white-space: nowrap;
width: 100%;
padding: var(--__vertical-gap) var(--__horizontal-gap);
background: transparent;
--__line-height: 1.5em;
--__horizontal-gap: 0.75em;
--__vertical-gap: 0.5em;
}
.button-unstyled {
border: none;
outline: none;
display: inline;
text-align: initial;
font-size: 100%;
font-family: inherit;
box-shadow: var(--shadow);
background-color: transparent;
padding: 0;
line-height: unset;
cursor: pointer;
box-sizing: content-box;
color: inherit;
&.-link {
/* stylelint-disable-next-line declaration-no-important */
color: var(--link) !important;
}
}
label {
&.-disabled {
color: var(--textFaint);
}
}
input,
textarea {
border: none;
display: inline-block;
outline: none;
}
.input {
&.unstyled {
border-radius: 0;
/* stylelint-disable-next-line declaration-no-important */
background: none !important;
box-shadow: none;
height: unset;
}
&::placeholder {
color: var(--textFaint)
}
--_padding: 0.5em;
border: none;
background-color: var(--background);
color: var(--text);
box-shadow: var(--shadow);
font-family: var(--font);
font-size: 1em;
margin: 0;
box-sizing: border-box;
display: inline-block;
position: relative;
line-height: 2;
hyphens: none;
padding: 0 var(--_padding);
&:disabled,
&[disabled="disabled"],
&.disabled {
cursor: not-allowed;
color: var(--textFaint);
/* stylelint-disable-next-line declaration-no-important */
background-color: transparent !important;
}
&[type="range"] {
background: none;
border: none;
margin: 0;
box-shadow: none;
flex: 1;
}
&[type="radio"] {
display: none;
&:checked + label::before {
box-shadow: var(--shadow);
background-color: var(--background);
color: var(--text);
}
&:disabled {
&,
& + label,
& + label::before {
opacity: 0.5;
}
background-color: var(--background);
}
+ label::before {
flex-shrink: 0;
display: inline-block;
content: "•";
transition: box-shadow 200ms;
width: 1.1em;
height: 1.1em;
border-radius: 100%; // Radio buttons should always be circle
background-color: var(--background);
box-shadow: var(--shadow);
margin-right: 0.5em;
vertical-align: top;
text-align: center;
line-height: 1.1;
font-size: 1.1em;
box-sizing: border-box;
color: transparent;
overflow: hidden;
}
}
&[type="checkbox"] {
&:checked + label::before {
color: var(--text);
background-color: var(--background);
box-shadow: var(--shadow);
}
&:disabled {
&,
& + label,
& + label::before {
opacity: 0.5;
}
}
+ label::before {
flex-shrink: 0;
display: inline-block;
content: "✓";
transition: color 200ms;
width: 1.1em;
height: 1.1em;
border-radius: var(--roundness);
box-shadow: var(--shadow);
margin-right: 0.5em;
vertical-align: top;
text-align: center;
line-height: 1.1;
font-size: 1.1em;
box-sizing: border-box;
color: transparent;
overflow: hidden;
}
}
&.resize-height {
resize: vertical;
}
}
.input,
.button-default {
--_roundness-left: var(--roundness);
--_roundness-right: var(--roundness);
border-top-left-radius: var(--_roundness-left);
border-bottom-left-radius: var(--_roundness-left);
border-top-right-radius: var(--_roundness-right);
border-bottom-right-radius: var(--_roundness-right);
}
// Textareas should have stock line-height + vertical padding instead of huge line-height
textarea.input {
padding: var(--_padding);
line-height: var(--post-line-height);
}
option {
color: var(--text);
background-color: var(--background);
}
.hide-number-spinner {
appearance: textfield;
&[type="number"]::-webkit-inner-spin-button,
&[type="number"]::-webkit-outer-spin-button {
opacity: 0;
display: none;
}
}
.cards-list {
list-style: none;
display: grid;
grid-auto-flow: row dense;
grid-template-columns: repeat(auto-fit, minmax(20em, 1fr));
gap: 0.5em;
li {
border: 1px solid var(--border);
border-radius: var(--roundness);
padding: 0.5em;
margin: 0.25em;
}
}
.btn-group {
position: relative;
display: inline-flex;
vertical-align: middle;
> .button-default {
--_roundness-left: 0;
--_roundness-right: 0;
position: relative;
&:first-child {
--_roundness-left: var(--roundness);
}
&:last-child {
--_roundness-right: var(--roundness);
}
}
}
.fa {
color: grey;
}
.mobile-shown {
display: none;
}
.badge {
box-sizing: border-box;
display: inline-block;
border-radius: 99px;
max-width: 10em;
min-width: 1.7em;
height: 1.3em;
padding: 0.15em;
vertical-align: middle;
font-weight: normal;
font-style: normal;
font-size: 0.9em;
line-height: 1;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.-dot,
&.-counter {
margin: 0;
position: absolute;
}
&.-dot {
min-height: 0.6em;
max-height: 0.6em;
min-width: 0.6em;
max-width: 0.6em;
left: calc(50% + 0.5em);
top: calc(50% - 1em);
line-height: 0;
padding: 0;
margin: 0;
}
&.-counter {
border-radius: var(--roundness);
font-size: 0.75em;
line-height: 1;
text-align: right;
padding: 0.2em;
min-width: 0;
left: calc(50% - 0.5em);
top: calc(50% - 0.4em);
margin-left: 0.7em;
margin-top: -1em;
}
}
.alert {
margin: 0 0.35em;
padding: 0 0.25em;
border-radius: var(--roundness);
border: 1px solid var(--border);
&.-dismissible {
display: flex;
padding-left: 0.5em;
margin: 0;
align-items: baseline;
line-height: 2;
span {
display: block;
flex: 1 0 auto;
}
}
}
.faint {
--text: var(--textFaint);
--link: var(--linkFaint);
color: var(--text);
}
.notice-dismissible {
display: flex;
padding: 0.75em 1em;
align-items: baseline;
line-height: 1.5;
p,
span {
display: block;
flex: 1 1 auto;
margin: 0;
}
.dismiss {
color: inherit;
}
}
.fa-scale-110 {
&.svg-inline--fa,
&.iconLetter {
font-size: 1.1em;
}
&.svg-inline--fa {
vertical-align: -0.15em;
}
}
.fa-old-padding {
&.iconLetter,
&.svg-inline--fa,
&-layer {
padding: 0 0.3em;
}
}
.veryfaint {
opacity: 0.25;
}
.timeago {
--link: var(--text);
--linkFaint: var(--textFaint);
}
.login-hint {
text-align: center;
@media all and (width >= 801px) {
display: none;
}
a {
display: inline-block;
padding: 1em 0;
width: 100%;
}
}
.btn.button-default {
min-height: 2em;
}
.new-status-notification {
position: relative;
font-size: 1.1em;
z-index: 1;
flex: 1;
}
@media all and (width <= 800px) {
.mobile-hidden {
display: none;
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
@keyframes shakeError {
0% {
transform: translateX(0);
}
15% {
transform: translateX(0.375rem);
}
30% {
transform: translateX(-0.375rem);
}
45% {
transform: translateX(0.375rem);
}
60% {
transform: translateX(-0.375rem);
}
75% {
transform: translateX(0.375rem);
}
90% {
transform: translateX(-0.375rem);
}
100% {
transform: translateX(0);
}
}
// Vue transitions
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-active {
opacity: 0;
}
/* stylelint-enable no-descending-specificity */
.visible-for-screenreader-only {
display: block;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
visibility: visible;
/* stylelint-disable property-no-deprecated */
clip: rect(0 0 0 0);
/* stylelint-enable property-no-deprecated */
padding: 0;
position: absolute;
}
*::selection {
color: var(--selectionText);
background-color: var(--selectionBackground);
}
#splash {
pointer-events: none;
#status {
&.css-ok {
&::before {
display: inline-block;
content: "CSS OK";
}
}
.initial-text {
display: none;
}
}
#throbber {
animation-duration: 3s;
animation-name: bounce;
animation-iteration-count: infinite;
animation-direction: normal;
transform-origin: bottom center;
&.dead {
animation-name: dead;
animation-duration: 0.5s;
animation-iteration-count: 1;
transform: rotateX(90deg) rotateY(0) rotateZ(-45deg);
}
@keyframes dead {
0% {
transform: rotateX(0) rotateY(0) rotateZ(0);
}
5% {
transform: rotateX(0) rotateY(0) rotateZ(1deg);
}
10% {
transform: rotateX(0) rotateY(0) rotateZ(-2deg);
}
15% {
transform: rotateX(0) rotateY(0) rotateZ(3deg);
}
20% {
transform: rotateX(0) rotateY(0) rotateZ(0);
}
25% {
transform: rotateX(0) rotateY(0) rotateZ(0);
}
30% {
transform: rotateX(10deg) rotateY(0) rotateZ(0);
}
35% {
transform: rotateX(-10deg) rotateY(0) rotateZ(0);
}
40% {
transform: rotateX(10deg) rotateY(0) rotateZ(0);
}
45% {
transform: rotateX(-10deg) rotateY(0) rotateZ(0);
}
50% {
transform: rotateX(10deg) rotateY(0) rotateZ(0);
}
100% {
transform: rotateX(90deg) rotateY(0) rotateZ(-45deg);
transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); /* easeInQuint */
}
}
@keyframes bounce {
0% {
scale: 1 1;
translate: 0 0;
animation-timing-function: ease-out;
}
10% {
scale: 1.2 0.8;
translate: 0 0;
transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-out;
}
30% {
scale: 0.9 1.1;
translate: 0 -40%;
transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-in;
}
40% {
scale: 1.1 0.9;
translate: 0 -50%;
transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-in;
}
45% {
scale: 0.9 1.1;
translate: 0 -45%;
transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-in;
}
50% {
scale: 1.05 0.95;
translate: 0 -40%;
animation-timing-function: ease-in;
}
55% {
scale: 0.985 1.025;
translate: 0 -35%;
transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-in;
}
60% {
scale: 1.0125 0.9985;
translate: 0 -30%;
transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-in;
}
80% {
scale: 1.0063 0.9938;
translate: 0 -10%;
transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-in-out;
}
90% {
scale: 1.2 0.8;
translate: 0 0;
transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-out;
}
100% {
scale: 1 1;
translate: 0 0;
transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-out;
}
}
}
}
@property --shadow {
syntax: "*";
inherits: false;
}
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 9c3d64909e..b57a2d6950 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -1,165 +1,165 @@
<template>
-<div
- class="AccountActions"
- role="presentation"
->
+ <div
+ class="AccountActions"
+ role="presentation"
+ >
<Popover
trigger="click"
placement="bottom"
remove-padding
:aria-label="$t('user_card.other')"
>
<template #content>
<div role="menu" class="dropdown-menu">
<template v-if="relationship.following">
<div
v-if="relationship.showing_reblogs"
class="menu-item dropdown-item"
role="menuitem"
>
<button
class="main-button"
@click="hideRepeats"
>
{{ $t('user_card.hide_repeats') }}
</button>
</div>
<div
v-if="!relationship.showing_reblogs"
class="menu-item dropdown-item"
role="menuitem"
>
<button
class="main-button"
@click="showRepeats"
>
{{ $t('user_card.show_repeats') }}
</button>
</div>
<hr
role="separator"
class="dropdown-divider"
/>
</template>
<UserListMenu :user="user" />
<div
v-if="relationship.followed_by"
class="menu-item dropdown-item"
role="menuitem"
>
<button
class="main-button"
@click="removeUserFromFollowers"
>
{{ $t('user_card.remove_follower') }}
</button>
</div>
<div
class="menu-item dropdown-item"
role="menuitem"
>
<button
v-if="relationship.blocking"
class="main-button"
@click="unblockUser"
>
{{ $t('user_card.unblock') }}
</button>
<button
v-else
class="main-button"
@click="blockUser"
>
{{ $t('user_card.block') }}
</button>
</div>
<div
class="menu-item dropdown-item"
role="menuitem"
>
<button
class="main-button"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
</div>
<div
v-if="pleromaChatMessagesAvailable"
class="menu-item dropdown-item"
role="menuitem"
>
<button
class="main-button"
@click="openChat"
>
{{ $t('user_card.message') }}
</button>
</div>
</div>
</template>
<template #trigger>
<FAIcon
class="icon"
icon="ellipsis-v"
/>
</template>
</Popover>
<teleport to="#modal">
<ConfirmModal
v-if="showingConfirmBlock && !blockExpiration"
ref="blockDialog"
:title="$t('user_card.block_confirm_title')"
:confirm-text="$t('user_card.block_confirm_accept_button')"
:cancel-text="$t('user_card.block_confirm_cancel_button')"
@accepted="doBlockUser"
@cancelled="hideConfirmBlock"
>
<i18n-t
keypath="user_card.block_confirm"
tag="span"
scope="global"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</ConfirmModal>
</teleport>
<teleport to="#modal">
<ConfirmModal
v-if="showingConfirmRemoveFollower"
:title="$t('user_card.remove_follower_confirm_title')"
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
:cancel-text="$t('user_card.remove_follower_confirm_cancel_button')"
@accepted="doRemoveUserFromFollowers"
@cancelled="hideConfirmRemoveUserFromFollowers"
>
<i18n-t
keypath="user_card.remove_follower_confirm"
tag="span"
scope="global"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</ConfirmModal>
<UserTimedFilterModal
v-if="blockExpiration"
ref="timedBlockDialog"
:is-mute="false"
:user="user"
/>
</teleport>
</div>
</template>
<script src="./account_actions.js"></script>
<style lang="scss">
</style>
diff --git a/src/components/button_unstyled.style.js b/src/components/button_unstyled.style.js
index c35fb8f695..b7ac7afce4 100644
--- a/src/components/button_unstyled.style.js
+++ b/src/components/button_unstyled.style.js
@@ -1,92 +1,93 @@
export default {
name: 'ButtonUnstyled',
selector: '.button-unstyled',
notEditable: true,
transparent: true,
states: {
toggled: '.toggled',
disabled: ':disabled',
hover: ':is(:hover, :focus-visible):not(:disabled)',
focused: ':focus-within:not(:is(:focus-visible))',
},
validInnerComponents: ['Text', 'Link', 'Icon', 'Badge'],
defaultRules: [
{
directives: {
shadow: [],
},
+ roundness: 3,
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['hover'],
},
directives: {
textColor: '--parent--text',
},
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled'],
},
directives: {
textColor: '--parent--text',
},
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled', 'hover'],
},
directives: {
textColor: '--parent--text',
},
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled', 'focused'],
},
directives: {
textColor: '--parent--text',
},
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled', 'focused', 'hover'],
},
directives: {
textColor: '--parent--text',
},
},
{
component: 'Text',
parent: {
component: 'ButtonUnstyled',
state: ['disabled'],
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend',
},
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['disabled'],
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend',
},
},
],
}
diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss
index 9755ac5905..d0dec61ba7 100644
--- a/src/components/desktop_nav/desktop_nav.scss
+++ b/src/components/desktop_nav/desktop_nav.scss
@@ -1,109 +1,116 @@
.DesktopNav {
width: 100%;
z-index: var(--ZI_navbar);
input {
color: var(--inputTopbarText, var(--inputText));
}
.inner-nav {
display: grid;
grid-template-rows: var(--navbar-height);
grid-template-columns: minmax(5em, 1fr) auto minmax(5em, 1fr);
grid-template-areas: "sitename logo actions";
box-sizing: border-box;
padding: 0 1.2em;
margin: auto;
max-width: 980px;
}
&.-column-stretch .inner-nav {
--miniColumn: 25rem;
--maxiColumn: 45rem;
--columnGap: 1em;
max-width:
calc(
var(--sidebarColumnWidth, var(--miniColumn)) +
var(--contentColumnWidth, var(--maxiColumn)) +
var(--columnGap)
);
}
&.-logoLeft .inner-nav {
grid-template-columns: auto minmax(5em, 1fr) minmax(5em, 1fr);
grid-template-areas: "logo sitename actions";
}
&.-column-stretch.-wide .inner-nav {
max-width:
calc(
var(--sidebarColumnWidth, var(--miniColumn)) +
var(--contentColumnWidth, var(--maxiColumn)) +
var(--notifsColumnWidth, var(--miniColumn)) +
var(--columnGap)
);
}
+ .button-unstyled {
+ &:focus-visible {
+ border-radius: var(--roundness);
+ outline: 2px solid var(--text);
+ }
+ }
+
.button-default {
&,
svg {
color: var(--text);
}
}
.logo {
grid-area: logo;
position: relative;
transition: opacity;
transition-timing-function: ease-out;
transition-duration: 100ms;
@media all and (width >= 800px) {
/* stylelint-disable-next-line declaration-no-important */
opacity: 1 !important;
}
.mask {
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
background-color: var(--text);
position: absolute;
inset: 0;
}
img {
display: inline-block;
height: var(--navbar-height);
}
}
.nav-icon {
margin-left: 0.2em;
width: 2em;
height: 100%;
text-align: center;
}
.sitename {
grid-area: sitename;
}
.actions {
grid-area: actions;
justify-content: flex-end;
text-align: right;
z-index: 1;
}
.item {
line-height: var(--navbar-height);
height: var(--navbar-height);
display: flex;
}
.spacer {
min-width: 1em;
}
}
diff --git a/src/components/select/select.js b/src/components/select/select.js
index 70a2fb3adc..2b41231d31 100644
--- a/src/components/select/select.js
+++ b/src/components/select/select.js
@@ -1,15 +1,16 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
library.add(faChevronDown)
export default {
emits: ['update:modelValue'],
props: {
+ rootClass: String,
modelValue: String,
disabled: Boolean,
unstyled: Boolean,
kind: String,
},
inheritAttrs: false,
}
diff --git a/src/components/select/select.vue b/src/components/select/select.vue
index d91cbf3aa1..cf485adb4d 100644
--- a/src/components/select/select.vue
+++ b/src/components/select/select.vue
@@ -1,92 +1,92 @@
<template>
<label
class="Select input"
- :class="{ disabled, unstyled }"
+ :class="[rootClass, { disabled, unstyled }]"
role="presentation"
>
<select
:disabled="disabled"
:value="modelValue"
v-bind="$attrs"
@change="$emit('update:modelValue', $event.target.value)"
>
<slot />
</select>
{{ ' ' }}
<FAIcon
v-if="!$attrs.size && !$attrs.multiple"
class="select-down-icon"
icon="chevron-down"
/>
</label>
</template>
<script src="./select.js"> </script>
<style lang="scss">
/* TODO fix order of styles */
label.Select {
padding: 0;
display: flex;
select {
appearance: none;
background: transparent;
border: none;
color: var(--text);
margin: 0;
padding: 0 2em 0 0.5em;
font-family: var(--font);
font-size: 1em;
width: 100%;
z-index: 1;
line-height: 2;
&[multiple],
&[size] {
height: 100%;
padding: 0.2em;
option {
background-color: transparent;
&:checked,
&.-active {
color: var(--selectionText);
background-color: var(--selectionBackground);
}
}
}
}
&.disabled,
&:disabled {
background-color: var(--background);
opacity: 1; /* override browser */
color: var(--faint);
select {
&[multiple],
&[size] {
option.-active {
color: var(--faint);
background: transparent;
}
}
}
}
.select-down-icon {
position: absolute;
top: 0;
bottom: 0;
right: 0.5em;
height: 100%;
width: 0.875em;
font-family: var(--font);
line-height: 2;
z-index: 1;
pointer-events: none;
}
}
</style>
diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss
index e583daf3b0..f24c4180b4 100644
--- a/src/components/user_card/user_card.scss
+++ b/src/components/user_card/user_card.scss
@@ -1,630 +1,638 @@
.user-card {
+ --user-card-margin: 0.6em;
+
position: relative;
z-index: 1;
- overflow: hidden;
+ overflow: clip;
border-top-left-radius: calc(var(--roundness) - 1px);
border-top-right-radius: calc(var(--roundness) - 1px);
// editing headers
h4 {
line-height: 2;
display: flex;
}
h3 {
- padding-left: 0.6em;
+ padding-left: var(--user-card-margin);
margin-bottom: 0;
.button-default {
font-size: 1rem;
line-height: 2;
- padding: 0 0.6em;
+ padding: 0 var(--user-card-margin);
}
}
summary {
font-weight: bold;
cursor: pointer;
}
details {
margin: 0.5em 0;
&[open] summary {
- margin-bottom: 0.5em;
+ margin-bottom: var(--user-card-margin);
}
ul {
margin: 0;
}
}
.input.bio {
height: auto; // override settings default textarea size
}
.follow-button {
flex: 1 0 auto;
}
.user-card-inner {
padding-bottom: 0;
}
&-setting,
&-bio {
color: var(--lightText);
display: block;
line-height: 1.3;
- padding: 0 0.6em;
+ padding: 0 var(--user-card-margin);
img {
object-fit: contain;
vertical-align: middle;
max-width: 100%;
max-height: 400px;
}
}
.user-card-setting {
- margin-left: 0.6em;
- margin-right: 0.6em;
+ margin-left: var(--user-card-margin);
+ margin-right: var(--user-card-margin);
}
.user-card-bio {
text-align: center;
- margin: 0.6em;
+ margin: var(--user-card-margin);
&.input {
margin: 0 1em;
textarea {
text-align: inherit;
}
}
&, * {
line-height: 1.5;
}
&.-justify-left {
text-align: start;
}
}
&:hover {
--_still-image-img-visibility: visible;
--_still-image-canvas-visibility: hidden;
--_still-image-label-visibility: hidden;
}
.admin-data,
.personal-marks {
- margin: 0 0.6em;
- padding: 0 0.6em;
+ margin: 0 var(--user-card-margin);
+ padding: 0 var(--user-card-margin);
&:not(:last-child) {
border-bottom: 1px dotted var(--border);
}
.highlighter {
margin-bottom: 0.5em;
h4 {
- margin-top: 0.6em;
+ margin-top: var(--user-card-margin);
}
.userHighlightSel {
vertical-align: bottom;
margin-left: -0.1em;
&.-none select {
color: var(--textFaint);
option {
color: var(--text);
background: var(--background);
}
}
}
.highlighter-color {
vertical-align: bottom;
- margin-left: 0.6em;
+ margin-left: var(--user-card-margin);
}
}
}
.header-overlay {
position: absolute;
inset: 0;
- right: -1.2em;
- left: -1.2em;
- top: -1.4em;
mask: linear-gradient(to top, transparent 0, white 5em) bottom no-repeat;
}
.banner-overlay,
.banner-image {
position: absolute;
inset: 0;
padding: 0;
border-top-left-radius: calc(var(--roundness) - 1px);
border-top-right-radius: calc(var(--roundness) - 1px);
}
.banner-image {
z-index: -2;
img {
object-fit: cover;
height: 100%;
width: 100%;
}
}
.banner-overlay {
background-color: var(--profileTint);
opacity: 0.5;
pointer-events: none; // let user copy bg url
z-index: -1;
}
.bottom-buttons {
display: flex;
- gap: 0.5em;
+ gap: var(--user-card-margin);
}
}
.user-info {
position: relative;
text-align: left;
display: flex;
flex-direction: column;
gap: 1em;
.user-identity {
position: relative;
min-height: 6em;
display: flex;
align-items: flex-end;
container: user-card / inline-size;
+ padding: var(--user-card-margin);
+ gap: var(--user-card-margin);
&:not(.-compact) {
aspect-ratio: 3;
}
> * {
min-width: 0;
}
> a {
vertical-align: middle;
display: flex;
}
.Avatar {
--_avatarShadowBox: var(--avatarShadow);
--_avatarShadowFilter: var(--avatarShadowFilter);
--_avatarShadowInset: var(--avatarShadowInset);
width: 7em;
width: calc(min(7em, 20cqw));
height: 7em;
height: calc(min(7em, 20cqw));
object-fit: cover;
}
}
.other-actions {
position: relative;
display: inline-grid;
grid-auto-flow: column;
- gap: 0.6em;
+ gap: var(--user-card-margin);
font-size: 1.25rem;
z-index: 2;
align-self: end;
- a, button, div {
+ a, button {
text-align: center;
width: 2em;
height: 2em;
line-height: 2em;
- padding: 0.6em;
- margin: -0.6em;
+
+ --icon: var(--text);
&.edit-banner-button {
width: auto;
}
- &:hover .icon {
- color: var(--textFaint);
+ &:focus-visible,
+ &:hover {
+ filter:
+ drop-shadow(0 0 0.5em var(--icon))
+ drop-shadow(0 0 0.75em var(--icon));
}
- &:not(:hover) .icon {
- color: var(--lightText);
+ &:focus-visible {
+ border-radius: var(--roundness);
+ outline: 2px solid var(--text);
}
}
}
&-avatar {
position: relative;
cursor: pointer;
- margin-right: 0.6em;
&.-overlay {
position: absolute;
- inset: -0.6em;
- right: -1.2em;
+ inset: -var(--user-card-margin);
display: flex;
justify-content: center;
align-items: center;
border-radius: var(--roundness);
opacity: 0;
transition: opacity 0.2s ease;
svg {
color: #fff;
- margin: 0.5em;
+ margin: 0.25em;
}
}
&:hover &.-overlay {
opacity: 1;
background-color: rgb(0 0 0 / 30%);
}
}
.user-info-avatar.-editable {
.-overlay {
opacity: 1;
place-items: start end;
justify-content: end;
}
}
.user-summary {
align-self: stretch;
display: flex;
flex-direction: column;
- margin-left: 0.6em;
+ margin-left: var(--user-card-margin);
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 0;
// This is so that text doesn't get overlapped by avatar's shadow if it has
// big one
z-index: 1;
line-height: 2em;
filter: drop-shadow(0 0 0.5em var(--profileTint))
drop-shadow(0 0 0.2em var(--profileTint));
.alert {
text-shadow: none;
}
--emoji-size: 1.7em;
.RichContent {
/* stylelint-disable-next-line declaration-no-important */
--link: var(--text) !important;
}
.name-wrapper {
display: flex;
align-items: baseline;
.edit-button {
width: 3em;
text-align: center;
&:hover .icon {
color: var(--textFaint);
}
&:not(:hover) .icon {
color: var(--lightText);
}
}
.input,
.user-name {
flex: 1;
font-weight: 600;
line-height: 2;
margin-right: 1em;
- text-overflow: ellipsis;
- overflow: hidden;
+ text-overflow: clip;
+ overflow: clip;
+ min-width: 3em;
text-decoration: none;
}
+ .user-name {
+ mask: linear-gradient(to left, transparent 0.1em, white 0.5em) right no-repeat;
+ }
+
.user-name:hover {
text-decoration: underline;
}
.input {
margin: 0 -0.5em;
padding: 0 0.5em;
}
}
.top-line {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
// these two normalize position and height when custom emoji are used
line-height: 2;
margin-bottom: -0.2em;
font-size: calc(max(110%, 4cqw));
}
.bottom-line {
display: flex;
align-items: baseline;
flex-wrap: wrap;
white-space: normal;
font-weight: 500;
font-size: 0.9em;
font-size: calc(max(90%, 2.5cqw));
line-height: 1.5;
margin-top: 0.5em;
}
.lock-icon {
display: inline-block;
vertical-align: baseline;
overflow-x: hidden;
margin: 0 0.35em 0 0.5em;
}
.user-screen-name {
- color: var(--text);
text-overflow: ellipsis;
overflow: hidden;
display: inline-block;
}
.user-role {
display: inline-block;
vertical-align: baseline;
}
}
.user-interactions {
position: relative;
display: flex;
flex-wrap: wrap;
- gap: 0.6em;
+ gap: var(--user-card-margin);
+ padding: 0 var(--user-card-margin) 0.6em;
> * {
flex: 0 0 8em;
}
.popover-trigger-button, .moderation-tools-button {
width: 100%;
}
> .btn-group, > button {
white-space: nowrap;
}
}
}
.sidebar .edit-profile-button {
display: none;
}
.user-extras {
- padding: 1.2em 0.6em;
+ padding: 1.2em var(--user-card-margin);
line-height: 1.5;
.user-stats {
display: block;
text-align: center;
overflow-wrap: break-word;
width: 100%;
dl {
display: inline;
margin-right: 1em;
white-space: nowrap;
}
dd {
font-weight: bolder;
text-overflow: ellipsis;
overflow: hidden;
margin: 0;
.svg-inline--fa {
color: var(--text);
}
}
dd, dt {
display: inline
}
/* stylelint-disable-next-line no-descending-specificity */
a {
text-decoration: none;
}
}
.birthday {
width: 100%;
text-align: center;
white-space: nowrap;
--icon: var(--text);
}
}
.user-profile-fields {
margin: 0 0.5em;
padding: 0 0.5em;
display: flex;
flex-direction: column;
--emoji-size: 1.8em;
.user-profile-field-add,
.user-profile-field {
display: flex;
align-items: center;
margin: 0.25em;
border: 1px solid var(--border);
border-radius: var(--roundness);
line-height: 2em;
.label {
margin-left: 0.5em;
}
}
.user-profile-field-add {
justify-content: center;
margin: 0.25em;
}
.user-profile-field {
/* stylelint-disable no-descending-specificity */
// input is a generic class
.input {
text-align: inherit;
flex: 1;
}
/* stylelint-enable no-descending-specificity */
.delete-field {
display: inline-block;
text-align: center;
width: 2em;
}
.user-profile-field-name,
.user-profile-field-value,
.user-profile-field-add {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
display: inline-flex;
&.-edit {
padding: 0;
input {
font-weight: inherit;
}
}
}
.user-profile-field-name {
flex: 0 1 50%;
font-weight: 600;
text-align: right;
justify-content: end;
color: var(--lightText);
min-width: 9em;
border-right: 1px solid var(--border);
padding-left: 1.2em;
- padding-right: 0.6em;
+ padding-right: var(--user-card-margin);
}
.user-profile-field-value {
flex: 1 1 55%;
color: var(--text);
- padding-left: 0.6em;
+ padding-left: var(--user-card-margin);
margin: 0;
}
}
}
.edit-image {
.panel-body {
text-align: center;
display: flex;
flex-direction: column;
}
.current-avatar {
object-fit: contain;
}
.image-container {
display: flex;
align-self: center;
margin: 0 0 1em;
max-height: 30em;
max-width: 100%;
flex: 1 0 20em;
aspect-ratio: 1;
gap: 0.5em;
&.-banner {
aspect-ratio: 3;
max-width: 100%;
}
.new-image {
display: flex;
flex-direction: column;
}
.cropper {
flex: 1;
overflow-x: auto;
}
> * {
flex: 1 0 10em;
max-height: 100%;
aspect-ratio: 1;
}
.separator {
min-width: 1.1em;
font-size: 500%;
align-self: center;
flex: 0 1 5em;
aspect-ratio: unset;
}
}
&.-banner {
.images-container {
grid-template-rows: 20em 5em 20em;
grid-template-columns: 1fr;
> * {
flex: 1 0 10em;
width: 100%;
aspect-ratio: 3;
}
}
.separator {
min-height: 1.1em;
font-size: 500%;
justify-self: center;
flex: 0 1 5em;
aspect-ratio: unset;
}
}
#modal.-mobile & {
#pick-image {
height: 3em;
}
.image-container {
&.-banner {
max-height: 10em;
}
}
}
}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 5feefcac0e..6d2ff96aed 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -1,885 +1,885 @@
<template>
<div class="user-card">
<div class="user-card-inner">
<div class="user-info">
<div
class="user-identity"
:class="{ '-compact': compact }"
>
<div class="header-overlay">
<div class="banner-image">
<img
:title="$t('user_card.banner')"
:src="bannerImgSrc"
:class="{ 'hide-bio': hideBio }"
>
</div>
<div
class="banner-overlay"
:class="{ 'hide-bio': hideBio }"
/>
</div>
<a
v-if="avatarAction === 'zoom'"
class="user-info-avatar -link"
@click="zoomAvatar"
:title="$t('user_card.avatar')"
>
<UserAvatar :user-id="user.id" />
<div class="user-info-avatar -link -overlay">
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="search-plus"
/>
</div>
</a>
<button
v-else-if="editable"
class="user-info-avatar button-unstyled -link"
:class="{ '-editable': editable }"
@click="changeAvatar"
:title="$t('user_card.avatar')"
>
<UserAvatar
:user-id="user.id"
:url="avatarImgSrc"
/>
<div class="user-info-avatar -link -overlay">
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="pencil"
/>
</div>
</button>
<UserAvatar
v-else-if="typeof avatarAction === 'function'"
class="user-info-avatar"
:user-id="user.id"
@click="avatarAction"
:title="$t('user_card.avatar')"
/>
<router-link
v-else
class="user-info-avatar"
:to="userProfileLink(user)"
:title="$t('user_card.avatar')"
>
<UserAvatar :user-id="user.id" />
</router-link>
<div class="user-summary">
<div class="top-line">
<div
class="other-actions"
role="toolbar"
:title="$t('user_card.actions')"
>
<button
v-if="editable"
:disabled="newName?.length === 0"
class="btn button-unstyled edit-banner-button"
@click="changeBanner"
>
{{ $t('settings.change_banner') }}
<FAIcon
fixed-width
class="icon"
icon="pencil"
:title="$t('settings.change_banner')"
/>
</button>
<button
v-else-if="!editable && !isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
:title="$t('user_card.edit')"
@click.stop="openProfileTab"
>
<FAIcon
fixed-width
class="icon"
icon="edit"
:title="$t('user_card.edit_profile')"
/>
</button>
<a
v-if="isOtherUser && !user.is_local"
role="button"
:title="$t('user_card.external_view')"
:href="user.statusnet_profile_url"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
icon="external-link-alt"
/>
</a>
<AccountActions
v-if="isOtherUser && loggedIn && !hideButtons"
:user="user"
:relationship="relationship"
/>
<router-link
v-if="showExpand"
role="button"
:title="$t('user_card.open_profile')"
:to="userProfileLink(user)"
class="button-unstyled external-link-button"
@click="$emit('close')"
>
<FAIcon
class="icon"
icon="expand-alt"
/>
</router-link>
<button
v-if="showClose"
:title="$t('general.close')"
class="button-unstyled external-link-button"
@click="$emit('close')"
>
<FAIcon
class="icon"
icon="times"
/>
</button>
</div>
<div class="name-wrapper">
<router-link
v-if="!editable || !editingName"
:to="userProfileLink(user)"
class="user-name"
>
<RichContent
:title="editable ? newName : user.name_unescaped"
:html="editable ? newName : user.name_unescaped"
:emoji="editable ? emoji : user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
/>
</router-link>
<EmojiInput
v-else-if="editingName"
v-model="newName"
enable-emoji-picker
:suggest="emojiSuggestor"
>
<template #default="inputProps">
<input
id="username"
v-model="newName"
class="input name-changer"
v-bind="propsToNative(inputProps)"
>
</template>
</EmojiInput>
<button
v-if="editable"
class="button-unstyled edit-button"
:title="$t('settings.toggle_edit')"
@click="editingName = !editingName"
>
<FAIcon
class="icon"
icon="pencil"
/>
</button>
</div>
</div>
<div class="bottom-line">
<UserLink
class="user-screen-name"
:user="user"
/>
<span
v-if="user.locked"
class="lock-icon"
>
<FAIcon
icon="lock"
size="sm"
/>
</span>
<span
v-if="relationship.followed_by && loggedIn && isOtherUser"
class="alert neutral user-role"
>
{{ $t('user_card.follows_you') }}
</span>
<template v-if="!hideBio">
<span
v-if="user.deactivated"
class="alert neutral user-role"
>
{{ $t('user_card.deactivated') }}
</span>
<span
v-if="!!visibleRole"
class="alert neutral user-role"
>
{{ $t(`general.role.${visibleRole}`) }}
</span>
<span
v-if="user.actor_type === 'Service'"
class="alert neutral user-role"
>
{{ $t('user_card.bot') }}
</span>
<span
v-if="user.actor_type === 'Group'"
class="alert neutral user-role"
>
{{ $t('user_card.group') }}
</span>
<span
v-for="tag in user.tags"
:key="tag"
class="alert warning user-role"
>
{{ isKnownTag ? $t('user_card.tags.' + tag) : tag }}
</span>
</template>
</div>
</div>
</div>
<div
v-if="loggedIn && isOtherUser && !hideButtons"
class="user-interactions"
role="toolbar"
:title="$t('user_card.interactions')"
>
<div class="btn-group" role="group">
<FollowButton
:relationship="relationship"
:user="user"
/>
<template v-if="relationship.following">
<ProgressButton
v-if="!relationship.notifying"
class="btn button-default"
:click="subscribeUser"
:title="$t('user_card.subscribe')"
>
<FAIcon icon="bell" />
</ProgressButton>
<ProgressButton
v-else
class="btn button-default toggled"
:click="unsubscribeUser"
:title="$t('user_card.unsubscribe')"
>
<FALayers>
<FAIcon
icon="rss"
transform="left-5 shrink-6 up-3 rotate-20"
flip="horizontal"
/>
<FAIcon
icon="rss"
transform="right-5 shrink-6 up-3 rotate-20"
/>
<FAIcon icon="bell" />
</FALayers>
</ProgressButton>
</template>
</div>
<button
v-if="relationship.muting"
class="btn button-default btn-mute toggled"
:disabled="user.deactivated"
@click="unmuteUser"
>
{{ $t('user_card.muted') }}
</button>
<button
v-else
class="btn button-default btn-mute"
:disabled="user.deactivated"
@click="muteUser"
>
{{ $t('user_card.mute') }}
</button>
<button
class="btn button-default btn-mention"
:disabled="user.deactivated"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
<ModerationTools
v-if="showModerationMenu"
:users="[user]"
/>
</div>
<div
v-if="!loggedIn && user.is_local"
class="user-interactions"
>
<RemoteFollow :user="user" />
</div>
</div>
</div>
<div
v-if="!editable && loggedIn && isOtherUser && (hasNote || !hideBio) && !hideRemarks"
class="personal-marks"
>
<UserNote
v-if="hasNote || (hasNoteEditor && supportsNote)"
:user="user"
:relationship="relationship"
:editable="hasNoteEditor"
/>
<div
v-if="!hideBio"
class="highlighter"
>
<h4>{{ $t('user_card.highlight_header') }}</h4>
<Select
:id="'userHighlightSel'+user.id"
v-model="userHighlightType"
- class="userHighlightSel unstyled"
- :class="{ '-none': userHighlightType === 'disabled' }"
+ :root-class="['userHighlightSel', { '-none': userHighlightType === 'disabled' }]"
+ unstyled
>
<option value="disabled">
{{ $t('user_card.highlight_new.disabled') }}
</option>
<option value="solid">
{{ $t('user_card.highlight_new.solid') }}
</option>
<option value="striped">
{{ $t('user_card.highlight_new.striped') }}
</option>
<option value="side">
{{ $t('user_card.highlight_new.side') }}
</option>
</Select>
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
<ColorInput
v-if="userHighlightType !== 'disabled'"
v-model="userHighlightColor"
class="highlighter-color"
:show-optional-checkbox="false"
name="'userHighlightColorTx'+user.id"
unstyled
/>
</div>
</div>
<div
v-if="user.adminData && !hideBio"
class="admin-data"
>
<details>
<summary>
{{ $t('user_card.admin_data.data') }}
</summary>
<div class="user-profile-fields">
<dl class="user-profile-field">
<dt class="user-profile-field-name">
{{ $t('admin_dash.users.local_id') }}
</dt>
<dd class="user-profile-field-value">
{{ user.adminData.id }}
</dd>
</dl>
<dl
v-if="user.is_local"
class="user-profile-field"
>
<dt class="user-profile-field-name">
{{ $t('admin_dash.users.labels.email') }}
</dt>
<dd
class="user-profile-field-value"
:class="{ faint: user.adminData.email == null }"
>
{{ user.adminData.email == null ? $t('general.not_available') : user.adminData.email }}
</dd>
</dl>
<dl
v-if="user.is_local"
class="user-profile-field"
>
<dt class="user-profile-field-name">
{{ $t('general.role.admin') }}
</dt>
<dd class="user-profile-field-value">
{{ $t('general.' + (user.adminData.roles.admin ? 'yes' : 'no')) }}
</dd>
</dl>
<dl
v-if="user.is_local"
class="user-profile-field"
>
<dt class="user-profile-field-name">
{{ $t('general.role.moderator') }}
</dt>
<dd class="user-profile-field-value">
{{ $t('general.' + (user.adminData.roles.moderator ? 'yes' : 'no')) }}
</dd>
</dl>
<dl
v-if="user.is_local"
class="user-profile-field"
>
<dt class="user-profile-field-name">
{{ $t('admin_dash.users.indicator.confirmed') }}
</dt>
<dd class="user-profile-field-value">
{{ $t('general.' + (user.adminData.is_confirmed ? 'yes' : 'no')) }}
</dd>
</dl>
<dl
v-if="user.is_local"
class="user-profile-field"
>
<dt class="user-profile-field-name">
{{ $t('admin_dash.users.indicator.approved') }}
</dt>
<dd class="user-profile-field-value">
{{ $t('general.' + (user.adminData.is_approved ? 'yes' : 'no')) }}
</dd>
</dl>
<dl class="user-profile-field">
<dt class="user-profile-field-name">
{{ $t('admin_dash.users.indicator.suggested') }}
</dt>
<dd class="user-profile-field-value">
{{ $t('general.' + (user.adminData.is_suggested ? 'yes' : 'no')) }}
</dd>
</dl>
<details
v-if="user.is_local"
open
>
<summary>
{{ $t('user_card.admin_data.registration_reason') }}
</summary>
<span>
{{ user.adminData.registration_reason == null ? $t('general.not_available') : user.adminData.registration_reason }}
</span>
</details>
<details open>
<summary>
{{ $t('user_card.admin_data.tags') }}
</summary>
<ul>
<li v-if="user.adminData.tags.length === 0">
{{ $t('general.none') }}
</li>
<li
v-for="tag in user.adminData.tags"
:key="tag"
>
<code>
{{ tag }}
</code>
{{ ' ' }}
</li>
</ul>
</details>
</div>
</details>
</div>
<h3 v-if="editable">
<span>
{{ $t('settings.bio') }}
</span>
{{ ' ' }}
<button
class="button-default"
@click="editingBio = !editingBio"
>
{{ $t('settings.toggle_edit') }}
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="pencil"
/>
</button>
</h3>
<template v-if="!editable || !editingBio">
<RichContent
v-if="!hideBio"
class="user-card-bio"
:class="{ '-justify-left': mergedConfig.userCardLeftJustify }"
:html="editable ? escapedNewBio : user.description_html"
:emoji="editable ? emoji : user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
:handle-links="true"
:aria-label="$t('user_card.bio')"
role="article"
/>
</template>
<template v-else-if="editingBio">
<EmojiInput
v-model="newBio"
enable-emoji-picker
class="user-card-bio"
:class="{ '-justify-left': mergedConfig.userCardLeftJustify }"
:suggest="emojiUserSuggestor"
>
<template #default="inputProps">
<textarea
v-model="newBio"
class="input bio resize-height"
v-bind="propsToNative(inputProps)"
:rows="newBio.split(/\n/g).length"
/>
</template>
</EmojiInput>
</template>
<h3 v-if="editable">
<span>
{{ $t('settings.profile_fields.label') }}
</span>
{{ ' ' }}
<button
class="button-default"
@click="editingFields = !editingFields"
>
{{ $t('settings.toggle_edit') }}
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="pencil"
/>
</button>
</h3>
<template v-if="!editable || !editingFields">
<div
v-if="!hideBio && user.fields_html && user.fields_html.length > 0"
class="user-profile-fields"
>
<dl
v-for="(field, index) in (editable ? newFields : user.fields_html)"
:key="index"
class="user-profile-field"
>
<dt
:title="field.name"
class="user-profile-field-name"
>
<RichContent
:html="field.name"
:emoji="editable ? emoji : user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
/>
</dt>
<dd
:title="field.value"
class="user-profile-field-value"
>
<RichContent
:html="field.value"
:emoji="editable ? emoji : user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
/>
</dd>
</dl>
</div>
</template>
<template v-else-if="editingFields">
<div
v-if="maxFields > 0"
class="user-profile-fields"
>
<dl
v-for="(_, i) in newFields"
:key="i"
class="user-profile-field"
>
<dt
class="user-profile-field-name -edit"
>
<EmojiInput
v-model="newFields[i].name"
enable-emoji-picker
:suggest="emojiSuggestor"
>
<template #default="inputProps">
<input
v-model="newFields[i].name"
:placeholder="$t('settings.profile_fields.name')"
v-bind="propsToNative(inputProps)"
class="input"
>
</template>
</EmojiInput>
</dt>
<dd
class="user-profile-field-value -edit"
>
<EmojiInput
v-model="newFields[i].value"
enable-emoji-picker
:suggest="emojiSuggestor"
>
<template #default="inputProps">
<input
v-model="newFields[i].value"
:placeholder="$t('settings.profile_fields.value')"
v-bind="propsToNative(inputProps)"
class="input input"
>
</template>
</EmojiInput>
<button
class="delete-field button-default -hover-highlight"
@click="deleteField(i)"
>
<!-- TODO something is wrong with v-show here -->
<FAIcon
v-if="newFields.length > 1"
icon="times"
/>
</button>
</dd>
</dl>
<button
v-if="newFields.length < maxFields"
class="user-profile-field-add add-field button-default -hover-highlight"
@click="addField"
>
<FAIcon
icon="plus"
class="icon"
/>
<span class="label">
{{ $t("settings.profile_fields.add_field") }}
</span>
</button>
</div>
</template>
<div
v-if="!hideBio"
class="user-extras"
>
<span
v-if="!editable && !hideUserStats"
class="user-stats"
>
<dl
v-if="!hideUserStats && !hideBio"
class="user-count"
>
<dd>{{ user.statuses_count }}</dd>
{{ ' ' }}
<dt>{{ $t('user_card.statuses') }}</dt>
</dl>
<dl class="user-count">
<dd>{{ dailyAvg }}</dd>
{{ ' ' }}
<dt>{{ $t('user_card.statuses_per_day') }}</dt>
</dl>
<dl class="user-count">
<dd>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</dd>
{{ ' ' }}
<dt>{{ $t('user_card.followees') }}</dt>
</dl>
<dl class="user-count">
<dd>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</dd>
{{ ' ' }}
<dt>{{ $t('user_card.followers') }}</dt>
</dl>
</span>
<span
v-if="!hideUserStats"
class="user-stats"
>
<template v-if="!hideBio">
<dl
v-if="user.birthday && !editable"
class="user-count"
>
<dd>
<FAIcon
class="fa-old-padding"
icon="birthday-cake"
/>
</dd>
{{ ' ' }}
<dt>
{{ $t('user_card.birthday', { birthday: formattedBirthday }) }}
</dt>
</dl>
<div
v-else-if="editable"
class="birthday"
>
<div>
<Checkbox v-model="newShowBirthday">
{{ $t('settings.birthday.show_birthday') }}
</Checkbox>
</div>
<FAIcon
class="fa-old-padding"
icon="birthday-cake"
/>
{{ $t('settings.birthday.label') }}
<input
id="birthday"
v-model="newBirthday"
type="date"
class="input birthday-input"
>
</div>
</template>
<dl
v-if="!editable"
class="user-count"
>
<dd>
{{ $t('user_card.joined') }}
</dd>
{{ ' ' }}
<dt>
{{ formattedJoinDate }}
</dt>
</dl>
</span>
</div>
<template v-if="editable">
<h3>{{ $t('settings.profile_other') }}</h3>
<p
v-if="role === 'admin' || role === 'moderator'"
class="user-card-setting"
>
<Checkbox v-model="newShowRole">
<template v-if="role === 'admin'">
{{ $t('settings.show_admin_badge') }}
</template>
<template v-if="role === 'moderator'">
{{ $t('settings.show_moderator_badge') }}
</template>
</Checkbox>
</p>
<p class="user-card-setting">
<label>
{{ $t('settings.actor_type') }}
<Select v-model="newActorType">
<option
v-for="option in availableActorTypes"
:key="option"
:value="option"
>
{{ $t('settings.actor_type_' + (option === 'Person' ? 'person_proper' : option)) }}
</option>
</Select>
<div v-if="groupActorAvailable">
<small>
{{ $t('settings.actor_type_description') }}
</small>
</div>
</label>
</p>
<div class="bottom-buttons">
<button
v-if="editable"
:disabled="!somethingToSave"
class="btn button-default reset-profile-button"
@click="resetState"
>
{{ $t('settings.reset') }}
<FAIcon
fixed-width
class="icon"
icon="clock-rotate-left"
:title="$t('user_card.edit_profile')"
/>
</button>
<button
v-if="editable"
:disabled="!somethingToSave"
class="btn button-default save-profile-button"
@click="updateProfile"
>
{{ $t('settings.save') }}
<FAIcon
fixed-width
class="icon"
icon="save"
:title="$t('user_card.edit_profile')"
/>
</button>
</div>
</template>
<teleport to="#modal">
<UserTimedFilterModal
v-if="isOtherUser"
ref="timedMuteDialog"
:user="user"
:is-mute="true"
/>
</teleport>
<teleport to="#modal">
<DialogModal
v-if="editImage"
class="edit-image"
>
<template #header>
{{ editImage === 'avatar' ? $t('settings.change_avatar') : $t('settings.change_banner') }}
</template>
<p>
{{ editImage === 'avatar' ? $t('settings.avatar_size_instruction') : $t('settings.banner_size_instruction' ) }}
</p>
<div
class="image-container"
:class="{ '-banner': editImage === 'banner' }"
>
<image-cropper
ref="cropper"
class="cropper"
:aspect-ratio="editImage === 'avatar' ? 1 : 3"
@submit="submitImage"
/>
</div>
<button
id="pick-image"
class="button-default btn"
type="button"
@click="() => $refs.cropper.pickImage()"
>
{{ $t('settings.select_picture') }}
</button>
<template #footer>
<button
class="button-default btn"
type="button"
@click="editImage = false"
>
{{ $t('image_cropper.cancel') }}
</button>
<button
:title="editImage === 'avatar' ? $t('settings.reset_avatar') : $t('settings.reset_banner')"
class="button-default btn reset-button"
@click="resetImage"
>
{{ editImage === 'avatar' ? $t('settings.reset_avatar') : $t('settings.reset_banner' ) }}
</button>
<button
class="button-default btn"
type="button"
@click="$refs.cropper.submit(false)"
>
{{ $t('image_cropper.save_without_cropping') }}
</button>
<button
class="button-default btn"
type="button"
@click="$refs.cropper.submit(true)"
>
{{ $t('image_cropper.save') }}
</button>
</template>
</DialogModal>
</teleport>
</div>
</template>
<script src="./user_card.js"></script>
<style lang="scss" src="./user_card.scss" />
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index 8369facb4a..3df50068d3 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -1,52 +1,51 @@
<template>
<aside
:aria-label="$t('nav.user_panel')"
class="user-panel"
>
<div
v-if="loggedIn"
key="user-panel-signed"
class="panel panel-default signed-in"
>
<UserCard
:user-id="currentUser.id"
:hide-bio="true"
/>
<PostStatusForm />
</div>
<AuthForm
v-else
key="user-panel"
/>
</aside>
</template>
<script src="./user_panel.js"></script>
<style lang="scss">
.user-panel {
.panel {
background: var(--background);
backdrop-filter: var(--backdrop-filter);
}
.user-info {
- margin: 0.6em 0.6em 0;
.Avatar {
width: 5em;
width: calc(min(5em, 20cqw));
height: 5em;
height: calc(min(5em, 20cqw));
}
}
.post-status-form {
margin: 0.5em;
}
.signed-in {
z-index: 10;
}
}
</style>
diff --git a/src/components/user_popover/user_popover.vue b/src/components/user_popover/user_popover.vue
index 803caa4dd4..0e6f14eb2f 100644
--- a/src/components/user_popover/user_popover.vue
+++ b/src/components/user_popover/user_popover.vue
@@ -1,56 +1,55 @@
<template>
<Popover
trigger="click"
popover-class="popover-default user-popover"
:overlay-centers-selector="overlayCentersSelector || '.user-info .Avatar'"
:overlay-centers="overlayCenters && userPopoverOverlay"
:link="link"
:href="href"
v-bind="$attrs"
>
<template #trigger>
<slot />
</template>
<template #content="{close}">
<UserCard
class="user-popover"
:compact="true"
:show-close="true"
:show-expand="true"
:user-id="userId"
:hide-bio="true"
:avatar-action="userPopoverAvatarAction == 'close' ? close : userPopoverAvatarAction"
@close="close"
/>
</template>
</Popover>
</template>
<script src="./user_popover.js"></script>
<style lang="scss">
/* popover styles load on-demand, so we need to override */
/* stylelint-disable block-no-empty */
.user-popover {
margin: 0;
.user-info {
width: 100%;
- margin: 1.2em;
}
.user-identity {
min-width: calc(min(30em, 98vw));
}
.user-card-inner {
display: flex;
}
&.popover {
overflow: hidden;
}
}
/* stylelint-enable block-no-empty */
</style>
diff --git a/src/components/user_profile/user_profile.scss b/src/components/user_profile/user_profile.scss
index 4b68b46b03..065093988e 100644
--- a/src/components/user_profile/user_profile.scss
+++ b/src/components/user_profile/user_profile.scss
@@ -1,65 +1,61 @@
.user-profile {
flex: 2;
.card-wrapper {
border-top-left-radius: var(--roundness);
border-top-right-radius: var(--roundness);
}
.panel-footer {
border-bottom-left-radius: var(--roundness);
border-bottom-right-radius: var(--roundness);
}
// No sticky header on user profile
--currentPanelStack: 0;
.userlist-placeholder {
display: flex;
justify-content: center;
align-items: center;
padding: 2em;
}
- .user-info {
- margin: 1.2em;
- }
-
&.-admin-view {
.filter {
padding: 1em;
}
.list-item {
padding: 0;
border-bottom: 1px solid var(--border);
.Status{
width: 100%
}
}
.footer {
background: var(--background);
}
}
}
.user-profile-placeholder {
.panel-body {
display: flex;
justify-content: center;
align-items: center;
padding: 7em;
}
.alert {
padding: 0.75em 5em;
border-width: 2px;
.error-message {
color: var(--text);
font-weight: bold;
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Oct 13, 3:26 AM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1786584
Default Alt Text
(80 KB)

Event Timeline