1
1
mirror of https://github.com/aelve/guide.git synced 2024-11-26 12:42:30 +03:00

Adaptation for mobile screens (#340)

* Consistent headings and paragraph styles for entire app

* Search adopted for mobile

* Category item adapted for mobile

* Consistent category header icon sizes and paddings, relatively rest of category page

* Hide logo when search input displayed on mobile

* removed excess css rule

* font size and line height changed

* Badge adjusted for new font size

* Lists margins added

* Footer padding and margins adapted for mobile

* Markdown editor correct focus on open

* Markdown editor autofocus now optionabe

* Typo fix

* Markdown editor component more props and styling

* Markdown editor border stylings

* Conflict dialog refactor and adapting for mobiles

* Typo fix

* Categories page restyled and adapted for mobiles

* Categories page group titles allow break words

* Removed excess comparison
This commit is contained in:
avele 2019-07-24 19:33:02 +04:00 committed by GitHub
parent bff3cac46e
commit 613ecf8c9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 732 additions and 281 deletions

View File

@ -43,21 +43,93 @@ export default class RootComponent extends Vue {
} }
</script> </script>
<style> <style lang="postcss">
*, *,
*:before, *:before,
*:after { *:after {
box-sizing: border-box; box-sizing: border-box;
} }
html {
/*
Formula is: calc(
[min-font-size] +
([max-font-size] - [min-font-size] * (100vw - [min-vw]) / ([max-vw] - [min-vw])
);
*/
font-size: calc(16px + (18 - 16) * (100vw - 320px) / (1920 - 320));
}
:root {
--h1-font-size: 1.8rem;
--h2-font-size: 1.4rem;
--h3-font-size: 1.17rem;
--h4-font-size: 1rem;
--h5-font-size: 0.83rem;
--h6-font-size: 0.75rem;
}
h1 {
font-size: var(--h1-font-size);
margin: 1.95rem 0 0.6rem;
}
h2 {
font-size: var(--h2-font-size);
margin: 1.65rem 0 0.45rem;
}
h3 {
font-size: var(--h3-font-size);
margin: 1.35rem 0 0.35rem;
}
h4 {
font-size: var(--h4-font-size);
margin: 1rem 0 0.25rem;
}
h5 {
font-size: var(--h5-font-size);
margin: 0.8rem 0 0.2rem;
}
h6 {
font-size: var(--h6-font-size);
margin: 0.5rem 0 0;
}
.text-h1 {
font-size: var(--h1-font-size);
}
.text-h2 {
font-size: var(--h2-font-size);
}
.text-h3 {
font-size: var(--h3-font-size);
}
.text-h4 {
font-size: var(--h4-font-size);
}
.text-h5 {
font-size: var(--h5-font-size);
}
.text-h6 {
font-size: var(--h6-font-size);
}
p { p {
margin-bottom: 10px; margin: 0.2rem 0 1.2rem 0;
} }
ul,
ol {
margin: 0.8rem 0;
}
ul > li:not(:last-child),
ol > li:not(:last-child) {
margin-bottom: 5px;
}
li p { li p {
margin-bottom: 0; margin-top: 0;
}
ul li:not(:last-child) {
margin-bottom: 2px;
} }
code { code {
color: inherit; color: inherit;
text-decoration: inherit; text-decoration: inherit;
@ -88,7 +160,7 @@ code.sourceCode {
padding: 8px; padding: 8px;
} }
.sourceCode:not(:last-child) code.sourceCode { .sourceCode:not(:last-child) code.sourceCode {
margin: 0 0 5px; margin: 0 0 0.2rem;
} }
a { a {
text-decoration-line: none; text-decoration-line: none;
@ -157,5 +229,6 @@ blockquote {
/* Should be same padding on any screen cause it changes with toolbar and we fixed toolbar's height (see Toolbar.vue) height on each screen */ /* Should be same padding on any screen cause it changes with toolbar and we fixed toolbar's height (see Toolbar.vue) height on each screen */
.app-content { .app-content {
padding: 64px 0px 0px !important; padding: 64px 0px 0px !important;
line-height: 150%;
} }
</style> </style>

View File

@ -1,5 +1,6 @@
<template> <template>
<v-footer <v-footer
class="pa-2"
height="auto" height="auto"
color="lighten-1" color="lighten-1"
> >
@ -8,8 +9,9 @@
align-center align-center
row row
wrap wrap
class="footer"
> >
<span class="mr-3"> <span>
made by made by
<a-link <a-link
openInNewTab openInNewTab
@ -18,7 +20,7 @@
Aelve Aelve
</a-link> </a-link>
</span> </span>
<span class="mr-3"> <span>
<a-link <a-link
openInNewTab openInNewTab
url="https://github.com/aelve/guide" url="https://github.com/aelve/guide"
@ -49,8 +51,15 @@ import Component from 'vue-class-component'
export default class AFooter extends Vue { } export default class AFooter extends Vue { }
</script> </script>
<style scoped> <style lang="postcss" scoped>
.footer-link { .footer-link {
display: inline-block; display: inline-block;
} }
.footer {
margin-right: -10px;
> * {
margin-right: 10px;
}
}
</style> </style>

View File

@ -1,95 +1,75 @@
<template> <template>
<v-container grid-list-md> <div class="categories">
<v-layout <div class="categories-flex-container">
row
wrap <div
justify-space-between class="categories-column"
>
<v-flex
class="mr-3 mt-3"
column
xs12
sm5
md3
lg3
xl1
v-for="(groupCategories, groupName, index) in groups" v-for="(groupCategories, groupName, index) in groups"
:key="index" :key="index"
> >
<div class="category-group"> <div class="category-group">
<h4 class="mb-2 display-1 font-weight-black category-group-name"> <h2 class="mt-0 mb-2 group-title">
{{ groupName }} {{ groupName }}
</h4> </h2>
<router-link <router-link
class="category-title" class="category-title ml-2"
v-for="category in groupCategories[CategoryStatus.finished]" v-for="category in groupCategories[CategoryStatus.finished]"
:key="category.id" :key="category.id"
:to="`/haskell/${getCategoryUrl(category)}`" :to="`/haskell/${getCategoryUrl(category)}`"
> >
<h6 {{ category.title }}
class="ml-2 subheading font-weight-bold"
>
{{ category.title }}
</h6>
</router-link> </router-link>
<h6 <template v-if="groupCategories[CategoryStatus.inProgress]">
class="ml-2 mb-1 body-2 font-weight-bold" <h3 class="status-title">
v-if="groupCategories[CategoryStatus.inProgress]" In progress
> </h3>
In progress <router-link
</h6> class="category-title ml-3"
<router-link v-for="category in groupCategories[CategoryStatus.inProgress]"
class="category-title ml-3" :key="category.id"
v-for="category in groupCategories[CategoryStatus.inProgress]" :to="`/haskell/${getCategoryUrl(category)}`"
:key="category.id"
:to="`/haskell/${getCategoryUrl(category)}`"
>
<h6
class="ml-2 body-1 font-weight-bold"
> >
{{ category.title }} {{ category.title }}
</h6> </router-link>
</router-link> </template>
<h6 <template v-if="groupCategories[CategoryStatus.toBeWritten]">
class="ml-2 mb-1 body-2 font-weight-bold" <h3 class="status-title">
v-if="groupCategories[CategoryStatus.toBeWritten]" To be written
> </h3>
To be written
</h6> <router-link
<router-link class="category-title ml-3"
class="category-title ml-3" v-for="category in groupCategories[CategoryStatus.toBeWritten]"
v-for="category in groupCategories[CategoryStatus.toBeWritten]" :key="category.id"
:key="category.id" :to="`/haskell/${getCategoryUrl(category)}`"
:to="`/haskell/${getCategoryUrl(category)}`"
>
<h6
class="ml-2 body-1 font-weight-bold"
> >
{{ category.title }} {{ category.title }}
</h6> </router-link>
</router-link> </template>
<v-btn <v-btn
class="ma-0 px-1" flat
class="ma-0 mt-1 px-1"
color="grey darken-2" color="grey darken-2"
title="Add new category" title="Add new category"
flat
@click="openAddCategoryDialog(groupName)" @click="openAddCategoryDialog(groupName)"
> >
<v-icon size="14" class="mr-1" left>$vuetify.icons.plus</v-icon> <v-icon size="14" class="mr-1" left>$vuetify.icons.plus</v-icon>
Add new category Add new category
</v-btn> </v-btn>
</div> </div>
</v-flex> </div>
<add-category-dialog
v-model="isAddGroupDialogOpen" </div>
:groupName="addCategoryGroupName"
/> <add-category-dialog
</v-layout> v-model="isAddGroupDialogOpen"
</v-container> :groupName="addCategoryGroupName"
/>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -145,15 +125,82 @@ export default class Categories extends Vue {
} }
</script> </script>
<style scoped> <style lang="postcss" scoped>
.categories {
margin-top: 30px;
}
.categories-flex-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: -15px -15px 0 0;
padding: 0 100px;
&:after {
content: "";
flex: 1;
}
@media screen and (max-width: 1150px) {
padding: 0 60px;
}
@media screen and (max-width: 830px) {
padding: 0 40px;
}
}
.categories-column {
margin: 15px 15px 0 0;
flex-shrink: 0;
flex-grow: 99999;
@media screen and (min-width: 1904px) {
flex-basis: calc(20% - 15px * 5);
max-width: calc(20% - 15px);
}
@media screen and (max-width: 1904px) {
flex-basis: calc(25% - 15px * 4);
max-width: calc(25% - 15px);
}
@media screen and (max-width: 1150px) {
flex-basis: calc(33.33333% - 15px * 3);
max-width: calc(33.33333% - 15px);
}
@media screen and (max-width: 830px) {
flex-basis: calc(50% - 15px * 2);
max-width: calc(50% - 15px);
}
@media screen and (max-width: 480px) {
flex-basis: calc(100% - 15px * 1);
max-width: calc(100% - 15px);
}
}
.category-group { .category-group {
text-align: left; display: flex;
flex-direction: column;
align-items: baseline;
}
.group-title {
font-weight: 900;
font-size: 1.6rem;
max-width: 90%;
word-break: break-word;
} }
.category-title { .category-title {
display: block;
line-height: 1.2; line-height: 1.2;
font-size: 0.9rem;
font-weight: 600;
} }
.category-title:not(:last-child) { .category-title:not(:first-child) {
margin-bottom: 5px; margin-top: 5px;
}
.status-title {
font-size: 0.9rem;
margin: 4px 0 0 8px;
line-height: 1.7;
+ .category-title {
margin-top: 0;
}
} }
</style> </style>

View File

@ -94,7 +94,7 @@ export default class Category extends Vue {
margin: 0 auto; margin: 0 auto;
} }
@media screend and (max-width: 768px) { @media screen and (max-width: 768px) {
.category-item { .category-item {
margin: 0 0 30px; margin: 0 0 30px;
} }

View File

@ -2,7 +2,7 @@
<div class="category-description"> <div class="category-description">
<div v-if="!editDescriptionShown"> <div v-if="!editDescriptionShown">
<p v-if="!categoryDescription">This category has no description yet, you can contribute to the category by adding description</p> <p v-if="!categoryDescription">This category has no description yet, you can contribute to the category by adding description</p>
<div class="description-content" v-else v-html="categoryDescription" /> <div class="category-description__content" v-else v-html="categoryDescription" />
</div> </div>
<markdown-editor <markdown-editor
@ -44,7 +44,7 @@ import CatchConflictDecorator from 'client/helpers/CatchConflictDecorator'
}, },
mixins: [conflictDialogMixin] mixins: [conflictDialogMixin]
}) })
export default class CategoryDescriptiom extends Vue { export default class CategoryDescription extends Vue {
editDescriptionShown: boolean = false editDescriptionShown: boolean = false
originalDescription: string = _get(this, '$store.state.category.category.description.text') originalDescription: string = _get(this, '$store.state.category.category.description.text')
modified: string = '' modified: string = ''
@ -95,24 +95,11 @@ export default class CategoryDescriptiom extends Vue {
.category-description { .category-description {
margin: 0 0 40px; margin: 0 0 40px;
} }
.category-description__content > :first-child {
.category-description { margin-top: 0;
font-size: 16px;
} }
.category-description__content > :last-child {
.category-description >>> { margin-bottom: 0;
h1 {
font-size: 1.4rem;
margin: 15px 0;
}
h2 {
font-size: 1.2rem;
margin: 10px 0;
}
h3 {
font-size: 1rem;
margin: 5px 0;
}
} }
</style> </style>

View File

@ -1,26 +1,25 @@
<template> <template>
<div class="category-header"> <div class="category-header">
<h2 class="category-name-title" :title="categoryTitle"> <h1 class="category-name-title" :title="categoryTitle">
<v-tooltip bottom> <v-tooltip bottom>
<template v-slot:activator="{ on }"> <template v-slot:activator="{ on }">
<a-link <a-link
openInNewTab openInNewTab
aria-label="RSS feed for all new items in this category" aria-label="RSS feed for all new items in this category"
:url="`https://guide.aelve.com/haskell/feed/category/${categoryId}`" :url="`https://guide.aelve.com/haskell/feed/category/${categoryId}`"
class="rss-link mr-1" class="rss-link"
v-on="on" v-on="on"
> >
<v-icon <v-icon
size="24"
class="rss-link-icon" class="rss-link-icon"
>$vuetify.icons.rss</v-icon> >$vuetify.icons.rss</v-icon>
</a-link> </a-link>
</template> </template>
<span>RSS feed for all new items in this category</span> <span>RSS feed for all new items in this category</span>
</v-tooltip>{{categoryTitle}} </v-tooltip>{{categoryTitle}}
</h2> </h1>
<div style="display: flex; align-items: center; justify-content: space-between;"> <div class="category-header__second-row">
<div class="category-group-title-wrap"> <div class="category-group-title-wrap">
in <span :title="categoryGroup" class="category-group-title"> {{ categoryGroup }} </span> in <span :title="categoryGroup" class="category-group-title"> {{ categoryGroup }} </span>
</div> </div>
@ -56,29 +55,27 @@
> >
<v-icon <v-icon
color="grey darken-2" color="grey darken-2"
size="16" size="18"
>$vuetify.icons.bars</v-icon> >$vuetify.icons.bars</v-icon>
</v-btn> </v-btn>
</template> </template>
<v-list> <v-list class="category-actions-menu-list">
<v-list-tile class="category-actions-menu-item"> <v-list-tile>
<CategoryHeaderBtn <CategoryHeaderBtn
text="New item" text="New item"
icon="plus" icon="plus"
class="mr-1"
@click="openAddItemDialog" @click="openAddItemDialog"
/> />
</v-list-tile> </v-list-tile>
<v-list-tile class="category-actions-menu-item"> <v-list-tile>
<CategoryHeaderBtn <CategoryHeaderBtn
text="Category settings" text="Category settings"
icon="cog" icon="cog"
class="mr-1"
@click="openCategorySettingsEditDialog" @click="openCategorySettingsEditDialog"
/> />
</v-list-tile> </v-list-tile>
<v-list-tile class="category-actions-menu-item"> <v-list-tile>
<CategoryHeaderBtn <CategoryHeaderBtn
text="Delete category" text="Delete category"
icon="trash-alt" icon="trash-alt"
@ -161,37 +158,33 @@ export default class CategoryHeader extends Vue {
} }
} }
.category-name-title { .category-name-title {
font-size: 28px; font-size: 1.9rem;
margin-bottom: 5px; font-weight: 700;
margin: 0 0 5px;
letter-spacing: -1px; letter-spacing: -1px;
@media (max-width: 768px) {
font-size: 24px;
}
@media (max-width: 425px) {
font-size: 20px;
}
} }
.rss-link { .rss-link {
/* For vertical aligning on one line with category title */ /* For vertical aligning on one line with category title */
font-size: 1px; font-size: 1px;
margin-right: 6px;
} }
.rss-link-icon { .rss-link-icon {
@media (max-width: 768px) { height: calc(1.8rem - 5px) !important;
height: 20px !important; width: auto;
}
@media (max-width: 425px) {
height: 18px !important;
}
&:hover { &:hover {
color: #000; color: #000;
} }
} }
.category-header__second-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.category-group-title-wrap { .category-group-title-wrap {
padding-left: 4px; padding-left: 4px;
white-space: nowrap; white-space: nowrap;
@ -202,7 +195,6 @@ export default class CategoryHeader extends Vue {
.category-group-title { .category-group-title {
font-weight: 600; font-weight: 600;
font-size: 16px;
} }
.category-actions-menu-btn { .category-actions-menu-btn {
@ -215,11 +207,10 @@ export default class CategoryHeader extends Vue {
flex: 1; flex: 1;
} }
.category-actions-menu-item { .category-actions-menu-list {
height: 36px;
>>> .v-list__tile { >>> .v-list__tile {
height: 36px; height: 36px;
padding: 0 6px;
} }
>>> button { >>> button {

View File

@ -7,7 +7,7 @@
v-bind="$attrs" v-bind="$attrs"
v-on="$listeners" v-on="$listeners"
> >
<v-icon size="14" class="mr-1" left>{{`$vuetify.icons.${icon}`}}</v-icon> <v-icon size="18" class="mr-1">{{`$vuetify.icons.${icon}`}}</v-icon>
{{ text }} {{ text }}
</v-btn> </v-btn>
</template> </template>
@ -28,8 +28,6 @@ export default class CategoryHeaderBtn extends Vue {
.category-header-btn { .category-header-btn {
margin: 0; margin: 0;
padding: 0 4px; padding: 0 4px;
height: 28px;
font-size: 12px;
font-weight: bold; font-weight: bold;
} }
</style> </style>

View File

@ -23,7 +23,7 @@
@save="updateSummary({original: summary.text, modified: $event})" @save="updateSummary({original: summary.text, modified: $event})"
> >
<div <div
class="mb-2 category-item-summary" class="mb-2"
v-html="summary.html" v-html="summary.html"
/> />
</category-item-section> </category-item-section>
@ -212,23 +212,11 @@ export default class CategoryItem extends Vue {
} }
</script> </script>
<style scoped> <style lang="postcss" scoped>
.category-item-body { .category-item-body {
padding: 15px 20px; padding: 15px 20px;
} }
.category-item-body >>> p {
font-size: 16px;
}
.category-item-body >>> li {
font-size: 16px;
}
.category-item-summary >>> h1 {
margin: 25px 0 5px;
}
.category-item { .category-item {
position: relative; position: relative;
background: #eeeeee; background: #eeeeee;
@ -237,6 +225,7 @@ export default class CategoryItem extends Vue {
.category-item-traits { .category-item-traits {
display: flex; display: flex;
flex-wrap: wrap;
} }
.category-item-traits > * { .category-item-traits > * {
@ -246,14 +235,21 @@ export default class CategoryItem extends Vue {
.category-item-traits > *:not(:last-child) { .category-item-traits > *:not(:last-child) {
margin-right: 20px; margin-right: 20px;
@media screen and (max-width: 768px) {
margin-right: 0;
}
} }
@media screend and (max-width: 768px) { @media screen and (max-width: 768px) {
.category-item-body { .category-item-body {
width: 100%; width: 100%;
} }
.category-item { .category-item {
margin: 0 0 30px; margin: 0 0 30px;
} }
.category-item-traits {
flex-direction: column;
}
} }
</style> </style>

View File

@ -1,16 +1,21 @@
<template> <template>
<v-btn <v-btn
:icon="!!icon" :icon="!!icon && !showTitle"
class="ma-0" flat
class="ma-0 font-weight-bold"
color="grey darken-2"
:style="style" :style="style"
:title="title"
v-bind="$attrs" v-bind="$attrs"
v-on="$listeners" v-on="$listeners"
> >
<v-icon <v-icon
v-if="icon" v-if="icon"
color="grey darken-2" color="grey darken-2"
:class="{ 'mr-1': showTitle }"
:size="iconSizeValue" :size="iconSizeValue"
>{{ iconText }}</v-icon> >{{ iconText }}</v-icon>
<template v-if="showTitle"> {{ title }} </template>
<slot /> <slot />
</v-btn> </v-btn>
</template> </template>
@ -27,6 +32,8 @@ export default class CategoryItemBtn extends Vue {
@Prop(Boolean) small: boolean @Prop(Boolean) small: boolean
@Prop(String) size: string @Prop(String) size: string
@Prop(String) iconSize: string @Prop(String) iconSize: string
@Prop(String) title: string
@Prop(Boolean) showTitle: boolean
get style () { get style () {
// Size prop overlaps small prop // Size prop overlaps small prop

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="category-item-section"> <div class="category-item-section">
<h3 class="category-item-section__title title font-weight-bold mb-1"> <h3 class="category-item-section__title title font-weight-bold mb-1 mt-0">
{{ title }} {{ title }}
<category-item-btn <category-item-btn
small small

View File

@ -11,71 +11,168 @@
class="elevation-2" class="elevation-2"
@click.stop="" @click.stop=""
> >
<v-toolbar-title class="headline"> <v-toolbar-title class="text-h2">
<router-link <span class="category-item-toolbar-title">
:to="{hash:`item-${itemUid}`}" <router-link
class="category-item-anchor" :to="{hash:`item-${itemUid}`}"
>#</router-link>&nbsp;<a-link class="category-item-anchor"
v-if="itemLink" >#</router-link>
:url="itemLink"
openInNewTab <div class="category-item-name-and-badges">
>{{ itemName }}</a-link> <a-link
<span v-else>{{ itemName }}</span><template v-if="this.itemHackage">&nbsp;(<a v-if="itemLink"
target="_blank" class="category-item-name"
:href="`https://hackage.haskell.org/package/${this.itemHackage}`" :url="itemLink"
>Hackage</a>)</template> openInNewTab
>{{ itemName }}</a-link>
<span class="category-item-name" v-else>{{ itemName }}</span>
<div class="category-item-badges">
<a
v-if="this.itemHackage"
class="text-h6 hackage-link"
target="_blank"
:href="`https://hackage.haskell.org/package/${this.itemHackage}`"
>
<v-icon
color="#fff"
class="mr-1"
size="12"
>$vuetify.icons.link</v-icon>hackage</a>
</div>
</div>
</span>
</v-toolbar-title> </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-toolbar-items class="category-item-toolbar-btns">
<category-item-btn
title="Move item up"
icon="arrow-up"
@click="moveItem('up')"
/>
<category-item-btn <v-toolbar-items>
title="Move item down" <div class="category-item-toolbar-btns">
icon="arrow-down" <category-item-btn
@click="moveItem('down')" iconSize="18"
/> title="Move item up"
icon="arrow-up"
@click="moveItem('up')"
/>
<category-item-btn
iconSize="18"
title="Move item down"
icon="arrow-down"
@click="moveItem('down')"
/>
<category-item-btn
iconSize="18"
title="Edit item info"
icon="cog"
@click="toggleEditItemInfoMenu"
>
<v-icon
v-if="isItemInfoEdited"
class="unsaved-changes-icon"
color="#6495ed"
size="8"
>$vuetify.icons.circle</v-icon>
</category-item-btn>
<category-item-btn <category-item-btn
title="Edit item info" iconSize="18"
icon="cog" title="Delete item"
@click="toggleEditItemInfoMenu" icon="trash-alt"
> @click="deleteItem"
<v-icon />
v-if="isItemInfoEdited" </div>
class="edit-item-info-changed-icon"
color="#6495ed"
size="8"
>$vuetify.icons.circle</v-icon>
</category-item-btn>
<category-item-btn <v-menu bottom left offset-y>
title="Delete item" <template v-slot:activator="{ on }">
icon="trash-alt" <v-btn
@click="deleteItem" flat
/> icon
title="Actions"
class="category-toolbar-mobile-menu-btn"
v-on="on"
>
<v-icon
size="18"
color="grey darken-2"
>$vuetify.icons.bars</v-icon>
</v-btn>
</template>
<v-list class="category-item-toolbar-mobile-menu-list">
<v-list-tile>
<category-item-btn
showTitle
iconSize="18"
title="Move item up"
icon="arrow-up"
@click="moveItem('up')"
/>
</v-list-tile>
<v-list-tile>
<category-item-btn
showTitle
iconSize="18"
title="Move item down"
icon="arrow-down"
@click="moveItem('down')"
/>
</v-list-tile>
<v-list-tile>
<category-item-btn
showTitle
iconSize="18"
title="Edit item info"
icon="cog"
@click="toggleEditItemInfoMenu"
>
<v-icon
v-if="isItemInfoEdited"
class="unsaved-changes-icon"
color="#6495ed"
size="8"
>$vuetify.icons.circle</v-icon>
</category-item-btn>
</v-list-tile>
<v-list-tile>
<category-item-btn
showTitle
iconSize="18"
title="Delete item"
icon="trash-alt"
@click="deleteItem"
/>
</v-list-tile>
</v-list>
</v-menu>
</v-toolbar-items> </v-toolbar-items>
</v-toolbar> </v-toolbar>
<v-layout column class="pa-3"> <v-layout column class="pa-3">
<v-flex> <v-flex>
<v-text-field <v-form @keydown.native.enter.ctrl="updateItemInfo">
v-model="itemNameEdit" <v-text-field
label="Name" v-model="itemNameEdit"
/> label="Name"
<v-text-field />
v-model="itemHackageEdit" <v-text-field
label="Name on Hackage (optional)" v-model="itemHackageEdit"
/> label="Name on Hackage (optional)"
<v-text-field />
v-model="itemLinkEdit" <v-text-field
label="Site (optional)" v-model="itemLinkEdit"
/> label="Site (optional)"
/>
</v-form>
</v-flex> </v-flex>
<v-flex align-self-end> <v-flex align-self-end>
<v-btn <v-btn
title="Cancel"
@click="resetAndToggleEditItemInfoMenu"
>
Cancel
</v-btn>
<v-btn
color="info"
class="mr-0" class="mr-0"
title="Save" title="Save"
:disabled="!isInfoSaveEnabled" :disabled="!isInfoSaveEnabled"
@ -135,7 +232,17 @@ export default class CategoryItemToolbar extends Vue {
this.isEditItemInfoMenuOpen = !this.isEditItemInfoMenuOpen this.isEditItemInfoMenuOpen = !this.isEditItemInfoMenuOpen
} }
resetAndToggleEditItemInfoMenu () {
this.itemNameEdit = this.itemName
this.itemLinkEdit = this.itemLink
this.itemHackageEdit = this.itemHackage
this.toggleEditItemInfoMenu()
}
async updateItemInfo (): Promise<void> { async updateItemInfo (): Promise<void> {
if (!this.isInfoSaveEnabled) {
return
}
await this.$store.dispatch('categoryItem/updateItemInfo', { await this.$store.dispatch('categoryItem/updateItemInfo', {
id: this.itemUid, id: this.itemUid,
body: { body: {
@ -172,31 +279,128 @@ export default class CategoryItemToolbar extends Vue {
} }
</script> </script>
<style scoped> <style lang="postcss" scoped>
.category-item-toolbar {
display: flex;
margin: 0;
box-shadow: none;
>>> {
.v-toolbar__content {
justify-content: space-between;
height: auto !important;
min-height: 56px;
.v-toolbar__title {
flex-wrap: wrap;
flex: 1;
overflow: visible;
padding: 12px 0;
}
.spacer {
display: none;
}
.v-toolbar__items {
height: 100%;
margin-top: 10px;
align-self: baseline;
margin-left: 5px;
}
@media screen and (max-width: 959px) {
padding: 0 8px;
}
}
.v-expansion-panel__header {
padding: 0;
align-items: center;
cursor: unset;
}
.v-expansion-panel__body {
background: rgb(222, 222, 222);
}
}
}
.category-item-toolbar-title {
display: inline-flex;
}
.category-item-anchor { .category-item-anchor {
color: rgb(151, 151, 151); color: rgb(151, 151, 151);
} }
.edit-item-info-changed-icon { .category-item-name-and-badges {
margin-left: 0.4rem;
}
.category-item-name {
white-space: pre-line;
word-break: break-word;
}
.category-item-badges {
display: inline-block;
margin-left: 8px;
> * {
vertical-align: middle;
display: inline-flex;
align-items: center;
padding: 0.1px 5px;
border-radius: 5px;
}
.hackage-link {
background: #5e5184;
color: #fff;
}
}
.unsaved-changes-icon {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
right: 5px; right: 5px;
} }
.category-item-toolbar >>> .v-toolbar__title {
overflow: visible; .category-item-toolbar-btns {
}
.category-item-toolbar {
display: flex; display: flex;
box-shadow: none;
}
.category-item-toolbar >>> .v-expansion-panel__header {
padding: 0;
align-items: center; align-items: center;
cursor: unset; flex: 1;
} }
.category-item-toolbar >>> .v-expansion-panel__body { .category-toolbar-mobile-menu-btn {
background: #d6d6d6; display: none;
margin: 0;
} }
.category-item-toolbar-btns > * {
margin: 0 2px; .category-item-toolbar-mobile-menu-list {
>>> .v-list__tile {
height: 36px;
padding: 0 6px;
}
>>> button {
width: 100%;
padding: 5px;
.v-btn__content {
justify-content: flex-start;
}
}
}
@media (max-width: 768px) {
.category-toolbar-mobile-menu-btn {
display: block;
}
.category-item-toolbar-btns {
display: none;
}
.category-item-badges {
display: block;
margin: 5px 0 0 0;
}
.unsaved-changes-icon {
right: unset;
left: 13px;
}
} }
</style> </style>

View File

@ -202,9 +202,6 @@ export default class CategoryItemTraits extends Vue {
.category-item-trait { .category-item-trait {
padding-right: 24px; padding-right: 24px;
} }
.category-item-trait:not(:last-child) {
margin-bottom: 2px;
}
.category-item-edit-trait-menu { .category-item-edit-trait-menu {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -1,15 +1,14 @@
<template> <template>
<v-dialog <v-dialog
lazy lazy
:value="value"
persistent persistent
max-width="99vw" :value="value"
> >
<slot slot="activator" /> <slot slot="activator" />
<div class="conflict-box"> <div class="conflict-box">
<div class="conflict-item"> <div class="conflict-item">
<p class="title mb-2">Your version</p> <h2 class="mt-0">Your version</h2>
<v-card <v-card
color="#fdd" color="#fdd"
class="conflict-content" class="conflict-content"
@ -17,8 +16,7 @@
<v-card-text>{{modified}}</v-card-text> <v-card-text>{{modified}}</v-card-text>
</v-card> </v-card>
<v-btn <v-btn
depressed class="conflict-dialog-btn"
small
title="Submit this version, disregard changes on server" title="Submit this version, disregard changes on server"
@click="save(modified)" @click="save(modified)"
> >
@ -26,7 +24,7 @@
</v-btn> </v-btn>
</div> </div>
<div class="conflict-item"> <div class="conflict-item">
<p class="title mb-2">Version on server</p> <h2 class="mt-0">Version on server</h2>
<v-card <v-card
color="#cfc" color="#cfc"
class="conflict-content" class="conflict-content"
@ -34,29 +32,40 @@
<v-card-text>{{serverModified}}</v-card-text> <v-card-text>{{serverModified}}</v-card-text>
</v-card> </v-card>
<v-btn <v-btn
depressed class="conflict-dialog-btn"
small title="Submit this version, disregard my changes"
title="Accept this version, disregard my changes"
@click="save(serverModified)" @click="save(serverModified)"
> >
Accept this version, disregard my changes Accept this version, disregard my changes
</v-btn> </v-btn>
</div> </div>
<div class="conflict-item"> <div class="conflict-item">
<p class="title mb-2">Merged version</p> <h2 class="mt-0">Merged version</h2>
<markdown-editor <markdown-editor
class="mb-2"
toolbar toolbar
:value="merged" class="conflict-content_markdown"
height="auto"
v-model="mergedEdit"
:autofocus="false"
:bottomToolbar="false"
@save="save" @save="save"
/> />
<v-btn
class="conflict-dialog-btn"
title="Submit merged version"
@click="save(mergedEdit)"
>
Submit the merged version
</v-btn>
</div> </div>
</div> </div>
</v-dialog> </v-dialog>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop, Watch } from 'vue-property-decorator'; import Vue from 'vue'
import Component from 'vue-class-component'
import { Prop, Watch } from 'vue-property-decorator'
import MarkdownEditor from 'client/components/MarkdownEditor.vue' import MarkdownEditor from 'client/components/MarkdownEditor.vue'
@Component({ @Component({
@ -70,6 +79,13 @@ export default class ConflictDialog extends Vue {
@Prop(String) modified!: string @Prop(String) modified!: string
@Prop(String) merged!: string @Prop(String) merged!: string
mergedEdit = this.merged
@Watch('merged')
onMergedChange (newVal) {
this.mergedEdit = newVal
}
save (newValue: string) { save (newValue: string) {
this.$emit('save', newValue) this.$emit('save', newValue)
} }
@ -77,45 +93,88 @@ export default class ConflictDialog extends Vue {
} }
</script> </script>
<style scoped> <style lang="postcss" scoped>
>>> .v-dialog {
overflow-x: hidden;
}
.conflict-box { .conflict-box {
display: flex; display: flex;
background: #fff; background: #fff;
padding: 20px; padding: 20px;
justify-content: space-between; max-height: 90vh;
}
.conflict-content { > *:not(:last-child) {
flex: 1; margin-right: 1.65rem;
margin-bottom: 16px; }
white-space: pre-wrap;
} }
.conflict-item { .conflict-item {
width: 32%;
display: flex; display: flex;
flex-flow: column; max-height: 100%;
} overflow: hidden;
align-items: center;
flex-direction: column;
flex: 1;
@media screen and (max-width: 1200px) { > h2 {
.conflict-box { text-align: center;
flex-wrap: wrap;
}
.conflict-item {
width: 49%;
}
.conflict-item:nth-last-child(1) {
width: 98%;
} }
} }
.conflict-content,
.conflict-content_markdown {
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
margin-bottom: 16px;
white-space: pre-wrap;
overflow: auto;
@media screen and (max-width: 768px) { >>> {
.CodeMirror {
flex: 1;
}
.v-card__text {
word-break: break-word;
}
}
}
.conflict-dialog-btn {
display: block;
margin: 0;
min-height: 60px;
min-width: 220px;
max-height: 60px;
max-width: 220px;
padding: 6px 16px;
font-weight: bold;
font-size: 0.7rem;
>>> .v-btn__content {
width: unset;
white-space: normal;
}
}
/* Without this styling codemirror input area scrolling breaks */
.conflict-content_markdown >>> .CodeMirror-scroll {
min-height: 0 !important;
}
@media screen and (max-width: 900px) {
.conflict-box { .conflict-box {
flex-flow: column; flex-direction: column;
max-height: unset;
> *:not(:last-child) {
margin-right: 0;
margin-bottom: 1.65rem;
}
} }
.conflict-item { /* For every content area to be the same */
width: 100%; .conflict-content,
.conflict-content_markdown {
max-height: 350px;
/* Without height setting codemirror input area is not scrollable */
height: 350px;
} }
} }
</style> </style>

View File

@ -3,8 +3,12 @@
cause easyMDE adds new html elements next to textarea --> cause easyMDE adds new html elements next to textarea -->
<div <div
class="elevation-2" class="elevation-2"
:class="{
'markdown-editor_has-bottom-toolbar': bottomToolbar,
'markdown-editor_has-top-toolbar': toolbar
}"
@keydown.capture.enter="onEnterDown" @keydown.capture.enter="onEnterDown"
@keydown.ctrl.enter="save" @keydown.ctrl.enter="onCtrlEnterDown"
@keydown.esc="cancel" @keydown.esc="cancel"
v-show="editor && isReady" v-show="editor && isReady"
> >
@ -14,7 +18,8 @@
flat flat
height="30" height="30"
color="#e5e5e5" color="#e5e5e5"
class="pa-2 markdown-editor-bottom-toolbar" class="pa-2 markdown-editor__bottom-toolbar"
v-if="bottomToolbar"
v-show="editor" v-show="editor"
> >
<v-toolbar-items> <v-toolbar-items>
@ -55,11 +60,23 @@ export default class MarkdownEditor extends Vue {
default: '' default: ''
}) value: string }) value: string
@Prop({ @Prop({
type: Number, type: [Number, String],
default: 300 default: 300
}) height: number }) height: number | string
@Prop(Boolean) toolbar: boolean @Prop(Boolean) toolbar: boolean
@Prop(Boolean) saveOnEnter: boolean @Prop(Boolean) saveOnEnter: boolean
@Prop({
type: Boolean,
default: true
}) saveOnCtrlEnter: boolean
@Prop({
type: Boolean,
default: true
}) autofocus: boolean
@Prop({
type: Boolean,
default: true
}) bottomToolbar: boolean
editor: object = null editor: object = null
isReady: boolean = false isReady: boolean = false
@ -68,6 +85,12 @@ export default class MarkdownEditor extends Vue {
return `press${this.saveOnEnter ? ' Enter or' : ''} Ctrl+Enter to save` return `press${this.saveOnEnter ? ' Enter or' : ''} Ctrl+Enter to save`
} }
get heightValue () {
return Number(this.height)
? this.height
: `${this.height}px`
}
@Watch('value') @Watch('value')
onValueChange (newVal: string): void { onValueChange (newVal: string): void {
if (!this.editor || this.editor.value() === newVal) { if (!this.editor || this.editor.value() === newVal) {
@ -91,7 +114,7 @@ export default class MarkdownEditor extends Vue {
initialValue: this.value, initialValue: this.value,
spellChecker: false, spellChecker: false,
status: false, status: false,
minHeight: `${this.height}px`, // minHeight: this.heightValue,
toolbar: this.toolbar toolbar: this.toolbar
? [ ? [
'bold', 'bold',
@ -168,14 +191,17 @@ export default class MarkdownEditor extends Vue {
if (!inputAreaEl) { if (!inputAreaEl) {
return return
} }
inputAreaEl.style.height = `${this.height}px` inputAreaEl.style.height = this.heightValue
} }
focusInputArea () { focusInputArea () {
if (!this.autofocus) {
return
}
// this function is triggered right after isReady set to true // this function is triggered right after isReady set to true
// isReady controls v-show of entire markup of component // isReady controls v-show of entire markup of component
// nextTick is used cause html needs to be rendered after v-show triggered so focus will work // nextTick is used cause html needs to be rendered after v-show triggered so focus will work
this.$nextTick(() => document.querySelector('.CodeMirror textarea').focus()) this.$nextTick(() => this.editor.codemirror.focus())
} }
onEnterDown (event: KeyboardEvent) { onEnterDown (event: KeyboardEvent) {
@ -185,6 +211,13 @@ export default class MarkdownEditor extends Vue {
} }
} }
onCtrlEnterDown (event) {
if (this.saveOnCtrlEnter) {
event.preventDefault()
this.save()
}
}
save () { save () {
this.$emit('save', this.editor.value()) this.$emit('save', this.editor.value())
} }
@ -196,18 +229,15 @@ export default class MarkdownEditor extends Vue {
</script> </script>
<style lang="postcss" scoped> <style lang="postcss" scoped>
>>> .editor-toolbar,
>>> .CodeMirror { >>> .CodeMirror {
border: none; border: 1px solid #bbb;
border-radius: 0; border-radius: 0;
border-bottom: 1px solid #bbb;
}
>>> .CodeMirror {
/* Fixes cutting of bottom edge of input /* Fixes cutting of bottom edge of input
https://github.com/sparksuite/simplemde-markdown-editor/issues/619 https://github.com/sparksuite/simplemde-markdown-editor/issues/619
*/ */
box-sizing: content-box; box-sizing: content-box;
font-size: 16px; font-size: 1rem;
.cm-header-1 { .cm-header-1 {
font-size: 2rem; font-size: 2rem;
@ -229,10 +259,21 @@ export default class MarkdownEditor extends Vue {
background: unset; background: unset;
} }
} }
.markdown-editor_has-top-toolbar >>> .CodeMirror {
border-top: 1px solid #ddd;
}
.markdown-editor_has-bottom-toolbar >>> .CodeMirror {
border-bottom: 1px solid #ddd;
}
.markdown-editor__bottom-toolbar {
border: 1px solid #bbb !important;
border-top: none !important;
}
>>> .v-toolbar__content { >>> .v-toolbar__content {
padding-left: 0; padding: 0;
} }
.markdown-editor-save-tip { .markdown-editor-save-tip {
font-size: 11px; font-size: 11px;
line-height: 14px;
} }
</style> </style>

View File

@ -28,6 +28,10 @@ export default class SearchField extends Vue {
setSearchInput (value: string) { setSearchInput (value: string) {
this.$store.commit('wiki/setSearchInput', value) this.$store.commit('wiki/setSearchInput', value)
} }
focus () {
this.$children[0].focus()
}
} }
</script> </script>

View File

@ -1,16 +1,31 @@
<template> <template>
<v-toolbar dark app> <v-toolbar dark app>
<v-toolbar-title> <v-toolbar-title>
<logo /> <logo
:class="{ 'mobile-hidden': !isSearchFieldHidden }"
/>
</v-toolbar-title> </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<search-field /> <search-field
:class="{ 'mobile-hidden': isSearchFieldHidden }"
ref="searchField"
/>
<v-btn
flat
icon
title="Search"
color="#fff"
class="mobile-displayed"
@click="toggleSearchField"
>
<v-icon size="20">$vuetify.icons.search</v-icon>
</v-btn>
</v-toolbar> </v-toolbar>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue' import Vue from 'vue'
import SearchField from 'client/components/Search.vue' import SearchField from 'client/components/SearchField.vue'
import Logo from 'client/components/Logo.vue' import Logo from 'client/components/Logo.vue'
import Component from 'vue-class-component' import Component from 'vue-class-component'
import axios from 'axios' import axios from 'axios'
@ -21,11 +36,34 @@ import axios from 'axios'
Logo Logo
} }
}) })
export default class Toolbar extends Vue {} export default class Toolbar extends Vue {
isSearchFieldHidden: boolean = true
toggleSearchField () {
this.isSearchFieldHidden = !this.isSearchFieldHidden
if (!this.isSearchFieldHidden) {
this.$nextTick(() => this.$refs.searchField.focus())
}
}
}
</script> </script>
<style scoped> <style lang="postcss" scoped>
>>> .v-toolbar__content { >>> .v-toolbar__content {
height: 64px !important; height: 64px !important;
} }
.mobile-hidden {
display: block;
}
.mobile-displayed {
display: none;
}
@media screen and (max-width: 475px) {
.mobile-hidden {
display: none;
}
.mobile-displayed {
display: block;
}
}
</style> </style>