Merge branch 'threecolumn' into shigusegubu-vue3

* threecolumn:
  Fix active popover style
  Use panel text instead of text for shoutbox icon
  Fix dropdown menu style inside panel header
  Fix phoenix sockets in dev mode
  Fix no reactivity on vuex 4 values
  fix tegulu
  heck
  teleport bread
  wide mode initial implementation + cleanup
  make chatlist header sticky
  fix random error that sometimes occurs
  cleanup & code splitting
  fix chat loading endlessly
  chats work and look a bit better
  fix main column having wild widths
  fixed tons of stuff, at least it looks normalish on desktop
  refactored how main app layout works
This commit is contained in:
Henry Jameson 2022-04-07 10:34:17 +03:00
commit 4e74e9d08c
31 changed files with 704 additions and 613 deletions

View file

@ -52,7 +52,10 @@ module.exports = {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost',
ws: true
ws: true,
headers: {
'Origin': target
}
},
'/oauth/revoke': {
target,

View file

@ -1,6 +1,5 @@
import UserPanel from './components/user_panel/user_panel.vue'
import NavPanel from './components/nav_panel/nav_panel.vue'
import Notifications from './components/notifications/notifications.vue'
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
@ -16,13 +15,14 @@ import PostStatusModal from './components/post_status_modal/post_status_modal.vu
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
import { mapGetters } from 'vuex'
import { defineAsyncComponent } from 'vue'
export default {
name: 'app',
components: {
UserPanel,
NavPanel,
Notifications,
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
InstanceSpecificPanel,
FeaturesPanel,
WhoToFollowPanel,
@ -65,7 +65,7 @@ export default {
}
}
},
shout () { return this.$store.state.shout.channel.state === 'joined' },
shout () { return this.$store.state.shout.joined },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel &&
@ -79,22 +79,20 @@ export default {
hideShoutbox () {
return this.$store.getters.mergedConfig.hideShoutbox
},
isMobileLayout () { return this.$store.state.interface.mobileLayout },
layoutType () { return this.$store.state.interface.layoutType },
privateMode () { return this.$store.state.instance.private },
sidebarAlign () {
return {
'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0
}
},
reverseLayout () { return this.$store.getters.mergedConfig.sidebarRight },
...mapGetters(['mergedConfig'])
},
methods: {
updateMobileState () {
const mobileLayout = windowWidth() <= 800
const wideLayout = windowWidth() >= 1300
const layoutHeight = windowHeight()
const changed = mobileLayout !== this.isMobileLayout
const layoutType = wideLayout ? 'wide' : (mobileLayout ? 'mobile' : 'normal')
const changed = layoutType !== this.layoutType
if (changed) {
this.$store.dispatch('setMobileLayout', mobileLayout)
this.$store.dispatch('setLayoutType', layoutType)
}
this.$store.dispatch('setLayoutHeight', layoutHeight)
}

View file

@ -1,62 +1,21 @@
// stylelint-disable rscss/class-format
@import './_variables.scss';
#app {
min-height: 100vh;
max-width: 100%;
overflow: hidden;
}
.app-bg-wrapper {
position: fixed;
z-index: -1;
height: 100%;
left: 0;
right: -20px;
background-size: cover;
background-repeat: no-repeat;
background-color: var(--wallpaper);
background-image: var(--body-background-image);
background-position: 50% 50px;
}
i[class^='icon-'] {
user-select: none;
}
h4 {
margin: 0;
}
#content {
box-sizing: border-box;
padding-top: 60px;
margin: auto;
min-height: 100vh;
max-width: 980px;
align-content: flex-start;
}
.underlay {
background-color: rgba(0,0,0,0.15);
background-color: var(--underlay, rgba(0,0,0,0.15));
}
.text-center {
text-align: center;
}
html {
font-size: 14px;
overflow: hidden;
max-height: 100vh;
}
body {
overscroll-behavior-y: none;
overflow: hidden;
max-height: 100vh;
max-width: 100vw;
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
margin: 0;
color: $fallback--text;
color: var(--text, $fallback--text);
max-width: 100vw;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@ -72,6 +31,171 @@ a {
color: var(--link, $fallback--link);
}
h4 {
margin: 0;
}
nav {
z-index: 1000;
color: var(--topBarText);
background-color: $fallback--fg;
background-color: var(--topBar, $fallback--fg);
color: $fallback--faint;
color: var(--faint, $fallback--faint);
box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--topBarShadow);
box-sizing: border-box;
height: var(--navbar-height);
}
#app-loaded {
min-height: 100vh;
min-width: 100vw;
overflow: hidden;
--navbar-height: 50px;
}
#content {
overscroll-behavior-y: none;
overflow-y: auto;
position: sticky;
}
#sidebar {
grid-area: sidebar;
}
#notifs-column {
grid-area: notifs;
}
#main-scroller {
grid-area: content;
}
.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%;
}
.underlay {
grid-column-start: 1;
grid-column-end: span 3;
grid-row-start: 1;
grid-row-end: 1;
margin: 0 -0.5em;
padding: 0 0.5em;
pointer-events: none;
background-color: rgba(0, 0, 0, 0.15);
background-color: var(--underlay, rgba(0, 0, 0, 0.15));
z-index: -2000;
}
.app-layout {
position: relative;
display: grid;
grid-template-columns: var(--miniColumn) var(--maxiColumn);
grid-template-areas: "sidebar content";
grid-template-rows: 1fr;
box-sizing: border-box;
margin: 0 auto;
height: calc(100vh - var(--navbar-height));
align-content: flex-start;
flex-wrap: wrap;
padding: 0 10px 0 10px;
justify-content: center;
--miniColumn: 345px;
--maxiColumn: minmax(345px, 615px);
.column {
display: grid;
grid-template-columns: 100%;
box-sizing: border-box;
padding-top: 10px;
grid-row-start: 1;
grid-row-end: 1;
margin: 0 0.5em;
row-gap: 1em;
align-content: start;
&.-scrollable {
padding-top: 10px;
position: sticky;
top: 0;
max-height: calc(100vh - var(--navbar-height));
overflow-y: auto;
overflow-x: hidden;
.panel-heading.-sticky {
top: -10px;
}
}
}
.column-inner {
display: grid;
grid-template-columns: 100%;
box-sizing: border-box;
row-gap: 1em;
align-content: start;
}
&.-reverse:not(.-wide):not(.-mobile) {
grid-template-columns: var(--maxiColumn) var(--miniColumn);
grid-template-areas: "content sidebar";
}
&.-wide {
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
grid-template-areas: "sidebar content notifs";
&.-reverse {
grid-template-areas: "notifs content sidebar";
}
}
&.-mobile {
grid-template-columns: 100vw;
grid-template-areas: "content";
padding: 0;
.column {
margin: 0;
}
.underlay {
display: none;
}
#sidebar {
display: none;
}
}
}
#content,
.column.-scrollable {
&::-webkit-scrollbar {
display: block;
width: 0;
}
}
.text-center {
text-align: center;
}
.button-default {
user-select: none;
color: $fallback--text;
@ -103,12 +227,12 @@ a {
}
&:hover {
box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3);
box-shadow: 0 0 4px rgba(255, 255, 255, 0.3);
box-shadow: var(--buttonHoverShadow);
}
&:active {
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow);
color: $fallback--text;
color: var(--btnPressedText, $fallback--text);
@ -141,7 +265,7 @@ a {
color: var(--btnToggledText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnToggled, $fallback--fg);
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow);
svg,
@ -191,8 +315,9 @@ a {
}
}
input, textarea, .input {
input,
textarea,
.input {
&.unstyled {
border-radius: 0;
background: none;
@ -203,7 +328,7 @@ input, textarea, .input {
border: none;
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 0px 2px 0px rgba(0, 0, 0, 1) inset;
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
box-shadow: var(--inputShadow);
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
@ -219,9 +344,11 @@ input, textarea, .input {
height: 28px;
line-height: 16px;
hyphens: none;
padding: 8px .5em;
padding: 8px 0.5em;
&:disabled, &[disabled=disabled], &.disabled {
&:disabled,
&[disabled=disabled],
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
@ -236,18 +363,21 @@ input, textarea, .input {
&[type=radio] {
display: none;
&:checked + label::before {
box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset;
box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset;
box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset;
box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset;
background-color: var(--accent, $fallback--link);
}
&:disabled {
&,
& + label,
& + label::before {
opacity: .5;
opacity: 0.5;
}
}
+ label::before {
flex-shrink: 0;
display: inline-block;
@ -256,9 +386,9 @@ input, textarea, .input {
width: 1.1em;
height: 1.1em;
border-radius: 100%; // Radio buttons should always be circle
box-shadow: 0px 0px 2px black inset;
box-shadow: 0 0 2px black inset;
box-shadow: var(--inputShadow);
margin-right: .5em;
margin-right: 0.5em;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
vertical-align: top;
@ -274,17 +404,20 @@ input, textarea, .input {
&[type=checkbox] {
display: none;
&:checked + label::before {
color: $fallback--text;
color: var(--inputText, $fallback--text);
}
&:disabled {
&,
& + label,
& + label::before {
opacity: .5;
opacity: 0.5;
}
}
+ label::before {
flex-shrink: 0;
display: inline-block;
@ -294,9 +427,9 @@ input, textarea, .input {
height: 1.1em;
border-radius: $fallback--checkboxRadius;
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
box-shadow: 0px 0px 2px black inset;
box-shadow: 0 0 2px black inset;
box-shadow: var(--inputShadow);
margin-right: .5em;
margin-right: 0.5em;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
vertical-align: top;
@ -324,6 +457,7 @@ option {
.hide-number-spinner {
-moz-appearance: textfield;
&[type=number]::-webkit-inner-spin-button,
&[type=number]::-webkit-outer-spin-button {
opacity: 0;
@ -331,7 +465,8 @@ option {
}
}
i[class*=icon-], .svg-inline--fa {
i[class*=icon-],
.svg-inline--fa {
color: $fallback--icon;
color: var(--icon, $fallback--icon);
}
@ -362,273 +497,16 @@ i[class*=icon-], .svg-inline--fa {
}
}
.container {
display: flex;
flex-wrap: wrap;
margin: 0;
padding: 0 10px 0 10px;
}
.auto-size {
flex: 1
}
main-router {
flex: 1;
}
.status.compact {
color: rgba(0, 0, 0, 0.42);
font-weight: 300;
p {
margin: 0;
font-size: 0.8em
}
}
/* Panel */
.panel {
display: flex;
position: relative;
flex-direction: column;
margin: 0.5em;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
&::after, & {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: none;
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
box-shadow: var(--panelShadow);
}
}
.panel-body:empty::before {
content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations
display: block;
margin: 1em;
text-align: center;
}
.panel-heading {
display: flex;
flex: none;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
background-size: cover;
padding: .6em .6em;
text-align: left;
line-height: 28px;
color: var(--panelText);
background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
align-items: baseline;
box-shadow: var(--panelHeaderShadow);
.title {
flex: 1 0 auto;
font-size: 1.3em;
}
.faint {
background-color: transparent;
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
.faint-link {
color: $fallback--faint;
color: var(--faintLink, $fallback--faint);
}
.alert {
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
}
.button-default,
.alert {
// height: 100%;
line-height: 21px;
min-height: 0;
box-sizing: border-box;
margin: 0;
margin-left: .5em;
min-width: 1px;
align-self: stretch;
}
.button-default {
flex-shrink: 0;
&,
i[class*=icon-] {
color: $fallback--text;
color: var(--btnPanelText, $fallback--text);
}
&:active {
background-color: $fallback--fg;
background-color: var(--btnPressedPanel, $fallback--fg);
color: $fallback--text;
color: var(--btnPressedPanelText, $fallback--text);
}
&:disabled {
color: $fallback--text;
color: var(--btnDisabledPanelText, $fallback--text);
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledPanelText, $fallback--text);
}
}
a,
.-link {
color: $fallback--link;
color: var(--panelLink, $fallback--link)
}
}
.panel-heading.stub {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
/* TODO Should remove timeline-footer from here when we refactor panels into
* separate component and utilize slots
*/
.panel-footer, .timeline-footer {
display: flex;
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
flex: none;
padding: 0.6em 0.6em;
text-align: left;
line-height: 28px;
align-items: baseline;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--border, $fallback--border);
.faint {
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
a,
.-link {
color: $fallback--link;
color: var(--panelLink, $fallback--link);
}
}
.panel-body > p {
line-height: 18px;
padding: 1em;
margin: 0;
}
.container > * {
min-width: 0px;
}
@import './panel.scss';
.fa {
color: grey;
}
nav {
z-index: 1000;
color: var(--topBarText);
background-color: $fallback--fg;
background-color: var(--topBar, $fallback--fg);
color: $fallback--faint;
color: var(--faint, $fallback--faint);
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
box-shadow: var(--topBarShadow);
box-sizing: border-box;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .2s
}
.fade-enter-from, .fade-leave-active {
opacity: 0
}
.main {
flex-basis: 50%;
flex-grow: 1;
flex-shrink: 1;
}
.sidebar-bounds {
flex: 0;
flex-basis: 35%;
}
.sidebar-flexer {
flex: 1;
flex-basis: 345px;
width: 365px;
}
.mobile-shown {
display: none;
}
@media all and (min-width: 800px) {
body {
overflow-y: scroll;
}
.sidebar-bounds {
overflow: hidden;
max-height: 100vh;
width: 345px;
position: fixed;
margin-top: -10px;
.sidebar-scroller {
height: 96vh;
width: 365px;
padding-top: 10px;
padding-right: 50px;
overflow-x: hidden;
overflow-y: scroll;
}
.sidebar {
width: 345px;
}
}
.sidebar-flexer {
max-height: 96vh;
flex-shrink: 0;
flex-grow: 0;
}
}
.badge {
box-sizing: border-box;
display: inline-block;
@ -712,7 +590,7 @@ nav {
}
.visibility-notice {
padding: .5em;
padding: 0.5em;
border: 1px solid $fallback--faint;
border: 1px solid var(--faint, $fallback--faint);
border-radius: $fallback--inputRadius;
@ -727,7 +605,7 @@ nav {
position: absolute;
top: 0;
right: 0;
padding: .5em;
padding: 0.5em;
color: inherit;
}
}
@ -744,72 +622,6 @@ nav {
}
}
@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);
}
}
@media all and (max-width: 800px) {
.mobile-hidden {
display: none;
}
.panel-switcher {
display: flex;
}
.container {
padding: 0;
}
.panel {
margin: 0.5em 0 0.5em 0;
}
.menu-button {
display: block;
margin-right: 0.8em;
}
.main {
margin-bottom: 7em;
}
}
.setting-list,
.option-list{
list-style-type: none;
padding-left: 2em;
li {
margin-bottom: 0.5em;
}
.suboptions {
margin-top: 0.3em
}
}
.login-hint {
text-align: center;
@ -819,7 +631,7 @@ nav {
a {
display: inline-block;
padding: 1em 0px;
padding: 1em 0;
width: 100%;
}
}
@ -828,9 +640,46 @@ nav {
min-height: 28px;
}
.animate-spin {
animation: spin 2s infinite linear;
display: inline-block;
.new-status-notification {
position: relative;
font-size: 1.1em;
z-index: 1;
flex: 1;
}
@media all and (min-width: 800px) {
.sidebar-bounds {
overflow: hidden;
max-height: 100vh;
width: 345px;
position: fixed;
margin-top: -10px;
.sidebar-scroller {
height: 96vh;
width: 365px;
padding-top: 10px;
padding-right: 50px;
overflow-x: hidden;
overflow-y: scroll;
}
.sidebar {
width: 345px;
}
}
.sidebar-flexer {
max-height: 96vh;
flex-shrink: 0;
flex-grow: 0;
}
}
@media all and (max-width: 800px) {
.mobile-hidden {
display: none;
}
}
@keyframes spin {
@ -843,49 +692,47 @@ nav {
}
}
.new-status-notification {
position: relative;
font-size: 1.1em;
z-index: 1;
flex: 1;
}
.chat-layout {
// Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens).
overflow: hidden;
height: 100%;
// Get rid of scrollbar on body as scrolling happens on different element
body {
overflow: hidden;
@keyframes shakeError {
0% {
transform: translateX(0);
}
// Ensures the fixed position of the mobile browser bars on scroll up / down events.
// Prevents the mobile browser bars from overlapping or hiding the message posting form.
@media all and (max-width: 800px) {
body {
height: 100%;
}
15% {
transform: translateX(0.375rem);
}
#app {
height: 100%;
overflow: hidden;
min-height: auto;
}
30% {
transform: translateX(-0.375rem);
}
#app_bg_wrapper {
overflow: hidden;
}
45% {
transform: translateX(0.375rem);
}
.main {
overflow: hidden;
height: 100%;
}
60% {
transform: translateX(-0.375rem);
}
#content {
padding-top: 0;
height: 100%;
overflow: visible;
}
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.2s;
}
.fade-enter-from,
.fade-leave-active {
opacity: 0;
}

View file

@ -7,33 +7,26 @@
id="app_bg_wrapper"
class="app-bg-wrapper"
/>
<MobileNav v-if="isMobileLayout" />
<MobileNav v-if="layoutType === 'mobile'" />
<DesktopNav v-else />
<div class="app-bg-wrapper app-container-wrapper" />
<notifications v-if="currentUser" />
<div
id="content"
class="container underlay"
class="app-layout container"
:class="[{ '-reverse': reverseLayout }, '-' + layoutType]"
>
<div
class="sidebar-flexer mobile-hidden"
:style="sidebarAlign"
>
<div class="sidebar-bounds">
<div class="sidebar-scroller">
<div class="sidebar">
<user-panel />
<div v-if="!isMobileLayout">
<nav-panel />
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<features-panel v-if="!currentUser && showFeaturesPanel" />
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
<notifications v-if="currentUser" />
</div>
</div>
</div>
</div>
<div class="underlay"/>
<div id="sidebar" class="column -scrollable">
<user-panel />
<template v-if="layoutType !== 'mobile'">
<nav-panel />
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<features-panel v-if="!currentUser && showFeaturesPanel" />
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
<div id="notifs-sidebar" />
</template>
</div>
<div class="main">
<div id="main-scroller" class="column main">
<div
v-if="!currentUser"
class="login-hint panel panel-default"
@ -47,6 +40,7 @@
</div>
<router-view />
</div>
<div id="notifs-column" class="column -scrollable"/>
<media-modal />
</div>
<shout-panel

View file

@ -332,8 +332,11 @@ const checkOAuthToken = async ({ store }) => {
}
const afterStoreSetup = async ({ store, i18n }) => {
const width = windowWidth()
store.dispatch('setMobileLayout', width <= 800)
// TODO cleanup copypasta
const mobileLayout = windowWidth() <= 800
const wideLayout = windowWidth() >= 1300
const layoutType = wideLayout ? 'wide' : (mobileLayout ? 'mobile' : 'normal')
store.dispatch('setLayoutType', layoutType)
FaviconService.initFaviconService()

View file

@ -1,5 +1,5 @@
<template>
<div class="sidebar">
<div class="column-inner">
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<staff-panel />
<terms-of-service-panel />

View file

@ -6,7 +6,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import ChatTitle from '../chat_title/chat_title.vue'
import chatService from '../../services/chat_service/chat_service.js'
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js'
import { getScrollPosition, getNewTopPosition, isBottomedOut, isScrollable } from './chat_layout_utils.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronDown,
@ -19,8 +19,10 @@ library.add(
faChevronLeft
)
const scroller = () => document.getElementById('content')
const BOTTOMED_OUT_OFFSET = 10
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
const SAFE_RESIZE_TIME_OFFSET = 100
const MARK_AS_READ_DELAY = 1500
const MAX_RETRIES = 10
@ -46,19 +48,18 @@ const Chat = {
window.addEventListener('resize', this.handleLayoutChange)
},
mounted () {
window.addEventListener('scroll', this.handleScroll)
scroller().addEventListener('scroll', this.handleScroll)
if (typeof document.hidden !== 'undefined') {
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
}
this.$nextTick(() => {
this.updateScrollableContainerHeight()
this.handleResize()
})
this.setChatLayout()
},
unmounted () {
window.removeEventListener('scroll', this.handleScroll)
scroller().removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleLayoutChange)
this.unsetChatLayout()
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
@ -132,7 +133,6 @@ const Chat = {
onFilesDropped () {
this.$nextTick(() => {
this.handleResize()
this.updateScrollableContainerHeight()
})
},
handleVisibilityChange () {
@ -154,10 +154,6 @@ const Chat = {
if (html) {
html.classList.add('chat-layout')
}
this.$nextTick(() => {
this.updateScrollableContainerHeight()
})
},
unsetChatLayout () {
let html = document.querySelector('html')
@ -167,17 +163,9 @@ const Chat = {
},
handleLayoutChange () {
this.$nextTick(() => {
this.updateScrollableContainerHeight()
this.scrollDown()
})
},
// Ensures the proper position of the posting form in the mobile layout (the mobile browser panel does not overlap or hide it)
updateScrollableContainerHeight () {
const header = this.$refs.header
const footer = this.$refs.footer
const inner = this.mobileLayout ? window.document.body : this.$refs.inner
this.scrollableContainerHeight = scrollableContainerHeight(inner, header, footer) + 'px'
},
// Preserves the scroll position when OSK appears or the posting form changes its height.
handleResize (opts = {}) {
const { expand = false, delayed = false } = opts
@ -190,17 +178,14 @@ const Chat = {
}
this.$nextTick(() => {
this.updateScrollableContainerHeight()
const { offsetHeight = undefined } = this.lastScrollPosition
this.lastScrollPosition = getScrollPosition(this.$refs.scrollable)
this.lastScrollPosition = getScrollPosition(scroller())
const diff = this.lastScrollPosition.offsetHeight - offsetHeight
if (diff < 0 || (!this.bottomedOut() && expand)) {
this.$nextTick(() => {
this.updateScrollableContainerHeight()
this.$refs.scrollable.scrollTo({
top: this.$refs.scrollable.scrollTop - diff,
scroller().scrollTo({
top: scroller().scrollTop - diff,
left: 0
})
})
@ -209,7 +194,7 @@ const Chat = {
},
scrollDown (options = {}) {
const { behavior = 'auto', forceRead = false } = options
const scrollable = this.$refs.scrollable
const scrollable = scroller()
if (!scrollable) { return }
this.$nextTick(() => {
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
@ -228,10 +213,10 @@ const Chat = {
})
},
bottomedOut (offset) {
return isBottomedOut(this.$refs.scrollable, offset)
return isBottomedOut(scroller(), offset)
},
reachedTop () {
const scrollable = this.$refs.scrollable
const scrollable = scroller()
return scrollable && scrollable.scrollTop <= 0
},
cullOlderCheck () {
@ -263,8 +248,8 @@ const Chat = {
}
}, 200),
handleScrollUp (positionBeforeLoading) {
const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
this.$refs.scrollable.scrollTo({
const positionAfterLoading = getScrollPosition(scroller())
scroller().scrollTo({
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
left: 0
})
@ -285,22 +270,18 @@ const Chat = {
chatService.clear(chatMessageService)
}
const positionBeforeUpdate = getScrollPosition(this.$refs.scrollable)
const positionBeforeUpdate = getScrollPosition(scroller())
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
this.$nextTick(() => {
if (fetchOlderMessages) {
this.handleScrollUp(positionBeforeUpdate)
}
if (isFirstFetch) {
this.updateScrollableContainerHeight()
}
// In vertical screens, the first batch of fetched messages may not always take the
// full height of the scrollable container.
// If this is the case, we want to fetch the messages until the scrollable container
// is fully populated so that the user has the ability to scroll up and load the history.
if (!isScrollable(this.$refs.scrollable) && messages.length > 0) {
if (!isScrollable(scroller()) && messages.length > 0) {
this.fetchChat({ maxId: this.currentChatMessageService.minId })
}
})
@ -336,9 +317,6 @@ const Chat = {
this.handleResize()
// When the posting form size changes because of a media attachment, we need an extra resize
// to account for the potential delay in the DOM update.
setTimeout(() => {
this.updateScrollableContainerHeight()
}, SAFE_RESIZE_TIME_OFFSET)
this.scrollDown({ forceRead: true })
})
},

View file

@ -1,19 +1,12 @@
.chat-view {
display: flex;
height: calc(100vh - 60px);
width: 100%;
.chat-title {
// prevents chat header jumping on when the user avatar loads
height: 28px;
}
height: 100%;
.chat-view-inner {
height: auto;
width: 100%;
overflow: visible;
display: flex;
margin: 0.5em 0.5em 0 0.5em;
}
.chat-view-body {
@ -32,11 +25,9 @@
}
}
.scrollable-message-list {
.message-list {
padding: 0 0.8em;
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
@ -44,12 +35,14 @@
.footer {
position: sticky;
bottom: 0;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
z-index: 10;
}
.chat-view-heading {
align-items: center;
justify-content: space-between;
top: 50px;
display: flex;
z-index: 2;
position: sticky;

View file

@ -8,7 +8,7 @@
>
<div
ref="header"
class="panel-heading chat-view-heading mobile-hidden"
class="panel-heading -sticky chat-view-heading mobile-hidden"
>
<a
class="go-back-button"
@ -27,10 +27,8 @@
</div>
</div>
<div
ref="scrollable"
class="scrollable-message-list"
class="message-list"
:style="{ height: scrollableContainerHeight }"
@scroll="handleScroll"
>
<template v-if="!errorLoadingChat">
<ChatMessage

View file

@ -6,7 +6,7 @@
v-else
class="chat-list panel panel-default"
>
<div class="panel-heading">
<div class="panel-heading -sticky">
<span class="title">
{{ $t("chats.chats") }}
</span>

View file

@ -15,7 +15,7 @@
</router-link>
<RichContent
class="username"
:title="'@'+user.screen_name_ui"
:title="'@'+(user && user.screen_name_ui)"
:html="htmlTitle"
:emoji="user.emoji"
/>

View file

@ -1,9 +1,7 @@
@import '../../_variables.scss';
.DesktopNav {
height: 50px;
width: 100%;
position: fixed;
a {
color: var(--topBarLink, $fallback--link);

View file

@ -78,7 +78,8 @@ const MobileNav = {
this.$store.dispatch('logout')
},
markNotificationsAsSeen () {
this.$refs.notifications.markAsSeen()
// this.$refs.notifications.markAsSeen()
this.$store.dispatch('markNotificationsAsSeen')
},
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
if (scrollTop + clientHeight >= scrollHeight) {

View file

@ -69,12 +69,9 @@
</div>
<div
class="mobile-notifications"
id="mobile-notifications"
@scroll="onScroll"
>
<Notifications
ref="notifications"
:no-heading="true"
/>
</div>
</div>
<SideDrawer
@ -92,12 +89,10 @@
.MobileNav {
.mobile-nav {
display: grid;
line-height: 50px;
height: 50px;
line-height: var(--navbar-height);
grid-template-rows: 50px;
grid-template-columns: 2fr auto;
width: 100%;
position: fixed;
box-sizing: border-box;
}
@ -182,7 +177,7 @@
.mobile-notifications {
margin-top: 50px;
width: 100vw;
height: calc(100vh - 50px);
height: calc(100vh - var(--navbar-height));
overflow-x: hidden;
overflow-y: scroll;

View file

@ -23,8 +23,6 @@ const Notifications = {
NotificationFilters
},
props: {
// Disables display of panel header
noHeading: Boolean,
// Disables panel styles, unread mark, potentially other notification-related actions
// meant for "Interactions" timeline
minimalMode: Boolean,
@ -65,6 +63,19 @@ const Notifications = {
loading () {
return this.$store.state.statuses.notifications.loading
},
noHeading () {
const { layoutType } = this.$store.state.interface
console.log(layoutType)
return layoutType === 'mobile'
},
teleportTarget () {
const { layoutType } = this.$store.state.interface
const map = {
wide: '#notifs-column',
mobile: '#mobile-notifications'
}
return map[layoutType] || '#notifs-sidebar'
},
notificationsToDisplay () {
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
},

View file

@ -1,69 +1,71 @@
<template>
<div
:class="{ minimal: minimalMode }"
class="Notifications"
>
<div :class="mainClass">
<div
v-if="!noHeading"
class="panel-heading"
>
<div class="title">
{{ $t('notifications.notifications') }}
<span
v-if="unseenCount"
class="badge badge-notification unseen-count"
>{{ unseenCount }}</span>
</div>
<button
v-if="unseenCount"
class="button-default read-button"
@click.prevent="markAsSeen"
>
{{ $t('notifications.read') }}
</button>
<NotificationFilters />
</div>
<div class="panel-body">
<teleport :to="teleportTarget">
<div
:class="{ minimal: minimalMode }"
class="Notifications"
>
<div :class="mainClass">
<div
v-for="notification in notificationsToDisplay"
:key="notification.id"
class="notification"
:class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"
v-if="!noHeading"
class="notifications-heading panel-heading -sticky"
>
<div class="notification-overlay" />
<notification :notification="notification" />
</div>
</div>
<div class="panel-footer notifications-footer">
<div
v-if="bottomedOut"
class="new-status-notification text-center faint"
>
{{ $t('notifications.no_more_notifications') }}
</div>
<button
v-else-if="!loading"
class="button-unstyled -link -fullwidth"
@click.prevent="fetchOlderNotifications()"
>
<div class="new-status-notification text-center">
{{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
<div class="title">
{{ $t('notifications.notifications') }}
<span
v-if="unseenCount"
class="badge badge-notification unseen-count"
>{{ unseenCount }}</span>
</div>
<button
v-if="unseenCount"
class="button-default read-button"
@click.prevent="markAsSeen"
>
{{ $t('notifications.read') }}
</button>
<NotificationFilters />
</div>
<div class="panel-body">
<div
v-for="notification in notificationsToDisplay"
:key="notification.id"
class="notification"
:class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"
>
<div class="notification-overlay" />
<notification :notification="notification" />
</div>
</div>
<div class="panel-footer notifications-footer">
<div
v-if="bottomedOut"
class="new-status-notification text-center faint"
>
{{ $t('notifications.no_more_notifications') }}
</div>
<button
v-else-if="!loading"
class="button-unstyled -link -fullwidth"
@click.prevent="fetchOlderNotifications()"
>
<div class="new-status-notification text-center">
{{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
</div>
</button>
<div
v-else
class="new-status-notification text-center"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div>
</button>
<div
v-else
class="new-status-notification text-center"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div>
</div>
</div>
</div>
</teleport>
</template>
<script src="./notifications.js"></script>

View file

@ -91,6 +91,9 @@
flex-direction: column;
margin-top: 0.6em;
max-width: 18rem;
> * {
min-width: 0;
}
}
.form-group {

View file

@ -149,5 +149,30 @@
}
}
.button-default.dropdown-item {
&,
i[class*=icon-] {
color: $fallback--text;
color: var(--btnText, $fallback--text);
}
&:active {
background-color: $fallback--lightBg;
background-color: var(--selectedMenuPopover, $fallback--lightBg);
color: $fallback--link;
color: var(--selectedMenuPopoverText, $fallback--link);
}
&:disabled {
color: $fallback--text;
color: var(--btnDisabledText, $fallback--text);
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledText, $fallback--text);
}
}
}
</style>

View file

@ -271,7 +271,10 @@ $validations-cRed: #f04124;
.container {
display: flex;
flex-direction: row;
//margin-bottom: 1em;
> * {
min-width: 0;
}
}
.terms-of-service {

View file

@ -2,6 +2,18 @@
.settings-modal {
overflow: hidden;
.setting-list,
.option-list {
list-style-type: none;
padding-left: 2em;
li {
margin-bottom: 0.5em;
}
.suboptions {
margin-top: 0.3em
}
}
&.peek {
.settings-modal-panel {
/* Explanation:

View file

@ -98,7 +98,7 @@
.icon {
color: $fallback--text;
color: var(--text, $fallback--text);
color: var(--panelText, $fallback--text);
margin-right: 0.5em;
}

View file

@ -49,7 +49,7 @@ const SideDrawer = {
currentUser () {
return this.$store.state.users.currentUser
},
shout () { return this.$store.state.shout.channel.state === 'joined' },
shout () { return this.$store.state.shout.joined },
unseenNotifications () {
return unseenNotificationsFromStore(this.$store)
},

View file

@ -42,6 +42,10 @@
display: flex;
padding: var(--status-margin, $status-margin);
> * {
min-width: 0;
}
&.-repeat {
padding-top: 0;
}

View file

@ -64,7 +64,7 @@ const Timeline = {
if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
return {
root: rootClasses,
header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []),
header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading', '-sticky'] : []),
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
}
@ -89,7 +89,7 @@ const Timeline = {
const credentials = store.state.users.currentUser.credentials
const showImmediately = this.timeline.visibleStatuses.length === 0
window.addEventListener('scroll', this.handleScroll)
document.getElementById('content').addEventListener('scroll', this.handleScroll)
if (store.state.api.fetchers[this.timelineName]) { return false }
@ -111,7 +111,7 @@ const Timeline = {
setTimeout(this.determineVisibleStatuses, 250)
},
unmounted () {
window.removeEventListener('scroll', this.handleScroll)
document.getElementById('content').removeEventListener('scroll', this.handleScroll)
window.removeEventListener('keydown', this.handleShortKey)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })

View file

@ -13,7 +13,6 @@
max-width: 100%;
flex-wrap: nowrap;
align-items: center;
position: relative;
.loadmore-button {
flex-shrink: 0;

View file

@ -8,7 +8,7 @@
:style="style"
class="background-image"
/>
<div class="panel-heading">
<div class="panel-heading -flexible-height">
<div class="user-info">
<div class="container">
<a
@ -289,6 +289,7 @@
.user-card {
position: relative;
z-index: 1;
&:hover {
--_still-image-img-visibility: visible;
@ -331,6 +332,7 @@
border-top-left-radius: calc(var(--panelRadius) - 1px);
border-top-right-radius: calc(var(--panelRadius) - 1px);
background-color: var(--profileBg);
z-index: -2;
&.hide-bio {
mask-size: 100% 40px;
@ -385,11 +387,16 @@
padding: 0 26px;
.container {
min-width: 0;
padding: 16px 0 6px;
display: flex;
align-items: flex-start;
max-height: 56px;
> * {
min-width: 0;
}
.Avatar {
--_avatarShadowBox: var(--avatarShadow);
--_avatarShadowFilter: var(--avatarShadowFilter);

View file

@ -32,7 +32,7 @@ const loaders = {
pt: () => import('./pt.json'),
ro: () => import('./ro.json'),
ru: () => import('./ru.json'),
// te: () => import('./te.json'), // buggy
te: () => import('./te.json'),
uk: () => import('./uk.json'),
zh: () => import('./zh.json'),
zh_Hant: () => import('./zh_Hant.json')

View file

@ -49,7 +49,7 @@
"notifications.repeated_you": "మీ స్థితిని పునరావృతం చేసారు",
"notifications.no_more_notifications": "ఇక నోటిఫికేషన్లు లేవు",
"post_status.new_status": "క్రొత్త స్థితిని పోస్ట్ చేయండి",
"post_status.account_not_locked_warning": "మీ ఖాతా {} కాదు. ఎవరైనా మిమ్మల్ని అనుసరించి అనుచరులకు మాత్రమే ఉద్దేశించిన పోస్టులను చూడవచ్చు.",
"post_status.account_not_locked_warning": "మీ ఖాతా {0} కాదు. ఎవరైనా మిమ్మల్ని అనుసరించి అనుచరులకు మాత్రమే ఉద్దేశించిన పోస్టులను చూడవచ్చు.",
"post_status.account_not_locked_warning_link": "తాళం వేయబడినది",
"post_status.attachments_sensitive": "జోడింపులను సున్నితమైనవిగా గుర్తించండి",
"post_status.content_type.text/plain": "సాధారణ అక్షరాలు",

View file

@ -13,7 +13,7 @@ const defaultState = {
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
)
},
mobileLayout: false,
layoutType: 'normal',
globalNotices: [],
layoutHeight: 0,
lastTimeline: null
@ -36,8 +36,8 @@ const interfaceMod = {
setNotificationPermission (state, permission) {
state.notificationPermission = permission
},
setMobileLayout (state, value) {
state.mobileLayout = value
setLayoutType (state, value) {
state.layoutType = value
},
closeSettingsModal (state) {
state.settingsModalState = 'hidden'
@ -86,8 +86,8 @@ const interfaceMod = {
setNotificationPermission ({ commit }, permission) {
commit('setNotificationPermission', permission)
},
setMobileLayout ({ commit }, value) {
commit('setMobileLayout', value)
setLayoutType ({ commit }, value) {
commit('setLayoutType', value)
},
closeSettingsModal ({ commit }) {
commit('closeSettingsModal')

View file

@ -1,7 +1,8 @@
const shout = {
state: {
messages: [],
channel: { state: '' }
channel: { state: '' },
joined: false
},
mutations: {
setChannel (state, channel) {
@ -13,11 +14,23 @@ const shout = {
},
setMessages (state, messages) {
state.messages = messages.slice(-19, 20)
},
setJoined (state, joined) {
state.joined = joined
}
},
actions: {
initializeShout (store, socket) {
const channel = socket.channel('chat:public')
channel.joinPush.receive('ok', () => {
store.commit('setJoined', true)
})
channel.onClose(() => {
store.commit('setJoined', false)
})
channel.onError(() => {
store.commit('setJoined', false)
})
channel.on('new_msg', (msg) => {
store.commit('addMessage', msg)
})

204
src/panel.scss Normal file
View file

@ -0,0 +1,204 @@
.panel {
position: relative;
display: flex;
flex-direction: column;
z-index: 0;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
&::after,
& {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--panelShadow);
pointer-events: none;
}
}
.panel-body:empty::before {
content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations
display: block;
margin: 1em;
text-align: center;
}
.panel-heading {
position: relative;
box-sizing: border-box;
display: flex;
flex: none;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
background-size: cover;
padding: 0.6em 0.6em;
text-align: left;
line-height: 28px;
color: var(--panelText);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
align-items: baseline;
height: var(--panelHeadingHeight);
z-index: -2;
--panelHeadingHeight: 45px;
&.-flexible-height {
--panelHeadingHeight: auto;
&::after,
&::before {
display: none;
}
}
&.-sticky {
position: sticky;
top: 0;
z-index: 2;
}
&::after,
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
pointer-events: none;
}
&::after {
background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
z-index: -2;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
box-shadow: var(--panelHeaderShadow);
}
&::before {
bottom: -20px;
z-index: -1;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
mask: linear-gradient(to bottom, white var(--panelHeadingHeight), transparent var(--panelHeadingHeight));
}
.title {
flex: 1 0 auto;
font-size: 1.3em;
}
.faint {
background-color: transparent;
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
.faint-link {
color: $fallback--faint;
color: var(--faintLink, $fallback--faint);
}
.alert {
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
}
.button-default,
.alert {
// height: 100%;
line-height: 21px;
min-height: 0;
box-sizing: border-box;
margin: 0;
margin-left: 0.5em;
min-width: 1px;
align-self: stretch;
}
.button-default {
flex-shrink: 0;
&,
i[class*=icon-] {
color: $fallback--text;
color: var(--btnPanelText, $fallback--text);
}
&:active {
background-color: $fallback--fg;
background-color: var(--btnPressedPanel, $fallback--fg);
color: $fallback--text;
color: var(--btnPressedPanelText, $fallback--text);
}
&:disabled {
color: $fallback--text;
color: var(--btnDisabledPanelText, $fallback--text);
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledPanelText, $fallback--text);
}
}
a,
.-link {
color: $fallback--link;
color: var(--panelLink, $fallback--link);
}
}
.panel-heading.stub {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
/* TODO Should remove timeline-footer from here when we refactor panels into
* separate component and utilize slots
*/
.panel-footer {
display: flex;
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
flex: none;
padding: 0.6em 0.6em;
text-align: left;
line-height: 28px;
align-items: baseline;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--border, $fallback--border);
.faint {
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
a,
.-link {
color: $fallback--link;
color: var(--panelLink, $fallback--link);
}
}
.panel-body > p {
line-height: 18px;
padding: 1em;
margin: 0;
}