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:
parent
7fc0756b01
commit
4b3f92ae34
27 changed files with 513 additions and 70 deletions
29
src/App.vue
29
src/App.vue
|
@ -3,13 +3,27 @@
|
|||
<div v-if="online">
|
||||
<!-- 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>
|
||||
<nav class="navbar main-theme is-fixed-top" role="navigation" aria-label="main navigation"
|
||||
v-if="userAuthenticated && (userInfo && userInfo.type === authTypes.USER)">
|
||||
<nav
|
||||
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">
|
||||
<router-link :to="{name: 'home'}" class="navbar-item logo">
|
||||
<img src="/images/logo-full.svg" alt="Vikunja"/>
|
||||
</router-link>
|
||||
</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 v-if="updateAvailable" class="update-notification">
|
||||
<p>There is an update for Vikunja available!</p>
|
||||
|
@ -49,7 +63,11 @@
|
|||
<a @click="mobileMenuActive = false" class="mobilemenu-hide-button" v-if="mobileMenuActive">
|
||||
<icon icon="times"></icon>
|
||||
</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="menu top-menu">
|
||||
<router-link :to="{name: 'home'}" class="logo">
|
||||
|
@ -141,7 +159,7 @@
|
|||
<div class="more-container" :key="n.id + 'child'">
|
||||
<ul class="menu-list can-be-hidden" >
|
||||
<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="color-bubble" v-if="l.hexColor !== ''" :style="{ backgroundColor: l.hexColor }"></span>
|
||||
{{l.title}}
|
||||
|
@ -316,6 +334,7 @@
|
|||
return state.namespaces.namespaces.filter(n => this.showArchived ? true : !n.isArchived)
|
||||
},
|
||||
currentList: CURRENT_LIST,
|
||||
background: 'background',
|
||||
}),
|
||||
methods: {
|
||||
logout() {
|
||||
|
@ -351,7 +370,7 @@
|
|||
this.$route.name === 'migrate.wunderlist' ||
|
||||
this.$route.name === 'userSettings'
|
||||
) {
|
||||
this.$store.commit(CURRENT_LIST, 0)
|
||||
this.$store.commit(CURRENT_LIST, {})
|
||||
}
|
||||
},
|
||||
showRefreshUI(e) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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">
|
||||
This list is archived.
|
||||
It is not possible to create new or edit tasks or it.
|
||||
|
@ -104,6 +104,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<background :list-id="$route.params.id"/>
|
||||
|
||||
<component
|
||||
:is="manageUsersComponent"
|
||||
:id="list.id"
|
||||
|
@ -141,6 +143,8 @@
|
|||
import ListModel from '../../models/list'
|
||||
import ListService from '../../services/list'
|
||||
import Fancycheckbox from '../global/fancycheckbox'
|
||||
import Background from './settings/background'
|
||||
import {CURRENT_LIST} from '../../store/mutation-types'
|
||||
|
||||
export default {
|
||||
name: "EditList",
|
||||
|
@ -156,6 +160,7 @@
|
|||
}
|
||||
},
|
||||
components: {
|
||||
Background,
|
||||
Fancycheckbox,
|
||||
LinkSharing,
|
||||
manageSharing,
|
||||
|
@ -183,6 +188,7 @@
|
|||
this.listService.get(list)
|
||||
.then(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.manageTeamsComponent = 'manageSharing'
|
||||
this.manageUsersComponent = 'manageSharing'
|
||||
|
|
|
@ -1,20 +1,33 @@
|
|||
<template>
|
||||
<div class="loader-container" :class="{ 'is-loading': listService.loading}">
|
||||
<div class="content">
|
||||
<router-link :to="{ name: 'editList', params: { id: list.id } }" class="icon settings is-medium">
|
||||
<icon icon="cog" size="2x"/>
|
||||
<div
|
||||
class="loader-container"
|
||||
:class="{ 'is-loading': listService.loading}"
|
||||
>
|
||||
<div class="switch-view">
|
||||
<router-link
|
||||
:to="{ name: 'list.list', params: { listId: listId } }"
|
||||
:class="{'is-active': $route.name === 'list.list'}">
|
||||
List
|
||||
</router-link>
|
||||
<h1 :style="{ 'opacity': list.title === '' ? '0': '1' }">{{ list.title === '' ? 'Loading...': list.title}}</h1>
|
||||
<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 class="switch-view">
|
||||
<router-link :to="{ name: 'list.list', params: { listId: listId } }" :class="{'is-active': $route.name === 'list.list'}">List</router-link>
|
||||
<router-link :to="{ name: 'list.gantt', params: { listId: listId } }" :class="{'is-active': $route.name === 'list.gantt'}">Gantt</router-link>
|
||||
<router-link :to="{ name: 'list.table', params: { listId: listId } }" :class="{'is-active': $route.name === 'list.table'}">Table</router-link>
|
||||
<router-link :to="{ name: 'list.kanban', params: { listId: listId } }" :class="{'is-active': $route.name === 'list.kanban'}">Kanban</router-link>
|
||||
</div>
|
||||
<router-link
|
||||
:to="{ name: 'list.gantt', params: { listId: listId } }"
|
||||
:class="{'is-active': $route.name === 'list.gantt'}">
|
||||
Gantt
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'list.table', params: { listId: listId } }"
|
||||
:class="{'is-active': $route.name === 'list.table'}">
|
||||
Table
|
||||
</router-link>
|
||||
<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>
|
||||
|
||||
<router-view/>
|
||||
|
@ -53,12 +66,15 @@
|
|||
listId() {
|
||||
return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId
|
||||
},
|
||||
background() {
|
||||
return this.$store.state.background
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadList() {
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -76,13 +92,12 @@
|
|||
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.
|
||||
let list = new ListModel({id: this.$route.params.listId})
|
||||
this.listService.get(list)
|
||||
.then(r => {
|
||||
this.$set(this, 'list', r)
|
||||
this.$store.commit(CURRENT_LIST, r)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
|
|
132
src/components/lists/settings/background.vue
Normal file
132
src/components/lists/settings/background.vue
Normal 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>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="gantt-chart-container">
|
||||
<div class="gantt-options">
|
||||
<fancycheckbox v-model="showTaskswithoutDates" class="is-block">
|
||||
Show tasks which don't have dates set
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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="heading">
|
||||
<h1 class="title task-id" v-if="task.identifier === ''">
|
||||
|
|
|
@ -22,8 +22,6 @@ export const getListView = listId => {
|
|||
localStorage.removeItem('listView')
|
||||
}
|
||||
|
||||
console.log('saved list view state', savedListView)
|
||||
|
||||
if (!savedListView) {
|
||||
return 'list.list'
|
||||
}
|
||||
|
|
12
src/models/backgroundImage.js
Normal file
12
src/models/backgroundImage.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import AbstractModel from './abstractModel'
|
||||
|
||||
export default class BackgroundImageModel extends AbstractModel {
|
||||
defaults() {
|
||||
return {
|
||||
id: 0,
|
||||
url: '',
|
||||
thumb: '',
|
||||
info: {},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -34,6 +34,7 @@ export default class ListModel extends AbstractModel {
|
|||
isArchived: false,
|
||||
hexColor: '',
|
||||
identifier: '',
|
||||
backgroundInformation: null,
|
||||
|
||||
created: null,
|
||||
updated: null,
|
||||
|
|
34
src/services/backgroundUnsplash.js
Normal file
34
src/services/backgroundUnsplash.js
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
|
@ -37,4 +37,22 @@ export default class ListService extends AbstractService {
|
|||
list.hexColor = list.hexColor.substring(1, 7)
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
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 tasks from './modules/tasks'
|
||||
import lists from './modules/lists'
|
||||
import ListService from '../services/list'
|
||||
|
||||
export const store = new Vuex.Store({
|
||||
modules: {
|
||||
|
@ -25,7 +27,8 @@ export const store = new Vuex.Store({
|
|||
online: true,
|
||||
isFullpage: false,
|
||||
// This is used to highlight the current list in menu for all list related views
|
||||
currentList: 0,
|
||||
currentList: {id: 0},
|
||||
background: '',
|
||||
},
|
||||
mutations: {
|
||||
[LOADING](state, loading) {
|
||||
|
@ -41,6 +44,29 @@ export const store = new Vuex.Store({
|
|||
state.isFullpage = fullpage
|
||||
},
|
||||
[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
|
||||
},
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@ export default {
|
|||
availableMigrators: [],
|
||||
taskAttachmentsEnabled: true,
|
||||
totpEnabled: true,
|
||||
enabledBackgroundProviders: [],
|
||||
}),
|
||||
mutations: {
|
||||
[CONFIG](state, config) {
|
||||
|
@ -26,6 +27,7 @@ export default {
|
|||
state.availableMigrators = config.available_migrators
|
||||
state.taskAttachmentsEnabled = config.task_attachments_enabled
|
||||
state.totpEnabled = config.totp_enabled
|
||||
state.enabledBackgroundProviders = config.enabled_background_providers
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
|
|
|
@ -16,3 +16,4 @@
|
|||
@import 'table-view';
|
||||
@import 'kanban';
|
||||
@import 'modal';
|
||||
@import 'list-backgrounds';
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
$gantt-border: 1px solid $grey-lighter;
|
||||
$gantt-vertical-border-color: lighten($grey, 45);
|
||||
|
||||
.gantt-chart-container {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.gantt-chart {
|
||||
padding: 5px 0;
|
||||
overflow-x: auto;
|
||||
|
|
|
@ -2,7 +2,7 @@ $bucket-background: #e8f0f5;
|
|||
$task-background: $white;
|
||||
$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 {
|
||||
|
||||
|
@ -26,7 +26,7 @@ $crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 116px - 1em - 1.5em';
|
|||
min-height: 20px;
|
||||
|
||||
.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;
|
||||
|
||||
.task {
|
||||
|
|
75
src/styles/components/list-backgrounds.scss
Normal file
75
src/styles/components/list-backgrounds.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
.card.is-fullwidth{
|
||||
.card.is-fullwidth {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.add-form {
|
||||
|
@ -9,26 +9,26 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.table{
|
||||
.table {
|
||||
border-top: 1px solid darken(#fff, 15%);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
td{
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td.type, td.actions{
|
||||
td.type, td.actions {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
td.actions{
|
||||
td.actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sharables-list, .sharables-namespace{
|
||||
.sharables-list, .sharables-namespace {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
|
@ -39,12 +39,16 @@
|
|||
|
||||
.search {
|
||||
max-width: 300px;
|
||||
margin-top: -78px;
|
||||
margin-top: -58px;
|
||||
float: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.button, .input {
|
||||
height: $switch-view-height;
|
||||
}
|
||||
|
||||
.field {
|
||||
transition: width $transition;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
.switch-view {
|
||||
background: $white;
|
||||
display: inline-block;
|
||||
margin: 1em 0;
|
||||
border-radius: $radius;
|
||||
font-size: .8em;
|
||||
box-shadow: 0.3em 0.3em 0.8em darken($light, 6);
|
||||
height: $switch-view-height;
|
||||
margin-bottom: 1em;
|
||||
|
||||
a {
|
||||
padding: .5em;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
.table-view {
|
||||
overflow-x: scroll;
|
||||
|
||||
.table {
|
||||
background: transparent;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
.user {
|
||||
margin: 0;
|
||||
|
@ -19,8 +19,12 @@
|
|||
width: 100%;
|
||||
max-width: 180px;
|
||||
position: absolute;
|
||||
right: 3em;
|
||||
margin-top: -80px;
|
||||
right: 1.5em;
|
||||
margin-top: -58px;
|
||||
|
||||
.button {
|
||||
height: $switch-view-height;
|
||||
}
|
||||
|
||||
.card {
|
||||
text-align: left;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.task-view {
|
||||
// This is a workaround to hide the llama background from the top on the task detail page
|
||||
margin-top: -1.5em;
|
||||
padding-top: 1.5em;
|
||||
padding: 1em;
|
||||
background-color: $light-background;
|
||||
|
||||
.subtitle {
|
||||
|
@ -165,6 +165,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.task-view-container {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.is-done {
|
||||
background: $green;
|
||||
color: $white;
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
.settings{
|
||||
float: right;
|
||||
color: rgb(74, 74, 74);
|
||||
}
|
||||
|
||||
.tasks {
|
||||
margin-top: 1rem;
|
||||
padding: 0;
|
||||
|
|
|
@ -9,3 +9,4 @@
|
|||
@import 'notification';
|
||||
@import 'offline';
|
||||
@import 'update-notification';
|
||||
@import 'background';
|
||||
|
|
50
src/styles/theme/background.scss
Normal file
50
src/styles/theme/background.scss
Normal 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;
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
border-color: darken($color, 5);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-dropdown {
|
||||
box-shadow: $navbar-dropdown-boxed-shadow;
|
||||
top: 101%;
|
||||
|
@ -18,6 +19,17 @@
|
|||
.navbar.main-theme {
|
||||
background: $light-background;
|
||||
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) {
|
||||
display: flex;
|
||||
|
@ -48,6 +60,7 @@
|
|||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@each $name, $pair in $colors {
|
||||
$color: nth($pair, 1);
|
||||
$color-invert: nth($pair, 2);
|
||||
|
@ -65,16 +78,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.navbar-menu .navbar-item .icon{
|
||||
.navbar-menu .navbar-item .icon {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.navbar{
|
||||
.navbar {
|
||||
z-index: 4 !important;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
.namespace-container{
|
||||
.namespace-container {
|
||||
background: $vikunja-nav-background;
|
||||
z-index: 6;
|
||||
color: $vikunja-nav-color;
|
||||
|
@ -107,7 +120,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.menu{
|
||||
.menu {
|
||||
.menu-label {
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
|
@ -169,12 +182,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.nsettings{
|
||||
.nsettings {
|
||||
float: right;
|
||||
padding: 10px 0.3em 0;
|
||||
}
|
||||
|
||||
.menu-label,.nsettings,.menu-list a{
|
||||
.menu-label, .nsettings, .menu-list a {
|
||||
color: $vikunja-nav-color;
|
||||
}
|
||||
|
||||
|
@ -240,6 +253,7 @@
|
|||
&.router-link-exact-active {
|
||||
color: $primary;
|
||||
border-left: $vikunja-nav-selected-width solid darken($primary, 5%);
|
||||
|
||||
.icon {
|
||||
color: $primary;
|
||||
}
|
||||
|
@ -280,9 +294,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.navbar .user{
|
||||
padding: 1em 0.5em 0;
|
||||
|
||||
.navbar .user {
|
||||
span {
|
||||
font-family: $vikunja-font;
|
||||
}
|
||||
|
@ -306,7 +318,7 @@
|
|||
.dropdown-trigger .button {
|
||||
background: none;
|
||||
|
||||
&:focus:not(:active), &:active{
|
||||
&:focus:not(:active), &:active {
|
||||
outline: none !important;
|
||||
-webkit-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;
|
||||
position: fixed;
|
||||
z-index: 31;
|
||||
font-weight: bold;
|
||||
font-size: 2em;
|
||||
color: $dark;
|
||||
&:hover, &:focus{
|
||||
|
||||
&:hover, &:focus {
|
||||
color: darken($dark, 20);
|
||||
}
|
||||
}
|
||||
|
||||
.mobilemenu-hide-button{
|
||||
.mobilemenu-hide-button {
|
||||
color: $dark;
|
||||
&:hover, &:focus{
|
||||
|
||||
&:hover, &:focus {
|
||||
color: $dark;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-overlay{
|
||||
.mobile-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(250,250,250,0.8);
|
||||
background: rgba(250, 250, 250, 0.8);
|
||||
z-index: 5;
|
||||
opacity: 0;
|
||||
transition: all $transition;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
.mobilemenu-hide-button{
|
||||
.mobilemenu-hide-button {
|
||||
display: block;
|
||||
top: 1vh;
|
||||
right: 4vh;
|
||||
}
|
||||
|
||||
.mobilemenu-show-button{
|
||||
.mobilemenu-show-button {
|
||||
display: block;
|
||||
top: 1vh;
|
||||
left: 4vh;
|
||||
}
|
||||
|
||||
.mobile-overlay{
|
||||
.mobile-overlay {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.navbar.is-dark .navbar-brand > .navbar-item{
|
||||
.navbar.is-dark .navbar-brand > .navbar-item {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,3 +57,7 @@ button.table {
|
|||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ $navbar-dropdown-boxed-shadow: $dropdown-content-shadow;
|
|||
$bulmaswatch-import-font: false !default;
|
||||
$light-background: #F1F5F8;
|
||||
$transition-duration: 100ms;
|
||||
$transparent-background-light: rgba($light-background, 0.9);
|
||||
|
||||
$vikunja-font: 'Quicksand', sans-serif;
|
||||
$vikunja-light-text: darken(#fff, 10%);
|
||||
|
@ -44,3 +45,5 @@ $scrollbar-thumb-color: lighten($dark, 40);
|
|||
$scrollbar-hover-color: lighten($dark, 30);
|
||||
|
||||
$button-height: 2.648em;
|
||||
|
||||
$switch-view-height: 43px;
|
Loading…
Reference in a new issue