Improved tabletop creature bar

This commit is contained in:
Stefan Zermatten
2023-07-17 15:56:30 +02:00
parent a568510f43
commit 6dcce2e36a
7 changed files with 248 additions and 81 deletions

View File

@@ -0,0 +1,30 @@
<template
lang="html"
functional
>
<component
:is="model.type"
v-if="model && components[model.type]"
/>
<v-card v-else-if="model">
<v-card-title class="text--error">
A property card for the {{ model.type }} isn't defined. You should report this error.
</v-card-title>
</v-card>
</template>
<script lang="js">
import ActionCard from '/imports/client/ui/properties/components/actions/ActionCard.vue';
export default {
components: {
action: ActionCard,
},
props: {
model: {
type: Object,
default: undefined,
},
},
}
</script>

View File

@@ -1,45 +1,9 @@
<template>
<dialog-base>
<template #toolbar-extension>
<v-tabs
v-if="creature && creature.settings"
:value="$store.getters.tabById(creatureId)"
:color="$vuetify.theme.themes.dark.primary"
class="flex"
style="min-width: 0"
centered
grow
max="100px"
@change="e => $store.commit(
'setTabForCharacterSheet',
{id: creatureId, tab: e}
)"
>
<v-tab>
Stats
</v-tab>
<v-tab>
Actions
</v-tab>
<v-tab v-if="!creature.settings.hideSpellsTab">
Spells
</v-tab>
<v-tab>
Inventory
</v-tab>
<v-tab>
Features
</v-tab>
<v-tab>
Journal
</v-tab>
<v-tab>
Build
</v-tab>
<v-tab v-if="creature.settings.showTreeTab">
Tree
</v-tab>
</v-tabs>
<template #toolbar>
<v-toolbar-title>
{{ creature && creature.name }}
</v-toolbar-title>
</template>
<template #unwrapped-content>
<character-sheet
@@ -48,6 +12,47 @@
:creature-id="creatureId"
/>
</template>
<v-bottom-navigation
slot="actions"
shift
mandatory
class="bottom-nav-btns"
style="position: relative;"
:value="$store.getters.tabById(creatureId)"
@change="e => $store.commit(
'setTabForCharacterSheet',
{id: creatureId, tab: e}
)"
>
<v-btn>
<span>Stats</span>
<v-icon>mdi-chart-box</v-icon>
</v-btn>
<v-btn>
<span>Actions</span>
<v-icon>mdi-lightning-bolt</v-icon>
</v-btn>
<v-btn v-if="!creature.settings.hideSpellsTab">
<span>Spells</span>
<v-icon>mdi-fire</v-icon>
</v-btn>
<v-btn>
<span>Inventory</span>
<v-icon>mdi-cube</v-icon>
</v-btn>
<v-btn>
<span>Features</span>
<v-icon>mdi-text</v-icon>
</v-btn>
<v-btn>
<span>Journal</span>
<v-icon>mdi-book-open-variant</v-icon>
</v-btn>
<v-btn>
<span>Build</span>
<v-icon>mdi-wrench</v-icon>
</v-btn>
</v-bottom-navigation>
</dialog-base>
</template>
@@ -67,15 +72,26 @@ export default {
required: true,
},
},
// @ts-ignore
meteor: {
creature() {
if (!this.creatureId) return;
return Creatures.findOne(this.creatureId);
},
},
data(){return {
tab: 0,
}},
}
</script>
<style lang="css" scoped>
.bottom-nav-btns {
margin: -8px;
box-shadow: none !important;
flex-grow: 1;
max-width: unset !important;
}
.bottom-nav-btns > .v-btn{
min-width: 0 !important;
padding: 0 !important;
flex: 1 1 auto !important;
font-size: 0.6rem !important;
}
</style>

View File

