diff options
24 files changed, 650 insertions, 600 deletions
@@ -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, @@ -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) } diff --git a/src/App.scss b/src/App.scss index 180c0daf..1867a54d 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,75 +1,199 @@ +// stylelint-disable rscss/class-format @import './_variables.scss'; -#app { - min-height: 100vh; - max-width: 100%; +html { + font-size: 14px; overflow: hidden; + max-height: 100vh; } -.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; +body { + 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); + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + &.hidden { + display: none; + } } -i[class^='icon-'] { - user-select: none; +a { + text-decoration: none; + color: $fallback--link; + color: var(--link, $fallback--link); } h4 { margin: 0; } -#content { +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; - padding-top: 60px; - margin: auto; + height: var(--navbar-height); +} + +#app-loaded { min-height: 100vh; - max-width: 980px; - align-content: flex-start; + min-width: 100vw; + overflow: hidden; + + --navbar-height: 50px; } -.underlay { - background-color: rgba(0,0,0,0.15); - background-color: var(--underlay, rgba(0,0,0,0.15)); +#content { + overscroll-behavior-y: none; + overflow-y: auto; + position: sticky; } -.text-center { - text-align: center; +#sidebar { + grid-area: sidebar; } -html { - font-size: 14px; +#notifs-column { + grid-area: notifs; } -body { - overscroll-behavior-y: none; - 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; +#main-scroller { + grid-area: content; +} - &.hidden { - display: none; +.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; + } } } -a { - text-decoration: none; - color: $fallback--link; - color: var(--link, $fallback--link); +#content, +.column.-scrollable { + &::-webkit-scrollbar { + display: block; + width: 0; + } +} + +.text-center { + text-align: center; } .button-default { @@ -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; -} +@keyframes shakeError { + 0% { + transform: translateX(0); + } -.chat-layout { - // Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens). - overflow: hidden; - height: 100%; + 15% { + transform: translateX(0.375rem); + } - // Get rid of scrollbar on body as scrolling happens on different element - body { - overflow: hidden; + 30% { + transform: translateX(-0.375rem); } - // 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%; - } + 45% { + transform: translateX(0.375rem); + } - #app { - height: 100%; - overflow: hidden; - min-height: auto; - } + 60% { + transform: translateX(-0.375rem); + } - #app_bg_wrapper { - overflow: hidden; - } + 75% { + transform: translateX(0.375rem); + } - .main { - overflow: hidden; - height: 100%; - } + 90% { + transform: translateX(-0.375rem); + } - #content { - padding-top: 0; - height: 100%; - overflow: visible; - } + 100% { + transform: translateX(0); } } + +// Vue transitions +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.2s; +} + +.fade-enter-from, +.fade-leave-active { + opacity: 0; +} diff --git a/src/App.vue b/src/App.vue index b18b3308..70084538 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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 diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 76832708..ce96141d 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -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() diff --git a/src/components/about/about.vue b/src/components/about/about.vue index 518f6184..5d5d6479 100644 --- a/src/components/about/about.vue +++ b/src/components/about/about.vue @@ -1,5 +1,5 @@ <template> - <div class="sidebar"> + <div class="column-inner"> <instance-specific-panel v-if="showInstanceSpecificPanel" /> <staff-panel /> <terms-of-service-panel /> diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index aef11712..5d2c1d09 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -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 }) }) }, diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss index 3a26686c..51bb3ea6 100644 --- a/src/components/chat/chat.scss +++ b/src/components/chat/chat.scss @@ -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; diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue index 493c5d5a..3b0129fa 100644 --- a/src/components/chat/chat.vue +++ b/src/components/chat/chat.vue @@ -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 diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue index f98b7ed2..58e8d0b3 100644 --- a/src/components/chat_list/chat_list.vue +++ b/src/components/chat_list/chat_list.vue @@ -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> diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue index a92028e8..06ad129d 100644 --- a/src/components/chat_title/chat_title.vue +++ b/src/components/chat_title/chat_title.vue @@ -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" /> diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss index 2d468588..1e731c70 100644 --- a/src/components/desktop_nav/desktop_nav.scss +++ b/src/components/desktop_nav/desktop_nav.scss @@ -1,9 +1,7 @@ @import '../../_variables.scss'; .DesktopNav { - height: 50px; width: 100%; - position: fixed; a { color: var(--topBarLink, $fallback--link); diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index 9e736cfb..877d52a9 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -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) { diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index 0f0ea457..6c810bba 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -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; diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index c8f1ebcb..2bb8a1d1 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -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) }, diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index 2ce5d56f..f50d3d38 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -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="{"unseen": !minimalMode && !notification.seen}" + v-if="!noHeading" + class="notifications-heading panel-heading -sticky" > - <div class="notification-overlay" /> - <notification :notification="notification" /> + <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> - <div class="panel-footer notifications-footer"> - <div - v-if="bottomedOut" - class="new-status-notification text-center faint" - > - {{ $t('notifications.no_more_notifications') }} + <div class="panel-body"> + <div + v-for="notification in notificationsToDisplay" + :key="notification.id" + class="notification" + :class="{"unseen": !minimalMode && !notification.seen}" + > + <div class="notification-overlay" /> + <notification :notification="notification" /> + </div> </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="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> diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue index 3ffa5425..e0579568 100644 --- a/src/components/password_reset/password_reset.vue +++ b/src/components/password_reset/password_reset.vue @@ -91,6 +91,9 @@ flex-direction: column; margin-top: 0.6em; max-width: 18rem; + > * { + min-width: 0; + } } .form-group { diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index 3d409109..1e735c69 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -271,7 +271,10 @@ $validations-cRed: #f04124; .container { display: flex; flex-direction: row; - //margin-bottom: 1em; + + > * { + min-width: 0; + } } .terms-of-service { diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss index 2f7649a9..f3738a8c 100644 --- a/src/components/settings_modal/settings_modal.scss +++ b/src/components/settings_modal/settings_modal.scss @@ -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: diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 5ed43e0d..e38e2a53 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -42,6 +42,10 @@ display: flex; padding: var(--status-margin, $status-margin); + > * { + min-width: 0; + } + &.-repeat { padding-top: 0; } diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 8ec5d1e5..9056319b 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -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 }) diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss index 2c5a67e2..c10375d4 100644 --- a/src/components/timeline/timeline.scss +++ b/src/components/timeline/timeline.scss @@ -13,7 +13,6 @@ max-width: 100%; flex-wrap: nowrap; align-items: center; - position: relative; .loadmore-button { flex-shrink: 0; diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 14b4643a..43a660e1 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -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); diff --git a/src/modules/interface.js b/src/modules/interface.js index 17277331..9df72b88 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -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') diff --git a/src/panel.scss b/src/panel.scss new file mode 100644 index 00000000..bb7af43a --- /dev/null +++ b/src/panel.scss @@ -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; +} |
