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:
parent
df1d2ee73b
commit
450a5ee46c
@ -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;
|
||||
|
@ -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>
|
@ -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
|
||||
},
|
||||
|
||||
|
@ -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
|
||||
},
|
||||
|
||||
|
191
src/components/sidebar/SidebarSection.vue
Normal file
191
src/components/sidebar/SidebarSection.vue
Normal 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>
|
@ -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
4
src/index.d.ts
vendored
@ -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;
|
||||
|
@ -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,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user