@@ -1,14 +1,19 @@
<template lang="html">
<v-btn
icon
plain
:plain="!selected"
large
@click="$emit('click')"
@mouseover="$emit('hover', $event)"
tile
:outlined="selected"
:color="prop && prop.color"
@click.prevent="$emit('click', $event)"
@mouseenter="$emit('mouseenter', $event)"
@mouseleave="$emit('mouseleave', $event)"
>
<property-icon
v-if="prop"
:model="prop"
:color="prop.color"
/>
<v-icon v-else-if="icon">
{{ icon }}
@@ -35,7 +40,8 @@ export default {
icon: {
type: String,
default: undefined,
}
},
selected: Boolean,
},
meteor: {
prop() {

View File

@@ -1,8 +1,8 @@
<template lang="html">
<div
v-if="creatureId"
class="selected-creature-bar d-flex pa-4"
style="gap: 8px;"
class="selected-creature-bar d-flex pa-3 justify-center"
style="gap: 8px; min-width: 100%;"
>
<!--
<tabletop-buff-icons
@@ -15,17 +15,42 @@
/>
-->
<v-menu
v-model="selectedProp"
v-model="menuOpen"
v-click-outside="{
handler: clickOutsideMenu,
include: menuClickOutsideInclude,
}"
:position-x="menuX"
:position-y="menuY"
absolute
offset-y
top
:nudge-left="150"
origin="center bottom"
:close-on-click="false"
:content-class="`tabletop-prop-menu rows-${rows}`"
:close-on-content-click="false"
>
<tabletop-action-card
v-if="selectedProp && selectedProp.type === 'action'"
style="width: 300px;"
:style="{
width: '300px',
opacity: selectedIcon ? 1 : 0.5,
transition: 'opacity 0.2s ease',
}"
:model="selectedProp"
/>
<v-card
v-else-if="activeIcon && activeIcon.tab"
style="width: 300px"
>
<v-card-title>
<v-icon left>
{{ activeIcon.icon }}
</v-icon>
{{ activeIcon.tabName }}
</v-card-title>
</v-card>
</v-menu>
<v-card
v-for="group in iconGroups"
@@ -43,8 +68,11 @@
:key="icon.propId || iconIndex"
:prop-id="icon.propId"
:icon="icon.icon"
@click="selectedIcon = icon"
@hover="e => {selectedIcon = icon; menuX = e.clientX; menuY = e.clientY; log(e)}"
:selected="selectedIcon === icon"
:data-id="icon.propId || icon.standardId"
@click="e => selectIcon(e, icon)"
@mouseenter="e => hoverIcon(e, icon)"
@mouseleave="unHoverIcon(icon)"
/>
</template>
</div>
@@ -101,19 +129,94 @@ export default {
data() {
return {
rows: 2,
hoveredIcon: undefined,
selectedIcon: undefined,
menuOpen: false,
menuX: 200,
menuY: 200,
menuY: window.innerHeight - 200,
};
},
computed: {
activeIcon() {
return this.selectedIcon || this.hoveredIcon;
}
},
watch: {
menuOpen(val) {
if (!val && this.selectIcon) {
this.selectedIcon = undefined;
}
},
},
methods: {
log(e) {
console.log(e);
},
hoverIcon(e, icon) {
if (this.selectedIcon) return;
// this.menuX = e.clientX - (e.clientX % 44);
const { left, right } = e.target.getBoundingClientRect();
const x = ( left + right ) / 2
this.menuX = x;
this.hoveredIcon = icon;
this.menuOpen = true;
},
unHoverIcon(icon) {
if (this.hoveredIcon === icon) {
this.hoveredIcon = undefined;
if (!this.selectedIcon) {
this.menuOpen = false;
}
}
},
selectIcon(e, icon) {
if (icon.tab) {
this.openCharacterSheet(icon.tab, icon.standardId);
return;
}
if (this.selectedIcon === icon) {
this.selectedIcon = undefined;
this.menuOpen = false;
return;
}
const { left, right } = e.target.getBoundingClientRect();
const x = ( left + right ) / 2
this.menuX = x;
this.selectedIcon = icon;
this.menuOpen = true;
},
clickOutsideMenu () {
this.menuOpen = false;
},
menuClickOutsideInclude() {
return [
document.querySelector('.selected-creature-bar'),
document.querySelector('.tabletop-prop-menu')
];
},
openCharacterSheet(tab, elementId) {
this.$store.commit(
'setTabForCharacterSheet',
{ id: this.creatureId, tab }
);
this.$store.commit('pushDialogStack', {
component: 'character-sheet-dialog',
elementId,
data: {
creatureId: this.creatureId,
},
});
},
},
meteor: {
creature() {
if (!this.creatureId) return;
return Creatures.findOne(this.creatureId)
},
selectedProp() {
if (!this.selectedIcon?.propId) return;
return CreatureProperties.findOne(this.selectedIcon.propId);
const propId = this.activeIcon?.propId;
if (!propId) return;
return CreatureProperties.findOne(propId);
},
iconGroups() {
if (!this.creature) return;
@@ -121,16 +224,16 @@ export default {
// Get the standard icons
const standardIconsById = {
'cast-spell': { groupName: 'Standard Actions', icon: 'mdi-fire' },
'make-check': { groupName: 'Standard Actions', icon: 'mdi-radiobox-marked' },
'roll-dice': { groupName: 'Standard Actions', icon: 'mdi-dice-d20' },
'tab-stats': { groupName: 'Tabs', icon: 'mdi-chart-box' },
'tab-actions': { groupName: 'Tabs', icon: 'mdi-lightning-bolt' },
'tab-spells': { groupName: 'Tabs', icon: 'mdi-fire' },
'tab-inventory': { groupName: 'Tabs', icon: 'mdi-cube' },
'tab-features': { groupName: 'Tabs', icon: 'mdi-text' },
'tab-journal': { groupName: 'Tabs', icon: 'mdi-book-open-variant' },
'tab-build': { groupName: 'Tabs', icon: 'mdi-wrench' },
'cast-spell': {standardId: 'cast-spell', groupName: 'Standard Actions', icon: 'mdi-fire' },
'make-check': {standardId: 'make-check', groupName: 'Standard Actions', icon: 'mdi-radiobox-marked' },
'roll-dice': {standardId: 'roll-dice', groupName: 'Standard Actions', icon: 'mdi-dice-d20' },
'tab-stats': {standardId: 'tab-stats', groupName: 'Tabs', icon: 'mdi-chart-box', tab: 'stats', tabName: 'Stats' },
'tab-actions': {standardId: 'tab-actions', groupName: 'Tabs', icon: 'mdi-lightning-bolt', tab: 'actions', tabName: 'Actions' },
'tab-spells': this.creature?.settings?.hideSpellsTab ? undefined : {standardId: 'tab-spells', groupName: 'Tabs', icon: 'mdi-fire', tab: 'spells', tabName: 'Spells' },
'tab-inventory': {standardId: 'tab-inventory', groupName: 'Tabs', icon: 'mdi-cube', tab: 'inventory', tabName: 'Inventory' },
'tab-features': {standardId: 'tab-features', groupName: 'Tabs', icon: 'mdi-text', tab: 'features', tabName: 'Features' },
'tab-journal': {standardId: 'tab-journal', groupName: 'Tabs', icon: 'mdi-book-open-variant', tab: 'journal', tabName: 'Journal' },
'tab-build': {standardId: 'tab-build', groupName: 'Tabs', icon: 'mdi-wrench', tab: 'build', tabName: 'Build' },
};
// Get the folders that could hide a property
@@ -226,7 +329,8 @@ export default {
// Add default groups for standard icons
for (let key in standardIconsById) {
const standardIcon = standardIconsById[key];
if (standardIcon._placedInGroup) return;
if (!standardIcon) continue;
if (standardIcon._placedInGroup) continue;
const groupName = standardIcon.groupName || 'no';
if (!groupsByName[groupName]) {
@@ -234,7 +338,7 @@ export default {
defaultGroups.push(groupsByName[groupName]);
}
groupsByName[groupName].iconList.push({ standardId: key, icon: standardIcon.icon });
groupsByName[groupName].iconList.push(standardIcon);
}
iconGroups.push(...defaultGroups);
@@ -247,16 +351,24 @@ export default {
return iconGroups;
}
},
methods: {
selectIcon(e) {
this.$emit('select-icon', e);
},
log(e) {
console.log(e);
},
}
}
</script>
<style lang="css" scoped>
<style lang="css">
.tabletop-prop-menu {
top: unset !important;
transition: all 0.2s ease;
}
.tabletop-prop-menu.rows-1 {
bottom: 68px;
}
.tabletop-prop-menu.rows-2 {
bottom: 112px;
}
.tabletop-prop-menu.rows-3 {
bottom: 156px;
}
.tabletop-prop-menu.rows-4 {
bottom: 200px;
}
</style>

View File

@@ -1,7 +1,7 @@
import Vue from 'vue';
//import Vuetify from 'vuetify/lib';
import Vuetify from 'vuetify/lib/framework';
import { Scroll, Ripple } from 'vuetify/lib/directives';
import { Scroll, Ripple, ClickOutside } from 'vuetify/lib/directives';
import SVG_ICONS from '/imports/constants/SVG_ICONS.js';
import SvgIconByName from '/imports/client/ui/icons/SvgIconByName.vue';
import themes from '/imports/client/ui/themes.js';
@@ -11,6 +11,7 @@ Vue.use(Vuetify, {
directives: {
Scroll,
Ripple,
ClickOutside,
},
});

View File

@@ -56,6 +56,7 @@ const store = new Vuex.Store({
setTabForCharacterSheet(state, { tab, id }) {
// Convert tab names to tab numbers
if (typeof tab === 'string') {
const tabInput = tab;
const creature = Creatures.findOne(id);
if (creature?.settings?.hideSpellsTab) {
tab = tabsWithoutSpells.indexOf(tab);
@@ -63,9 +64,9 @@ const store = new Vuex.Store({
tab = tabs.indexOf(tab);
}
if (!(tab > -1)) {
throw 'Could not find requested tab';
console.warn(`could not find a tab called ${tabInput}`);
tab = 0;
}
console.log('resolved: ', tab);
}
Vue.set(state.characterSheetTabs, id, tab);
},

View File

@@ -50,6 +50,7 @@ Meteor.publish('tabletop', function (tabletopId) {
avatarPicture: 1,
tabletop: 1,
initiativeRoll: 1,
settings: 1,
},
});
const creatureIds = creatureSummaries.map(c => c._id);