Custom backgrounds for lists (#144)

Make backgrounds list responsive

Show initial collection of backgrounds

Remove test data

Fix "backgroundInformation is null" when navigating

Fix kanban height

Remove debug log

Move list title to top header

Add styling for title in top header

Set the current list (and background) when loading settings

Only load the background if it changed

Make task detail view look good again

Fix bottom spacing

Make list and table view look good again

Make pages with background at least 100vh

Fix kanban height

Make extra buttons look good again

Move list title and view-switcher in one row

Add styling for backgrounds

Set background globally

Add getting list background and putting it in vuex

Add setting list background

Move list background setting to seperate list

Add search timeout to not search on every keypress

Add getting thumbnails through api

Add basic search for unsplash backgrounds

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/144
This commit is contained in:
konrad 2020-05-31 19:17:10 +00:00
parent 7fc0756b01
commit 4b3f92ae34
27 changed files with 513 additions and 70 deletions

View file

@ -3,13 +3,27 @@
<div v-if="online"> <div v-if="online">
<!-- This is a workaround to get the sw to "see" the to-be-cached version of the offline background image --> <!-- This is a workaround to get the sw to "see" the to-be-cached version of the offline background image -->
<div class="offline" style="height: 0;width: 0;"></div> <div class="offline" style="height: 0;width: 0;"></div>
<nav class="navbar main-theme is-fixed-top" role="navigation" aria-label="main navigation" <nav
v-if="userAuthenticated && (userInfo && userInfo.type === authTypes.USER)"> class="navbar main-theme is-fixed-top"
:class="{'has-background': background}"
role="navigation"
aria-label="main navigation"
v-if="userAuthenticated && (userInfo && userInfo.type === authTypes.USER)">
<div class="navbar-brand"> <div class="navbar-brand">
<router-link :to="{name: 'home'}" class="navbar-item logo"> <router-link :to="{name: 'home'}" class="navbar-item logo">
<img src="/images/logo-full.svg" alt="Vikunja"/> <img src="/images/logo-full.svg" alt="Vikunja"/>
</router-link> </router-link>
</div> </div>
<div class="list-title" v-if="currentList.id">
<h1
class="title"
:style="{ 'opacity': currentList.title === '' ? '0': '1' }">
{{ currentList.title === '' ? 'Loading...': currentList.title}}
</h1>
<router-link :to="{ name: 'editList', params: { id: currentList.id } }" class="icon">
<icon icon="cog" size="2x"/>
</router-link>
</div>
<div class="navbar-end"> <div class="navbar-end">
<div v-if="updateAvailable" class="update-notification"> <div v-if="updateAvailable" class="update-notification">
<p>There is an update for Vikunja available!</p> <p>There is an update for Vikunja available!</p>
@ -49,7 +63,11 @@
<a @click="mobileMenuActive = false" class="mobilemenu-hide-button" v-if="mobileMenuActive"> <a @click="mobileMenuActive = false" class="mobilemenu-hide-button" v-if="mobileMenuActive">
<icon icon="times"></icon> <icon icon="times"></icon>
</a> </a>
<div class="app-container"> <div
class="app-container"
:class="{'has-background': background}"
:style="{'background-image': `url(${background})`}"
>
<div class="namespace-container" :class="{'is-active': mobileMenuActive}"> <div class="namespace-container" :class="{'is-active': mobileMenuActive}">
<div class="menu top-menu"> <div class="menu top-menu">
<router-link :to="{name: 'home'}" class="logo"> <router-link :to="{name: 'home'}" class="logo">
@ -141,7 +159,7 @@
<div class="more-container" :key="n.id + 'child'"> <div class="more-container" :key="n.id + 'child'">
<ul class="menu-list can-be-hidden" > <ul class="menu-list can-be-hidden" >
<li v-for="l in n.lists" :key="l.id"> <li v-for="l in n.lists" :key="l.id">
<router-link :to="{ name: 'list.index', params: { listId: l.id} }" :class="{'router-link-exact-active': currentList === l.id}"> <router-link :to="{ name: 'list.index', params: { listId: l.id} }" :class="{'router-link-exact-active': currentList.id === l.id}">
<span class="name"> <span class="name">
<span class="color-bubble" v-if="l.hexColor !== ''" :style="{ backgroundColor: l.hexColor }"></span> <span class="color-bubble" v-if="l.hexColor !== ''" :style="{ backgroundColor: l.hexColor }"></span>
{{l.title}} {{l.title}}
@ -316,6 +334,7 @@
return state.namespaces.namespaces.filter(n => this.showArchived ? true : !n.isArchived) return state.namespaces.namespaces.filter(n => this.showArchived ? true : !n.isArchived)
}, },
currentList: CURRENT_LIST, currentList: CURRENT_LIST,
background: 'background',
}), }),
methods: { methods: {
logout() { logout() {
@ -351,7 +370,7 @@
this.$route.name === 'migrate.wunderlist' || this.$route.name === 'migrate.wunderlist' ||
this.$route.name === 'userSettings' this.$route.name === 'userSettings'
) { ) {
this.$store.commit(CURRENT_LIST, 0) this.$store.commit(CURRENT_LIST, {})
} }
}, },
showRefreshUI(e) { showRefreshUI(e) {

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="loader-container" :class="{ 'is-loading': listService.loading}"> <div class="loader-container edit-list" :class="{ 'is-loading': listService.loading}">
<div class="notification is-warning" v-if="list.isArchived"> <div class="notification is-warning" v-if="list.isArchived">
This list is archived. This list is archived.
It is not possible to create new or edit tasks or it. It is not possible to create new or edit tasks or it.
@ -104,6 +104,8 @@
</div> </div>
</div> </div>
<background :list-id="$route.params.id"/>
<component <component
:is="manageUsersComponent" :is="manageUsersComponent"
:id="list.id" :id="list.id"
@ -141,6 +143,8 @@
import ListModel from '../../models/list' import ListModel from '../../models/list'
import ListService from '../../services/list' import ListService from '../../services/list'
import Fancycheckbox from '../global/fancycheckbox' import Fancycheckbox from '../global/fancycheckbox'
import Background from './settings/background'
import {CURRENT_LIST} from '../../store/mutation-types'
export default { export default {
name: "EditList", name: "EditList",
@ -156,6 +160,7 @@
} }
}, },
components: { components: {
Background,
Fancycheckbox, Fancycheckbox,
LinkSharing, LinkSharing,
manageSharing, manageSharing,
@ -183,6 +188,7 @@
this.listService.get(list) this.listService.get(list)
.then(r => { .then(r => {
this.$set(this, 'list', r) this.$set(this, 'list', r)
this.$store.commit(CURRENT_LIST, r)
// This will trigger the dynamic loading of components once we actually have all the data to pass to them // This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'manageSharing' this.manageTeamsComponent = 'manageSharing'
this.manageUsersComponent = 'manageSharing' this.manageUsersComponent = 'manageSharing'

View file

@ -1,20 +1,33 @@
<template> <template>
<div class="loader-container" :class="{ 'is-loading': listService.loading}"> <div
<div class="content"> class="loader-container"
<router-link :to="{ name: 'editList', params: { id: list.id } }" class="icon settings is-medium"> :class="{ 'is-loading': listService.loading}"
<icon icon="cog" size="2x"/> >
<div class="switch-view">
<router-link
:to="{ name: 'list.list', params: { listId: listId } }"
:class="{'is-active': $route.name === 'list.list'}">
List
</router-link> </router-link>
<h1 :style="{ 'opacity': list.title === '' ? '0': '1' }">{{ list.title === '' ? 'Loading...': list.title}}</h1> <router-link
<div class="notification is-warning" v-if="list.isArchived"> :to="{ name: 'list.gantt', params: { listId: listId } }"
This list is archived. :class="{'is-active': $route.name === 'list.gantt'}">
It is not possible to create new or edit tasks or it. Gantt
</div> </router-link>
<div class="switch-view"> <router-link
<router-link :to="{ name: 'list.list', params: { listId: listId } }" :class="{'is-active': $route.name === 'list.list'}">List</router-link> :to="{ name: 'list.table', params: { listId: listId } }"
<router-link :to="{ name: 'list.gantt', params: { listId: listId } }" :class="{'is-active': $route.name === 'list.gantt'}">Gantt</router-link> :class="{'is-active': $route.name === 'list.table'}">
<router-link :to="{ name: 'list.table', params: { listId: listId } }" :class="{'is-active': $route.name === 'list.table'}">Table</router-link> Table
<router-link :to="{ name: 'list.kanban', params: { listId: listId } }" :class="{'is-active': $route.name === 'list.kanban'}">Kanban</router-link> </router-link>
</div> <router-link
:to="{ name: 'list.kanban', params: { listId: listId } }"
:class="{'is-active': $route.name === 'list.kanban'}">
Kanban
</router-link>
</div>
<div class="notification is-warning" v-if="list.isArchived">
This list is archived.
It is not possible to create new or edit tasks or it.
</div> </div>
<router-view/> <router-view/>
@ -53,12 +66,15 @@
listId() { listId() {
return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId
}, },
background() {
return this.$store.state.background
},
}, },
methods: { methods: {
loadList() { loadList() {
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently // Don't load the list if we either already loaded it or aren't dealing with a list at all currently
if(this.$route.params.listId === this.listLoaded || typeof this.$route.params.listId === 'undefined') { if (this.$route.params.listId === this.listLoaded || typeof this.$route.params.listId === 'undefined') {
return return
} }
@ -76,13 +92,12 @@
return return
} }
this.$store.commit(CURRENT_LIST, Number(this.$route.params.listId))
// We create an extra list object instead of creating it in this.list because that would trigger a ui update which would result in bad ux. // We create an extra list object instead of creating it in this.list because that would trigger a ui update which would result in bad ux.
let list = new ListModel({id: this.$route.params.listId}) let list = new ListModel({id: this.$route.params.listId})
this.listService.get(list) this.listService.get(list)
.then(r => { .then(r => {
this.$set(this, 'list', r) this.$set(this, 'list', r)
this.$store.commit(CURRENT_LIST, r)
}) })
.catch(e => { .catch(e => {
this.error(e, this) this.error(e, this)

View file

@ -0,0 +1,132 @@
<template>
<div
v-if="unsplashBackgroundEnabled"
class="card list-background-setting loader-container"
:class="{ 'is-loading': backgroundService.loading}">
<header class="card-header">
<p class="card-header-title">
Set list background
</p>
</header>
<div class="card-content">
<div class="content">
<input
type="text"
placeholder="Search for a background..."
class="input is-expanded"
v-model="backgroundSearchTerm"
@keyup="() => newBackgroundSearch()"
:class="{'is-loading': backgroundService.loading}"
/>
<div class="image-search-result">
<a
@click="() => setBackground(im.id)"
class="image"
v-for="im in backgroundSearchResult"
:style="{'background-image': `url(${backgroundThumbs[im.id]})`}"
:key="im.id">
<a class="info" :href="`https://unsplash.com/@${im.info.author}`">
{{ im.info.authorName }}
</a>
</a>
</div>
<a
v-if="backgroundSearchResult.length > 0"
class="button is-primary is-centered is-load-more-button is-outlined noshadow"
@click="() => searchBackgrounds(currentPage + 1)"
:disabled="backgroundService.loading"
>
<template v-if="backgroundService.loading">
Loading...
</template>
<template v-else>
Load more photos
</template>
</a>
</div>
</div>
</div>
</template>
<script>
import BackgroundUnsplashService from '../../../services/backgroundUnsplash'
import {CURRENT_LIST} from '../../../store/mutation-types'
export default {
name: 'background',
data() {
return {
backgroundSearchTerm: '',
backgroundSearchResult: [],
backgroundService: null,
backgroundThumbs: {},
currentPage: 1,
backgroundSearchTimeout: null,
}
},
props: {
listId: {
default: 0,
required: true,
}
},
computed: {
unsplashBackgroundEnabled() {
return this.$store.state.config.enabledBackgroundProviders.includes('unsplash')
},
},
created() {
this.backgroundService = new BackgroundUnsplashService()
// Show the default collection of backgrounds
this.newBackgroundSearch()
},
methods: {
newBackgroundSearch() {
// This is an extra method to reset a few things when searching to not break loading more photos.
this.$set(this, 'backgroundSearchResult', [])
this.$set(this, 'backgroundThumbs', {})
this.searchBackgrounds()
},
searchBackgrounds(page = 1) {
if(this.backgroundSearchTimeout !== null) {
clearTimeout(this.backgroundSearchTimeout)
}
// We're using the timeout to not search on every keypress but with a 300ms delay.
// If another key is pressed within these 300ms, the last search request is dropped and a new one is scheduled.
this.backgroundSearchTimeout = setTimeout(() => {
this.currentPage = page
this.backgroundService.getAll({}, {s: this.backgroundSearchTerm, p: page})
.then(r => {
this.backgroundSearchResult = this.backgroundSearchResult.concat(r)
r.forEach(b => {
this.backgroundService.thumb(b)
.then(t => {
this.$set(this.backgroundThumbs, b.id, t)
})
})
})
.catch(e => {
this.error(e, this)
})
}, 300)
},
setBackground(backgroundId) {
// Don't set a background if we're in the process of setting one
if (this.backgroundService.loading) {
return
}
this.backgroundService.update({id: backgroundId, listId: this.listId})
.then(l => {
this.$store.commit(CURRENT_LIST, l)
this.success({message: 'The background has been set successfully!'}, this)
})
.catch(e => {
this.error(e, this)
})
},
},
}
</script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="gantt-chart-container">
<div class="gantt-options"> <div class="gantt-options">
<fancycheckbox v-model="showTaskswithoutDates" class="is-block"> <fancycheckbox v-model="showTaskswithoutDates" class="is-block">
Show tasks which don't have dates set Show tasks which don't have dates set

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="loader-container" :class="{ 'is-loading': taskService.loading}"> <div class="loader-container task-view-container" :class="{ 'is-loading': taskService.loading}">
<div class="task-view"> <div class="task-view">
<div class="heading"> <div class="heading">
<h1 class="title task-id" v-if="task.identifier === ''"> <h1 class="title task-id" v-if="task.identifier === ''">

View file

@ -22,8 +22,6 @@ export const getListView = listId => {
localStorage.removeItem('listView') localStorage.removeItem('listView')
} }
console.log('saved list view state', savedListView)
if (!savedListView) { if (!savedListView) {
return 'list.list' return 'list.list'
} }

View file

@ -0,0 +1,12 @@
import AbstractModel from './abstractModel'
export default class BackgroundImageModel extends AbstractModel {
defaults() {
return {
id: 0,
url: '',
thumb: '',
info: {},
}
}
}

View file

@ -34,6 +34,7 @@ export default class ListModel extends AbstractModel {
isArchived: false, isArchived: false,
hexColor: '', hexColor: '',
identifier: '', identifier: '',
backgroundInformation: null,
created: null, created: null,
updated: null, updated: null,

View file

@ -0,0 +1,34 @@
import AbstractService from './abstractService'
import BackgroundImageModel from '../models/backgroundImage'
import ListModel from '../models/list'
export default class BackgroundUnsplashService extends AbstractService {
constructor() {
super({
getAll: '/backgrounds/unsplash/search',
update: '/lists/{listId}/backgrounds/unsplash',
})
}
modelFactory(data) {
return new BackgroundImageModel(data)
}
modelUpdateFactory(data) {
return new ListModel(data)
}
thumb(model) {
return this.http({
url: `/backgrounds/unsplash/images/${model.url}/thumb`,
method: 'GET',
responseType: 'blob',
})
.then(response => {
return window.URL.createObjectURL(new Blob([response.data]))
})
.catch(e => {
return e
})
}
}

View file

@ -37,4 +37,22 @@ export default class ListService extends AbstractService {
list.hexColor = list.hexColor.substring(1, 7) list.hexColor = list.hexColor.substring(1, 7)
return list return list
} }
background(list) {
if (list.background === null) {
return Promise.resolve('')
}
return this.http({
url: `/lists/${list.id}/background`,
method: 'GET',
responseType: 'blob',
})
.then(response => {
return window.URL.createObjectURL(new Blob([response.data]))
})
.catch(e => {
return e
})
}
} }

View file

@ -1,5 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
Vue.use(Vuex) Vue.use(Vuex)
import {CURRENT_LIST, ERROR_MESSAGE, IS_FULLPAGE, LOADING, ONLINE} from './mutation-types' import {CURRENT_LIST, ERROR_MESSAGE, IS_FULLPAGE, LOADING, ONLINE} from './mutation-types'
@ -9,6 +10,7 @@ import namespaces from './modules/namespaces'
import kanban from './modules/kanban' import kanban from './modules/kanban'
import tasks from './modules/tasks' import tasks from './modules/tasks'
import lists from './modules/lists' import lists from './modules/lists'
import ListService from '../services/list'
export const store = new Vuex.Store({ export const store = new Vuex.Store({
modules: { modules: {
@ -25,7 +27,8 @@ export const store = new Vuex.Store({
online: true, online: true,
isFullpage: false, isFullpage: false,
// This is used to highlight the current list in menu for all list related views // This is used to highlight the current list in menu for all list related views
currentList: 0, currentList: {id: 0},
background: '',
}, },
mutations: { mutations: {
[LOADING](state, loading) { [LOADING](state, loading) {
@ -41,6 +44,29 @@ export const store = new Vuex.Store({
state.isFullpage = fullpage state.isFullpage = fullpage
}, },
[CURRENT_LIST](state, currentList) { [CURRENT_LIST](state, currentList) {
// Not sure if this is the right way to do it but hey, it works
if (
currentList.id !== state.currentList.id ||
(
currentList.backgroundInformation &&
currentList.backgroundInformation.unsplashId &&
currentList.backgroundInformation.unsplashId !== state.currentList.backgroundInformation.unsplashId
)
) {
if (currentList.backgroundInformation) {
const listService = new ListService()
listService.background(currentList)
.then(b => {
state.background = b
})
.catch(e => {
console.error('Error getting background image for list', currentList.id, e)
})
} else {
state.background = null
}
}
state.currentList = currentList state.currentList = currentList
}, },
}, },

View file

@ -14,6 +14,7 @@ export default {
availableMigrators: [], availableMigrators: [],
taskAttachmentsEnabled: true, taskAttachmentsEnabled: true,
totpEnabled: true, totpEnabled: true,
enabledBackgroundProviders: [],
}), }),
mutations: { mutations: {
[CONFIG](state, config) { [CONFIG](state, config) {
@ -26,6 +27,7 @@ export default {
state.availableMigrators = config.available_migrators state.availableMigrators = config.available_migrators
state.taskAttachmentsEnabled = config.task_attachments_enabled state.taskAttachmentsEnabled = config.task_attachments_enabled
state.totpEnabled = config.totp_enabled state.totpEnabled = config.totp_enabled
state.enabledBackgroundProviders = config.enabled_background_providers
}, },
}, },
actions: { actions: {

View file

@ -16,3 +16,4 @@
@import 'table-view'; @import 'table-view';
@import 'kanban'; @import 'kanban';
@import 'modal'; @import 'modal';
@import 'list-backgrounds';

View file

@ -1,6 +1,10 @@
$gantt-border: 1px solid $grey-lighter; $gantt-border: 1px solid $grey-lighter;
$gantt-vertical-border-color: lighten($grey, 45); $gantt-vertical-border-color: lighten($grey, 45);
.gantt-chart-container {
padding-bottom: 1em;
}
.gantt-chart { .gantt-chart {
padding: 5px 0; padding: 5px 0;
overflow-x: auto; overflow-x: auto;

View file

@ -2,7 +2,7 @@ $bucket-background: #e8f0f5;
$task-background: $white; $task-background: $white;
$ease-out: all .3s cubic-bezier(0.23, 1, 0.32, 1); $ease-out: all .3s cubic-bezier(0.23, 1, 0.32, 1);
$crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 116px - 1em - 1.5em'; $crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1em - 1.5em + 4px';
.kanban { .kanban {
@ -26,7 +26,7 @@ $crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 116px - 1em - 1.5em';
min-height: 20px; min-height: 20px;
.tasks { .tasks {
max-height: calc(#{$crazy-height-calculation} - 1rem - 2.5rem - 1em - #{$button-height} - 1em); max-height: calc(#{$crazy-height-calculation} - 1rem - 2.5rem - 2em - #{$button-height} - 1em);
overflow: auto; overflow: auto;
.task { .task {

View file

@ -0,0 +1,75 @@
.list-background-setting {
.image-search-result {
margin-top: 1em;
display: flex;
flex-flow: row wrap;
.image {
width: calc(100% / 6 - 1em);
height: 120px;
margin: .5em;
background-size: cover;
background-position: center;
display: flex;
@media screen and (min-width: $desktop) {
&:nth-child(6n) {
page-break-after: always; // CSS 2.1 syntax
break-after: always; // New syntax
}
}
@media screen and (max-width: $desktop) {
width: calc(100% / 4 - 1em);
&:nth-child(4n) {
page-break-after: always; // CSS 2.1 syntax
break-after: always; // New syntax
}
}
@media screen and (max-width: $tablet) {
width: calc(100% / 2 - 1em);
&:nth-child(2n) {
page-break-after: always; // CSS 2.1 syntax
break-after: always; // New syntax
}
}
@media screen and (max-width: ($tablet / 2)) {
width: calc(100% - 1em);
&:nth-child(1n) {
page-break-after: always; // CSS 2.1 syntax
break-after: always; // New syntax
}
}
.info {
align-self: flex-end;
display: block;
opacity: 0;
width: 100%;
padding: .25em 0;
text-align: center;
background: rgba(0, 0, 0, 0.5);
font-size: .75em;
font-weight: bold;
color: $white;
transition: opacity $transition;
}
&:hover .info {
opacity: 1;
}
}
}
.is-load-more-button {
margin: 0 auto;
display: block;
width: 200px;
}
}

View file

@ -1,4 +1,4 @@
.card.is-fullwidth{ .card.is-fullwidth {
margin-bottom: 1rem; margin-bottom: 1rem;
.add-form { .add-form {
@ -9,26 +9,26 @@
padding: 0; padding: 0;
} }
.table{ .table {
border-top: 1px solid darken(#fff, 15%); border-top: 1px solid darken(#fff, 15%);
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
td{ td {
vertical-align: middle; vertical-align: middle;
} }
td.type, td.actions{ td.type, td.actions {
width: 250px; width: 250px;
} }
td.actions{ td.actions {
text-align: right; text-align: right;
} }
} }
} }
.sharables-list, .sharables-namespace{ .sharables-list, .sharables-namespace {
padding: 0 !important; padding: 0 !important;
} }
@ -39,12 +39,16 @@
.search { .search {
max-width: 300px; max-width: 300px;
margin-top: -78px; margin-top: -58px;
float: right; float: right;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
.button, .input {
height: $switch-view-height;
}
.field { .field {
transition: width $transition; transition: width $transition;
width: 100%; width: 100%;
@ -61,3 +65,23 @@
} }
} }
} }
.list-title {
display: flex;
align-items: center;
h1 {
margin: 0;
}
.icon {
color: $grey-dark;
margin-left: 1rem;
height: 1rem;
width: 1rem;
}
}
.edit-list {
padding-bottom: 1em;
}

View file

@ -1,10 +1,11 @@
.switch-view { .switch-view {
background: $white; background: $white;
display: inline-block; display: inline-block;
margin: 1em 0;
border-radius: $radius; border-radius: $radius;
font-size: .8em; font-size: .8em;
box-shadow: 0.3em 0.3em 0.8em darken($light, 6); box-shadow: 0.3em 0.3em 0.8em darken($light, 6);
height: $switch-view-height;
margin-bottom: 1em;
a { a {
padding: .5em; padding: .5em;

View file

@ -1,8 +1,8 @@
.table-view { .table-view {
overflow-x: scroll;
.table { .table {
background: transparent; background: transparent;
overflow-x: auto;
overflow-y: hidden;
.user { .user {
margin: 0; margin: 0;
@ -19,8 +19,12 @@
width: 100%; width: 100%;
max-width: 180px; max-width: 180px;
position: absolute; position: absolute;
right: 3em; right: 1.5em;
margin-top: -80px; margin-top: -58px;
.button {
height: $switch-view-height;
}
.card { .card {
text-align: left; text-align: left;

View file

@ -1,7 +1,7 @@
.task-view { .task-view {
// This is a workaround to hide the llama background from the top on the task detail page // This is a workaround to hide the llama background from the top on the task detail page
margin-top: -1.5em; margin-top: -1.5em;
padding-top: 1.5em; padding: 1em;
background-color: $light-background; background-color: $light-background;
.subtitle { .subtitle {
@ -165,6 +165,10 @@
} }
} }
.task-view-container {
padding-bottom: 1em;
}
.is-done { .is-done {
background: $green; background: $green;
color: $white; color: $white;

View file

@ -1,8 +1,3 @@
.settings{
float: right;
color: rgb(74, 74, 74);
}
.tasks { .tasks {
margin-top: 1rem; margin-top: 1rem;
padding: 0; padding: 0;

View file

@ -9,3 +9,4 @@
@import 'notification'; @import 'notification';
@import 'offline'; @import 'offline';
@import 'update-notification'; @import 'update-notification';
@import 'background';

View file

@ -0,0 +1,50 @@
.app-container.has-background {
background-position: center;
background-size: cover;
background-repeat: no-repeat;
background-attachment: fixed;
min-height: 100vh;
.namespace-container {
background: $transparent-background-light;
}
.tasks {
background: $transparent-background-light;
border-radius: $radius;
.task:first-child {
border-radius: $radius $radius 0 0;
}
.task:last-child {
border-radius: 0 0 $radius $radius;
}
}
.table-view .table {
background: $transparent-background-light;
border-radius: $radius;
}
.pagination-link:not(.is-current) {
background: $light-background;
}
.box,
.card,
.switch-view,
.table-view .button,
.search .button {
box-shadow: none;
}
.task-view {
border-radius: $radius;
margin-top: 1em;
}
}
.navbar.has-background {
background: $transparent-background-light;
}

View file

@ -9,6 +9,7 @@
border-color: darken($color, 5); border-color: darken($color, 5);
} }
} }
.navbar-dropdown { .navbar-dropdown {
box-shadow: $navbar-dropdown-boxed-shadow; box-shadow: $navbar-dropdown-boxed-shadow;
top: 101%; top: 101%;
@ -18,6 +19,17 @@
.navbar.main-theme { .navbar.main-theme {
background: $light-background; background: $light-background;
z-index: 5 !important; z-index: 5 !important;
justify-content: space-between;
align-items: center;
.title {
margin: 0;
font-size: 1.75rem;
}
.navbar-end {
margin-left: 0;
}
@media screen and (max-width: $desktop) { @media screen and (max-width: $desktop) {
display: flex; display: flex;
@ -48,6 +60,7 @@
border: none; border: none;
box-shadow: none; box-shadow: none;
} }
@each $name, $pair in $colors { @each $name, $pair in $colors {
$color: nth($pair, 1); $color: nth($pair, 1);
$color-invert: nth($pair, 2); $color-invert: nth($pair, 2);
@ -65,16 +78,16 @@
} }
} }
.navbar-menu .navbar-item .icon{ .navbar-menu .navbar-item .icon {
margin: 0 0.5em; margin: 0 0.5em;
} }
.navbar{ .navbar {
z-index: 4 !important; z-index: 4 !important;
} }
.app-container { .app-container {
.namespace-container{ .namespace-container {
background: $vikunja-nav-background; background: $vikunja-nav-background;
z-index: 6; z-index: 6;
color: $vikunja-nav-color; color: $vikunja-nav-color;
@ -107,7 +120,7 @@
} }
} }
.menu{ .menu {
.menu-label { .menu-label {
font-size: 1em; font-size: 1em;
font-weight: 700; font-weight: 700;
@ -169,12 +182,12 @@
} }
} }
.nsettings{ .nsettings {
float: right; float: right;
padding: 10px 0.3em 0; padding: 10px 0.3em 0;
} }
.menu-label,.nsettings,.menu-list a{ .menu-label, .nsettings, .menu-list a {
color: $vikunja-nav-color; color: $vikunja-nav-color;
} }
@ -240,6 +253,7 @@
&.router-link-exact-active { &.router-link-exact-active {
color: $primary; color: $primary;
border-left: $vikunja-nav-selected-width solid darken($primary, 5%); border-left: $vikunja-nav-selected-width solid darken($primary, 5%);
.icon { .icon {
color: $primary; color: $primary;
} }
@ -280,9 +294,7 @@
} }
} }
.navbar .user{ .navbar .user {
padding: 1em 0.5em 0;
span { span {
font-family: $vikunja-font; font-family: $vikunja-font;
} }
@ -306,7 +318,7 @@
.dropdown-trigger .button { .dropdown-trigger .button {
background: none; background: none;
&:focus:not(:active), &:active{ &:focus:not(:active), &:active {
outline: none !important; outline: none !important;
-webkit-box-shadow: none !important; -webkit-box-shadow: none !important;
-moz-box-shadow: none !important; -moz-box-shadow: none !important;
@ -321,57 +333,59 @@
} }
} }
.mobilemenu-hide-button,.mobilemenu-show-button{ .mobilemenu-hide-button, .mobilemenu-show-button {
display: none; display: none;
position: fixed; position: fixed;
z-index: 31; z-index: 31;
font-weight: bold; font-weight: bold;
font-size: 2em; font-size: 2em;
color: $dark; color: $dark;
&:hover, &:focus{
&:hover, &:focus {
color: darken($dark, 20); color: darken($dark, 20);
} }
} }
.mobilemenu-hide-button{ .mobilemenu-hide-button {
color: $dark; color: $dark;
&:hover, &:focus{
&:hover, &:focus {
color: $dark; color: $dark;
} }
} }
.mobile-overlay{ .mobile-overlay {
display: none; display: none;
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: rgba(250,250,250,0.8); background: rgba(250, 250, 250, 0.8);
z-index: 5; z-index: 5;
opacity: 0; opacity: 0;
transition: all $transition; transition: all $transition;
} }
@media screen and (max-width: $tablet) { @media screen and (max-width: $tablet) {
.mobilemenu-hide-button{ .mobilemenu-hide-button {
display: block; display: block;
top: 1vh; top: 1vh;
right: 4vh; right: 4vh;
} }
.mobilemenu-show-button{ .mobilemenu-show-button {
display: block; display: block;
top: 1vh; top: 1vh;
left: 4vh; left: 4vh;
} }
.mobile-overlay{ .mobile-overlay {
display: block; display: block;
opacity: 1; opacity: 1;
} }
.navbar.is-dark .navbar-brand > .navbar-item{ .navbar.is-dark .navbar-brand > .navbar-item {
margin: 0 auto; margin: 0 auto;
} }
} }

View file

@ -57,3 +57,7 @@ button.table {
background-color: transparent; background-color: transparent;
} }
} }
.pagination {
padding-bottom: 1em;
}

View file

@ -18,6 +18,7 @@ $navbar-dropdown-boxed-shadow: $dropdown-content-shadow;
$bulmaswatch-import-font: false !default; $bulmaswatch-import-font: false !default;
$light-background: #F1F5F8; $light-background: #F1F5F8;
$transition-duration: 100ms; $transition-duration: 100ms;
$transparent-background-light: rgba($light-background, 0.9);
$vikunja-font: 'Quicksand', sans-serif; $vikunja-font: 'Quicksand', sans-serif;
$vikunja-light-text: darken(#fff, 10%); $vikunja-light-text: darken(#fff, 10%);
@ -44,3 +45,5 @@ $scrollbar-thumb-color: lighten($dark, 40);
$scrollbar-hover-color: lighten($dark, 30); $scrollbar-hover-color: lighten($dark, 30);
$button-height: 2.648em; $button-height: 2.648em;
$switch-view-height: 43px;