Refactor sidebar sections

- Rename CollapsibleSection to SidebarSection with a collapsible prop
- Move section__content element to SidebarSection
- Move sidebar section styling to SidebarSection
This commit is contained in:
James Lyne 2022-01-10 19:36:06 +00:00
parent df1d2ee73b
commit 450a5ee46c
10 changed files with 256 additions and 198 deletions

View File

@ -152,8 +152,6 @@ export default defineComponent({
</script>
<style lang="scss">
@import '../scss/placeholders';
.sidebar {
position: fixed;
z-index: 110;
@ -230,80 +228,6 @@ export default defineComponent({
}
}
.sidebar__section {
@extend %panel;
margin-bottom: var(--ui-element-spacing);
box-sizing: border-box;
width: 100%;
max-width: 25rem;
flex: 0 0 auto;
.section__heading {
cursor: pointer;
user-select: none;
text-align: left;
align-items: center;
margin: 0;
button {
display: flex;
font-size: 2rem;
padding: 1.5rem 1.5rem 1rem;
margin: -1.5rem -1.5rem 0;
background-color: transparent;
font-weight: 400;
color: inherit;
width: calc(100% + 3rem);
align-items: center;
text-shadow: var(--text-shadow);
.svg-icon {
margin-left: auto;
width: 1.5rem;
height: 1.5rem;
}
}
&:hover, &:focus-visible, &.focus-visible, &:active {
background-color: transparent;
}
}
.section__content {
padding: 0 0.5rem;
margin: 0 -.5rem 1rem;
min-width: 0;
&:last-child {
margin-bottom: 0;
}
}
.section__skeleton {
font-style: italic;
color: var(--text-disabled);
text-align: center;
align-self: center;
margin-top: 1rem;
}
&.section--collapsed {
.section__heading button {
padding-bottom: 1.5rem;
margin-bottom: -1.5rem;
}
.section__content {
display: none;
}
}
@media (max-width: 320px) {
box-sizing: border-box;
width: 100%;
}
}
@media (max-width: 480px) {
padding-right: 7rem;
padding-top: 0.8rem;

View File

@ -1,82 +0,0 @@
<!--
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<section :class="{'sidebar__section': true, 'section--collapsible': true, 'section--collapsed': collapsed}">
<h2 class="section__heading">
<button :id="`${name}-heading`" type="button"
@click.prevent="toggle" :title="title"
:aria-expanded="!collapsed" :aria-controls="`${name}-content`">
<span>
<slot name="heading"></slot>
</span>
<SvgIcon name="arrow"></SvgIcon>
</button>
</h2>
<div :id="`${name}-content`" :aria-hidden="collapsed">
<slot></slot>
</div>
</section>
</template>
<script lang="ts">
import {useStore} from "@/store";
import {LiveAtlasSidebarSection} from "@/index";
import {defineComponent} from "@vue/runtime-core";
import SvgIcon from "@/components/SvgIcon.vue";
import '@/assets/icons/arrow.svg';
import {MutationTypes} from "@/store/mutation-types";
import {computed} from "vue";
export default defineComponent({
name: 'CollapsibleSection',
components: {SvgIcon},
props: {
name: {
type: String as () => LiveAtlasSidebarSection,
required: true,
}
},
setup(props) {
const store = useStore(),
title = computed(() => store.state.messages.toggleTitle),
collapsed = computed(() => store.state.ui.sidebar.collapsedSections.has(props.name));
const toggle = () => store.commit(MutationTypes.TOGGLE_SIDEBAR_SECTION_COLLAPSED_STATE, props.name);
return {
title,
collapsed,
toggle
}
}
});
</script>
<style lang="scss" scoped>
.section--collapsible {
.section__heading .svg-icon {
transform: rotate(180deg);
}
&.section--collapsed {
.section__heading .svg-icon {
transform: none;
}
}
}
</style>

View File

@ -15,35 +15,33 @@
-->
<template>
<CollapsibleSection name="players" class="players">
<SidebarSection name="players" class="players">
<template v-slot:heading>{{ messageHeading }}</template>
<template v-slot:default>
<div class="section__content">
<input v-if="players && searchEnabled" id="players__search" type="text" name="search"
v-model="searchQuery" :placeholder="messagePlayersSearchPlaceholder" @keydown="onKeydown">
<RadioList v-if="filteredPlayers.length" aria-labelledby="players-heading">
<PlayerListItem v-for="player in filteredPlayers" :key="player.name"
:player="player"></PlayerListItem>
</RadioList>
<div v-else-if="searchQuery" class="section__skeleton">{{ messageSkeletonPlayersSearch }}</div>
<div v-else class="section__skeleton">{{ messageSkeletonPlayers }}</div>
</div>
<input v-if="players && searchEnabled" id="players__search" type="text" name="search"
v-model="searchQuery" :placeholder="messagePlayersSearchPlaceholder" @keydown="onKeydown">
<RadioList v-if="filteredPlayers.length" aria-labelledby="players-heading">
<PlayerListItem v-for="player in filteredPlayers" :key="player.name"
:player="player"></PlayerListItem>
</RadioList>
<div v-else-if="searchQuery" class="section__skeleton">{{ messageSkeletonPlayersSearch }}</div>
<div v-else class="section__skeleton">{{ messageSkeletonPlayers }}</div>
</template>
</CollapsibleSection>
</SidebarSection>
</template>
<script lang="ts">
import PlayerListItem from "./PlayerListItem.vue";
import {computed, defineComponent} from "@vue/runtime-core";
import {useStore} from "@/store";
import CollapsibleSection from "@/components/sidebar/CollapsibleSection.vue";
import RadioList from "@/components/util/RadioList.vue";
import {ref} from "vue";
import SidebarSection from "@/components/sidebar/SidebarSection.vue";
export default defineComponent({
components: {
SidebarSection,
RadioList,
CollapsibleSection,
PlayerListItem
},

View File

@ -15,28 +15,28 @@
-->
<template>
<CollapsibleSection v-if="servers.size > 1" name="servers">
<SidebarSection v-if="servers.size > 1" name="servers">
<template v-slot:heading>{{ heading }}</template>
<template v-slot:default>
<RadioList class="section__content" aria-labelledby="servers-heading">
<RadioList aria-labelledby="servers-heading">
<ServerListItem :server="server" v-for="[name, server] in servers" :key="name"></ServerListItem>
</RadioList>
</template>
</CollapsibleSection>
</SidebarSection>
</template>
<script lang="ts">
import ServerListItem from './ServerListItem.vue';
import {computed, defineComponent} from 'vue';
import {useStore} from "@/store";
import CollapsibleSection from "@/components/sidebar/CollapsibleSection.vue";
import SidebarSection from "@/components/sidebar/SidebarSection.vue";
import RadioList from "@/components/util/RadioList.vue";
export default defineComponent({
name: 'ServerList',
components: {
RadioList,
CollapsibleSection,
SidebarSection,
ServerListItem
},

View File

@ -0,0 +1,191 @@
<!--
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<section :class="{
'sidebar__section': true,
'section--collapsible': collapsible,
'section--collapsed': collapsed
}" :data-section="name">
<h2 class="section__heading">
<button :id="`${name}-heading`" type="button"
@click.prevent="toggle" :title="title"
:aria-expanded="!collapsed" :aria-controls="`${name}-content`">
<span>
<slot name="heading"></slot>
</span>
<SvgIcon name="arrow"></SvgIcon>
</button>
</h2>
<div :id="`${name}-content`" class="section__content" :aria-hidden="collapsed">
<slot></slot>
</div>
</section>
</template>
<script lang="ts">
import {useStore} from "@/store";
import {LiveAtlasSidebarSection} from "@/index";
import {defineComponent} from "@vue/runtime-core";
import SvgIcon from "@/components/SvgIcon.vue";
import '@/assets/icons/arrow.svg';
import {MutationTypes} from "@/store/mutation-types";
import {computed, ref} from "vue";
export default defineComponent({
name: 'SidebarSection',
components: {SvgIcon},
props: {
name: {
type: String as () => LiveAtlasSidebarSection,
required: true,
},
collapsible: {
type: Boolean,
required: false,
default: true,
}
},
setup(props) {
const store = useStore(),
title = computed(() => store.state.messages.toggleTitle),
collapsed = computed(() => store.state.ui.sidebar[props.name].collapsed),
customPosition = computed(() => store.state.ui.sidebar[props.name].customPosition),
customSize = computed(() => store.state.ui.sidebar[props.name].customSize),
smallScreen = computed(() => store.state.ui.smallScreen),
offsetX = ref(0),
offsetY = ref(0);
const toggle = () => {
if(!props.collapsible) {
return;
}
store.commit(MutationTypes.TOGGLE_SIDEBAR_SECTION_COLLAPSED_STATE, props.name);
}
return {
title,
collapsed,
customPosition,
customSize,
smallScreen,
toggle,
}
}
});
</script>
<style lang="scss">
@import '../../scss/placeholders';
.sidebar__section {
@extend %panel;
margin-bottom: var(--ui-element-spacing);
box-sizing: border-box;
width: 100%;
max-width: 25rem;
flex: 0 0 auto;
.section__heading {
cursor: pointer;
user-select: none;
text-align: left;
align-items: center;
margin: 0;
button {
display: flex;
font-size: 2rem;
padding: 1.5rem 1.5rem 1rem;
margin: -1.5rem -1.5rem 0;
background-color: transparent;
font-weight: 400;
color: inherit;
width: calc(100% + 3rem);
align-items: center;
text-shadow: var(--text-shadow);
.svg-icon {
margin-left: auto;
width: 1.5rem;
height: 1.5rem;
}
}
&:hover, &:focus-visible, &.focus-visible, &:active {
background-color: transparent;
}
}
.section__content {
padding: 0 0.5rem;
margin: 0 -.5rem 1rem;
min-width: 0;
position: relative;
&:last-child {
margin-bottom: 0;
}
}
.section__skeleton {
font-style: italic;
color: var(--text-disabled);
text-align: center;
align-self: center;
margin-top: 1rem;
}
&.section--collapsible {
.section__heading .svg-icon {
transform: rotate(180deg);
}
&.section--collapsed {
.section__heading .svg-icon {
transform: none;
}
.section__heading button {
padding-bottom: 1.5rem;
margin-bottom: -1.5rem;
}
.section__content {
display: none;
}
}
.section__content {
display: flex;
flex-direction: column;
align-items: stretch;
flex-shrink: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
}
}
@media (max-width: 320px) {
box-sizing: border-box;
width: 100%;
}
}
</style>

View File

@ -15,29 +15,29 @@
-->
<template>
<CollapsibleSection name="maps">
<SidebarSection name="maps">
<template v-slot:heading>{{ heading }}</template>
<template v-slot:default>
<RadioList v-if="worlds.size" class="section__content" aria-labelledby="maps-heading">
<RadioList v-if="worlds.size" aria-labelledby="maps-heading">
<WorldListItem :world="world" v-for="[name, world] in worlds" :key="`${prefix}_${currentServer}_${name}`"></WorldListItem>
</RadioList>
<div v-else class="section__content section__skeleton">{{ skeleton }}</div>
<div v-else class="section__skeleton">{{ skeleton }}</div>
</template>
</CollapsibleSection>
</SidebarSection>
</template>
<script lang="ts">
import WorldListItem from './WorldListItem.vue';
import {computed, defineComponent} from 'vue';
import {useStore} from "@/store";
import CollapsibleSection from "@/components/sidebar/CollapsibleSection.vue";
import RadioList from "@/components/util/RadioList.vue";
import SidebarSection from "@/components/sidebar/SidebarSection.vue";
export default defineComponent({
name: 'WorldList',
components: {
SidebarSection,
RadioList,
CollapsibleSection,
WorldListItem
},

4
src/index.d.ts vendored
View File

@ -160,6 +160,10 @@ export type LiveAtlasUIModal = 'login' | 'settings';
export type LiveAtlasSidebarSection = 'servers' | 'players' | 'maps';
export type LiveAtlasDimension = 'overworld' | 'nether' | 'end';
export type LiveAtlasSidebarSectionState = {
collapsed?: boolean;
}
interface LiveAtlasPlayer {
name: string;
displayName: string;

View File

@ -43,7 +43,9 @@ console.info(`LiveAtlas version ${store.state.version} - https://github.com/JLyn
store.subscribe((mutation, state) => {
if(mutation.type === 'toggleSidebarSectionCollapsedState' || mutation.type === 'setSidebarSectionCollapsedState') {
localStorage.setItem('collapsedSections', JSON.stringify(Array.from(state.ui.sidebar.collapsedSections)));
localStorage.setItem('uiSettings', JSON.stringify({
sidebar: state.ui.sidebar,
}));
}
});

View File

@ -39,7 +39,12 @@ import {
LiveAtlasMarker,
LiveAtlasMarkerSet,
LiveAtlasServerDefinition,
LiveAtlasServerConfig, LiveAtlasChat, LiveAtlasPartialComponentConfig, LiveAtlasComponentConfig, LiveAtlasUIModal
LiveAtlasServerConfig,
LiveAtlasChat,
LiveAtlasPartialComponentConfig,
LiveAtlasComponentConfig,
LiveAtlasUIModal,
LiveAtlasSidebarSectionState
} from "@/index";
import DynmapMapProvider from "@/providers/DynmapMapProvider";
import Pl3xmapMapProvider from "@/providers/Pl3xmapMapProvider";
@ -97,12 +102,27 @@ export type Mutations<S = State> = {
export const mutations: MutationTree<State> & Mutations = {
[MutationTypes.INIT](state: State, config: LiveAtlasGlobalConfig) {
const collapsedSections = localStorage.getItem('collapsedSections'),
messageConfig = config?.messages || {},
const messageConfig = config?.messages || {},
uiConfig = config?.ui || {};
if(collapsedSections) {
state.ui.sidebar.collapsedSections = new Set(JSON.parse(collapsedSections));
try {
const uiSettings = JSON.parse(localStorage.getItem('uiSettings') || '{}');
if(uiSettings && uiSettings.sidebar) {
for(const element in uiSettings.sidebar) {
const elementState: LiveAtlasSidebarSectionState = uiSettings.sidebar[element];
if(!elementState) {
continue;
}
if(typeof state.ui.sidebar[element as LiveAtlasSidebarSection] !== 'undefined') {
state.ui.sidebar[element as LiveAtlasSidebarSection].collapsed = !!elementState.collapsed;
}
}
}
} catch(e) {
console.warn('Failed to load saved UI settings', e);
}
const messages: LiveAtlasGlobalMessageConfig = {
@ -615,11 +635,7 @@ export const mutations: MutationTree<State> & Mutations = {
},
[MutationTypes.TOGGLE_SIDEBAR_SECTION_COLLAPSED_STATE](state: State, section: LiveAtlasSidebarSection): void {
if(state.ui.sidebar.collapsedSections.has(section)) {
state.ui.sidebar.collapsedSections.delete(section);
} else {
state.ui.sidebar.collapsedSections.add(section);
}
state.ui.sidebar[section].collapsed = !state.ui.sidebar[section].collapsed;
},
[MutationTypes.SET_LOGGED_IN](state: State, payload: boolean): void {

View File

@ -32,7 +32,10 @@ import {
LiveAtlasPlayer,
LiveAtlasMarkerSet,
LiveAtlasComponentConfig,
LiveAtlasServerConfig, LiveAtlasChat, LiveAtlasUIModal
LiveAtlasServerConfig,
LiveAtlasChat,
LiveAtlasUIModal,
LiveAtlasSidebarSectionState
} from "@/index";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
@ -86,8 +89,8 @@ export type State = {
previouslyVisibleElements: Set<LiveAtlasUIElement>;
sidebar: {
collapsedSections: Set<LiveAtlasSidebarSection>;
}
[K in LiveAtlasSidebarSection]: LiveAtlasSidebarSectionState
};
};
parsedUrl?: LiveAtlasParsedUrl;
@ -265,7 +268,9 @@ export const state: State = {
previouslyVisibleElements: new Set(),
sidebar: {
collapsedSections: new Set(),
servers: {},
players: {},
maps: {},
},
}
};