diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2020-07-11 10:13:11 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-07-11 10:13:11 +0900 |
| commit | cf3fc97202588e835ade5d6ab1a3c087e46958ad (patch) | |
| tree | b3fe472b455bf913a47df4d41b1363c7122bf1d9 /src/client/components | |
| parent | タイムライン上でTwitterウィジットを展開できるようにな... (diff) | |
| download | misskey-cf3fc97202588e835ade5d6ab1a3c087e46958ad.tar.gz misskey-cf3fc97202588e835ade5d6ab1a3c087e46958ad.tar.bz2 misskey-cf3fc97202588e835ade5d6ab1a3c087e46958ad.zip | |
Deck (#6504)
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
Diffstat (limited to 'src/client/components')
22 files changed, 1770 insertions, 85 deletions
diff --git a/src/client/components/deck/antenna-column.vue b/src/client/components/deck/antenna-column.vue new file mode 100644 index 0000000000..83fe14f2cc --- /dev/null +++ b/src/client/components/deck/antenna-column.vue @@ -0,0 +1,80 @@ +<template> +<x-column :menu="menu" :column="column" :is-stacked="isStacked"> + <template #header> + <fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span> + </template> + + <x-timeline ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faSatellite, faCog } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import XTimeline from '../timeline.vue'; + +export default Vue.extend({ + components: { + XColumn, + XTimeline, + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + menu: null, + faSatellite + }; + }, + + watch: { + mediaOnly() { + (this.$refs.timeline as any).reload(); + } + }, + + created() { + this.menu = [{ + icon: faCog, + text: this.$t('antenna'), + action: async () => { + const antennas = await this.$root.api('antennas/list'); + this.$root.dialog({ + title: this.$t('antenna'), + type: null, + select: { + items: antennas.map(x => ({ + value: x, text: x.name + })) + }, + showCancelButton: true + }).then(({ canceled, result: antenna }) => { + if (canceled) return; + this.column.antennaId = antenna.id; + this.$store.commit('deviceUser/updateDeckColumn', this.column); + }); + } + }]; + }, + + methods: { + focus() { + (this.$refs.timeline as any).focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +</style> diff --git a/src/client/components/deck/column-core.vue b/src/client/components/deck/column-core.vue new file mode 100644 index 0000000000..44f19e7eda --- /dev/null +++ b/src/client/components/deck/column-core.vue @@ -0,0 +1,50 @@ +<template> +<!-- TODO: リファクタの余地がありそう --> +<x-widgets-column v-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<x-notifications-column v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<x-tl-column v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<x-list-column v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<x-antenna-column v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<!-- TODO: <x-tl-column v-else-if="column.type === 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> --> +<x-mentions-column v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +<x-direct-column v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XTlColumn from './tl-column.vue'; +import XAntennaColumn from './antenna-column.vue'; +import XListColumn from './list-column.vue'; +import XNotificationsColumn from './notifications-column.vue'; +import XWidgetsColumn from './widgets-column.vue'; +import XMentionsColumn from './mentions-column.vue'; +import XDirectColumn from './direct-column.vue'; + +export default Vue.extend({ + components: { + XTlColumn, + XAntennaColumn, + XListColumn, + XNotificationsColumn, + XWidgetsColumn, + XMentionsColumn, + XDirectColumn + }, + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: false, + default: false + } + }, + methods: { + focus() { + this.$children[0].focus(); + } + } +}); +</script> diff --git a/src/client/components/deck/column.vue b/src/client/components/deck/column.vue new file mode 100644 index 0000000000..f7620e5749 --- /dev/null +++ b/src/client/components/deck/column.vue @@ -0,0 +1,426 @@ +<template> +<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> +<section class="dnpfarvg _panel _narrow_" :class="{ naked, paged: isMainColumn, _close_: !isMainColumn, active, isStacked, draghover, dragging, dropready }" + @dragover.prevent.stop="onDragover" + @dragleave="onDragleave" + @drop.prevent.stop="onDrop" + v-hotkey="keymap" + :style="{ width: `${width}px` }" +> + <header :class="{ indicated }" + draggable="true" + @click="goTop" + @dragstart="onDragstart" + @dragend="onDragend" + @contextmenu.prevent.stop="onContextmenu" + > + <button class="toggleActive _button" @click="toggleActive" v-if="isStacked"> + <template v-if="active"><fa :icon="faAngleUp"/></template> + <template v-else><fa :icon="faAngleDown"/></template> + </button> + <div class="action"> + <slot name="action"></slot> + </div> + <span class="header"><slot name="header"></slot></span> + <button v-if="!isMainColumn" class="menu _button" ref="menu" @click.stop="showMenu"><fa :icon="faCaretDown"/></button> + <button v-else-if="$route.name !== 'index'" class="close _button" @click.stop="close"><fa :icon="faTimes"/></button> + </header> + <div ref="body" v-show="active"> + <slot></slot> + </div> +</section> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { faWindowMaximize, faTrashAlt, faWindowRestore } from '@fortawesome/free-regular-svg-icons'; + +export default Vue.extend({ + props: { + column: { + type: Object, + required: false, + default: null + }, + isStacked: { + type: Boolean, + required: false, + default: false + }, + menu: { + type: Array, + required: false, + default: null + }, + naked: { + type: Boolean, + required: false, + default: false + }, + indicated: { + type: Boolean, + required: false, + default: false + }, + }, + + data() { + return { + active: true, + dragging: false, + draghover: false, + dropready: false, + faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, + }; + }, + + computed: { + isMainColumn(): boolean { + return this.column == null; + }, + + width(): number { + return this.isMainColumn ? 350 : this.column.width; + }, + + keymap(): any { + return { + 'shift+up': () => this.$parent.$emit('parentFocus', 'up'), + 'shift+down': () => this.$parent.$emit('parentFocus', 'down'), + 'shift+left': () => this.$parent.$emit('parentFocus', 'left'), + 'shift+right': () => this.$parent.$emit('parentFocus', 'right'), + }; + } + }, + + watch: { + active(v) { + this.$emit('change-active-state', v); + }, + + dragging(v) { + this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd'); + } + }, + + mounted() { + if (!this.isMainColumn) { + this.$root.$on('deck.column.dragStart', this.onOtherDragStart); + this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd); + } + }, + + beforeDestroy() { + if (!this.isMainColumn) { + this.$root.$off('deck.column.dragStart', this.onOtherDragStart); + this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd); + } + }, + + methods: { + onOtherDragStart() { + this.dropready = true; + }, + + onOtherDragEnd() { + this.dropready = false; + }, + + toggleActive() { + if (!this.isStacked) return; + this.active = !this.active; + }, + + getMenu() { + const items = [{ + icon: faPencilAlt, + text: this.$t('rename'), + action: () => { + this.$root.dialog({ + title: this.$t('rename'), + input: { + default: this.column.name, + allowEmpty: false + } + }).then(({ canceled, result: name }) => { + if (canceled) return; + this.$store.commit('deviceUser/renameDeckColumn', { id: this.column.id, name }); + }); + } + }, null, { + icon: faArrowLeft, + text: this.$t('swap-left'), + action: () => { + this.$store.commit('deviceUser/swapLeftDeckColumn', this.column.id); + } + }, { + icon: faArrowRight, + text: this.$t('swap-right'), + action: () => { + this.$store.commit('deviceUser/swapRightDeckColumn', this.column.id); + } + }, this.isStacked ? { + icon: faArrowUp, + text: this.$t('swap-up'), + action: () => { + this.$store.commit('deviceUser/swapUpDeckColumn', this.column.id); + } + } : undefined, this.isStacked ? { + icon: faArrowDown, + text: this.$t('swap-down'), + action: () => { + this.$store.commit('deviceUser/swapDownDeckColumn', this.column.id); + } + } : undefined, null, { + icon: faWindowRestore, + text: this.$t('stack-left'), + action: () => { + this.$store.commit('deviceUser/stackLeftDeckColumn', this.column.id); + } + }, this.isStacked ? { + icon: faWindowMaximize, + text: this.$t('pop-right'), + action: () => { + this.$store.commit('deviceUser/popRightDeckColumn', this.column.id); + } + } : undefined, null, { + icon: faTrashAlt, + text: this.$t('remove'), + action: () => { + this.$store.commit('deviceUser/removeDeckColumn', this.column.id); + } + }]; + + if (this.menu) { + for (const i of this.menu.reverse()) { + items.unshift(i); + } + } + + return items; + }, + + onContextmenu(e) { + if (this.isMainColumn) return; + this.showMenu(); + }, + + showMenu() { + this.$root.menu({ + items: this.getMenu(), + source: this.$refs.menu, + }); + }, + + close() { + this.$router.push('/'); + }, + + goTop() { + this.$refs.body.scrollTo({ + top: 0, + behavior: 'smooth' + }); + }, + + onDragstart(e) { + // メインカラムはドラッグさせない + if (this.isMainColumn) { + e.preventDefault(); + return; + } + + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('mk-deck-column', this.column.id); + this.dragging = true; + }, + + onDragend(e) { + this.dragging = false; + }, + + onDragover(e) { + // メインカラムにはドロップさせない + if (this.isMainColumn) { + e.dataTransfer.dropEffect = 'none'; + return; + } + + // 自分自身がドラッグされている場合 + if (this.dragging) { + // 自分自身にはドロップさせない + e.dataTransfer.dropEffect = 'none'; + return; + } + + const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column'; + + e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; + + if (!this.dragging && isDeckColumn) this.draghover = true; + }, + + onDragleave() { + this.draghover = false; + }, + + onDrop(e) { + this.draghover = false; + this.$root.$emit('deck.column.dragEnd'); + + const id = e.dataTransfer.getData('mk-deck-column'); + if (id != null && id != '') { + this.$store.commit('deviceUser/swapDeckColumn', { + a: this.column.id, + b: id + }); + } + } + } +}); +</script> + +<style lang="scss" scoped> +.dnpfarvg { + $header-height: 42px; + + height: 100%; + overflow: hidden; + box-shadow: 0 0 0 1px var(--deckColumnBorder); + + &.draghover { + box-shadow: 0 0 0 2px var(--focus); + + &:after { + content: ""; + display: block; + position: absolute; + z-index: 1000; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--focus); + } + } + + &.dragging { + box-shadow: 0 0 0 2px var(--focus); + } + + &.dropready { + * { + pointer-events: none; + } + } + + &:not(.active) { + flex-basis: $header-height; + min-height: $header-height; + + > header.indicated { + box-shadow: 4px 0px var(--accent) inset; + } + } + + &.naked { + //background: var(--deckAcrylicColumnBg); + background: transparent !important; + + > header { + background: transparent; + box-shadow: none; + + > button { + color: var(--fg); + } + } + } + + &.paged { + > div { + background: var(--bg); + padding: var(--margin); + } + } + + > header { + position: relative; + display: flex; + z-index: 2; + line-height: $header-height; + padding: 0 16px; + font-size: 0.9em; + color: var(--panelHeaderFg); + background: var(--panelHeaderBg); + box-shadow: 0 1px 0 0 var(--panelHeaderDivider); + cursor: pointer; + + &, * { + user-select: none; + } + + &.indicated { + box-shadow: 0 3px 0 0 var(--accent); + } + + > .header { + display: inline-block; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + > span:only-of-type { + width: 100%; + } + + > .toggleActive, + > .action > *, + > .menu, + > .close { + z-index: 1; + width: $header-height; + line-height: $header-height; + font-size: 16px; + color: var(--faceTextButton); + + &:hover { + color: var(--faceTextButtonHover); + } + + &:active { + color: var(--faceTextButtonActive); + } + } + + > .toggleActive, > .action { + margin-left: -16px; + } + + > .action { + z-index: 1; + } + + > .action:empty { + display: none; + } + + > .menu, + > .close { + margin-left: auto; + margin-right: -16px; + } + } + + > div { + height: calc(100% - #{$header-height}); + overflow: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + box-sizing: border-box; + } +} +</style> diff --git a/src/client/components/deck/direct-column.vue b/src/client/components/deck/direct-column.vue new file mode 100644 index 0000000000..f340048d6a --- /dev/null +++ b/src/client/components/deck/direct-column.vue @@ -0,0 +1,39 @@ +<template> +<x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu"> + <template #header><fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template> + + <x-direct/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faEnvelope } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import XDirect from '../../pages/messages.vue'; + +export default Vue.extend({ + components: { + XColumn, + XDirect + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + menu: null, + faEnvelope + } + }, +}); +</script> diff --git a/src/client/components/deck/list-column.vue b/src/client/components/deck/list-column.vue new file mode 100644 index 0000000000..a3576e8d67 --- /dev/null +++ b/src/client/components/deck/list-column.vue @@ -0,0 +1,87 @@ +<template> +<x-column :menu="menu" :column="column" :is-stacked="isStacked"> + <template #header> + <fa :icon="faListUl"/><span style="margin-left: 8px;">{{ column.name }}</span> + </template> + + <x-timeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faListUl, faCog } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import XTimeline from '../timeline.vue'; + +export default Vue.extend({ + components: { + XColumn, + XTimeline, + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + faListUl + }; + }, + + watch: { + mediaOnly() { + (this.$refs.timeline as any).reload(); + } + }, + + created() { + this.menu = [{ + icon: faCog, + text: this.$t('list'), + action: this.setList + }]; + }, + + mounted() { + if (this.column.listId == null) { + this.setList(); + } + }, + + methods: { + async setList() { + const lists = await this.$root.api('users/lists/list'); + const { canceled, result: list } = await this.$root.dialog({ + title: this.$t('list'), + type: null, + select: { + items: lists.map(x => ({ + value: x, text: x.name + })), + default: this.column.listId + }, + showCancelButton: true + }); + if (canceled) return; + Vue.set(this.column, 'listId', list.id); + this.$store.commit('deviceUser/updateDeckColumn', this.column); + }, + + focus() { + (this.$refs.timeline as any).focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +</style> diff --git a/src/client/components/deck/mentions-column.vue b/src/client/components/deck/mentions-column.vue new file mode 100644 index 0000000000..19e49d2a89 --- /dev/null +++ b/src/client/components/deck/mentions-column.vue @@ -0,0 +1,39 @@ +<template> +<x-column :column="column" :is-stacked="isStacked" :menu="menu"> + <template #header><fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template> + + <x-mentions/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faAt } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import XMentions from '../../pages/mentions.vue'; + +export default Vue.extend({ + components: { + XColumn, + XMentions + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + menu: null, + faAt + } + }, +}); +</script> diff --git a/src/client/components/deck/notifications-column.vue b/src/client/components/deck/notifications-column.vue new file mode 100644 index 0000000000..58873aa130 --- /dev/null +++ b/src/client/components/deck/notifications-column.vue @@ -0,0 +1,69 @@ +<template> +<x-column :column="column" :is-stacked="isStacked" :menu="menu"> + <template #header><fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template> + + <x-notifications/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faCog } from '@fortawesome/free-solid-svg-icons'; +import { faBell } from '@fortawesome/free-regular-svg-icons'; +import XColumn from './column.vue'; +import XNotifications from '../notifications.vue'; + +export default Vue.extend({ + components: { + XColumn, + XNotifications + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + menu: null, + faBell + } + }, + + created() { + if (this.column.notificationType == null) { + this.column.notificationType = 'all'; + this.$store.commit('deviceUser/updateDeckColumn', this.column); + } + + this.menu = [{ + icon: faCog, + text: this.$t('@.notification-type'), + action: () => { + this.$root.dialog({ + title: this.$t('@.notification-type'), + type: null, + select: { + items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({ + value: x, text: this.$t('@.notification-types.' + x) + })) + default: this.column.notificationType, + }, + showCancelButton: true + }).then(({ canceled, result: type }) => { + if (canceled) return; + this.column.notificationType = type; + this.$store.commit('deviceUser/updateDeckColumn', this.column); + }); + } + }]; + }, +}); +</script> diff --git a/src/client/components/deck/tl-column.vue b/src/client/components/deck/tl-column.vue new file mode 100644 index 0000000000..c3ee67af3a --- /dev/null +++ b/src/client/components/deck/tl-column.vue @@ -0,0 +1,141 @@ +<template> +<x-column :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState"> + <template #header> + <fa v-if="column.tl === 'home'" :icon="faHome"/> + <fa v-else-if="column.tl === 'local'" :icon="faComments"/> + <fa v-else-if="column.tl === 'social'" :icon="faShareAlt"/> + <fa v-else-if="column.tl === 'global'" :icon="faGlobe"/> + <span style="margin-left: 8px;">{{ column.name }}</span> + </template> + + <div class="iwaalbte" v-if="disabled"> + <p> + <fa :icon="faMinusCircle"/> + {{ $t('disabled-timeline.title') }} + </p> + <p class="desc">{{ $t('disabled-timeline.description') }}</p> + </div> + <x-timeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faMinusCircle, faHome, faComments, faShareAlt, faGlobe, faCog } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import XTimeline from '../timeline.vue'; + +export default Vue.extend({ + components: { + XColumn, + XTimeline, + }, + + props: { + column: { + type: Object, + required: true + }, + isStacked: { + type: Boolean, + required: true + } + }, + + data() { + return { + menu: null, + disabled: false, + indicated: false, + columnActive: true, + faMinusCircle, faHome, faComments, faShareAlt, faGlobe, + }; + }, + + watch: { + mediaOnly() { + (this.$refs.timeline as any).reload(); + } + }, + + created() { + this.menu = [{ + icon: faCog, + text: this.$t('timeline'), + action: this.setType + }]; + }, + + mounted() { + if (this.column.tl == null) { + this.setType(); + } else { + this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && ( + this.$store.state.instance.meta.disableLocalTimeline && ['local', 'social'].includes(this.column.tl) || + this.$store.state.instance.meta.disableGlobalTimeline && ['global'].includes(this.column.tl)); + } + }, + + methods: { + async setType() { + const { canceled, result: src } = await this.$root.dialog({ + title: this.$t('timeline'), + type: null, + select: { + items: [{ + value: 'home', text: this.$t('_timelines.home') + }, { + value: 'local', text: this.$t('_timelines.local') + }, { + value: 'social', text: this.$t('_timelines.social') + }, { + value: 'global', text: this.$t('_timelines.global') + }] + }, + showCancelButton: true + }); + if (canceled) return; + Vue.set(this.column, 'tl', src); + this.$store.commit('deviceUser/updateDeckColumn', this.column); + }, + + queueUpdated(q) { + if (this.columnActive) { + this.indicated = q !== 0; + } + }, + + onNote() { + if (!this.columnActive) { + this.indicated = true; + } + }, + + onChangeActiveState(state) { + this.columnActive = state; + + if (this.columnActive) { + this.indicated = false; + } + }, + + focus() { + (this.$refs.timeline as any).focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +.iwaalbte { + text-align: center; + + > p { + margin: 16px; + + &.desc { + font-size: 14px; + } + } +} +</style> diff --git a/src/client/components/deck/widgets-column.vue b/src/client/components/deck/widgets-column.vue new file mode 100644 index 0000000000..37b17451ec --- /dev/null +++ b/src/client/components/deck/widgets-column.vue @@ -0,0 +1,151 @@ +<template> +<x-column :menu="menu" :naked="true" :column="column" :is-stacked="isStacked"> + <template #header><fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template> + + <div class="wtdtxvec"> + <template v-if="edit"> + <header> + <select v-model="widgetAdderSelected" @change="addWidget"> + <option v-for="widget in widgets" :value="widget" :key="widget">{{ widget }}</option> + </select> + </header> + <x-draggable + :list="column.widgets" + animation="150" + @sort="onWidgetSort" + > + <div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)"> + <button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button> + <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/> + </div> + </x-draggable> + </template> + <component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :column="column"/> + </div> +</x-column> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as XDraggable from 'vuedraggable'; +import { v4 as uuid } from 'uuid'; +import { faWindowMaximize, faTimes } from '@fortawesome/free-solid-svg-icons'; +import XColumn from './column.vue'; +import { widgets } from '../../widgets'; + +export default Vue.extend({ + components: { + XColumn, + XDraggable, + }, + + props: { + column: { + type: Object, + required: true, + }, + isStacked: { + type: Boolean, + required: true, + }, + }, + + data() { + return { + edit: false, + menu: null, + widgetAdderSelected: null, + widgets, + faWindowMaximize, faTimes + }; + }, + + created() { + this.menu = [{ + icon: 'cog', + text: this.$t('edit'), + action: () => { + this.edit = !this.edit; + } + }]; + }, + + methods: { + widgetFunc(id) { + this.$refs[id][0].setting(); + }, + + onWidgetSort() { + this.saveWidgets(); + }, + + addWidget() { + this.$store.commit('deviceUser/addDeckWidget', { + id: this.column.id, + widget: { + name: this.widgetAdderSelected, + id: uuid(), + data: {} + } + }); + + this.widgetAdderSelected = null; + }, + + removeWidget(widget) { + this.$store.commit('deviceUser/removeDeckWidget', { + id: this.column.id, + widget + }); + }, + + saveWidgets() { + this.$store.commit('deviceUser/updateDeckColumn', this.column); + } + } +}); +</script> + +<style lang="scss" scoped> +.wtdtxvec { + padding-top: 1px; // ウィジェットのbox-shadowを利用した1px borderを隠さないようにするため + + > header { + padding: 16px; + + > * { + width: 100%; + padding: 4px; + } + } + + > .widget, .customize-container { + margin: 8px; + + &:first-of-type { + margin-top: 0; + } + } + + .customize-container { + position: relative; + cursor: move; + + > *:not(.remove) { + pointer-events: none; + } + + > .remove { + position: absolute; + z-index: 2; + top: 8px; + right: 8px; + width: 32px; + height: 32px; + color: #fff; + background: rgba(#000, 0.7); + border-radius: 4px; + } + } +} +</style> diff --git a/src/client/components/error.vue b/src/client/components/error.vue index b1d91fb3ef..90efa700b2 100644 --- a/src/client/components/error.vue +++ b/src/client/components/error.vue @@ -40,7 +40,7 @@ export default Vue.extend({ > img { vertical-align: bottom; - height: 150px; + height: 128px; margin-bottom: 16px; border-radius: 16px; } diff --git a/src/client/components/form-window.vue b/src/client/components/form-window.vue new file mode 100644 index 0000000000..25eee91647 --- /dev/null +++ b/src/client/components/form-window.vue @@ -0,0 +1,71 @@ +<template> +<x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false"> + <template #header> + {{ title }} + </template> + <div class="xkpnjxcv"> + <label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item"> + <mk-input v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"><span v-text="form[item].label || item"></span></mk-input> + <mk-input v-else-if="form[item].type === 'string' && !item.multiline" v-model="values[item]" type="text"><span v-text="form[item].label || item"></span></mk-input> + <mk-textarea v-else-if="form[item].type === 'string' && item.multiline" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-textarea> + <mk-switch v-else-if="form[item].type === 'boolean'" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-switch> + </label> + </div> +</x-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XWindow from './window.vue'; +import MkInput from './ui/input.vue'; +import MkTextarea from './ui/textarea.vue'; +import MkSwitch from './ui/switch.vue'; + +export default Vue.extend({ + components: { + XWindow, + MkInput, + MkTextarea, + MkSwitch, + }, + + props: { + title: { + type: String, + required: true, + }, + form: { + type: Object, + required: true, + }, + }, + + data() { + return { + values: {} + }; + }, + + created() { + for (const item in this.form) { + Vue.set(this.values, item, this.form[item].default || null); + } + }, + + methods: { + ok() { + this.$emit('ok', this.values); + this.$refs.window.close(); + }, + } +}); +</script> + +<style lang="scss" scoped> +.xkpnjxcv { + > label { + display: block; + padding: 16px 24px; + } +} +</style> diff --git a/src/client/components/modal.vue b/src/client/components/modal.vue index 1a9d98a8cc..f941d4d503 100644 --- a/src/client/components/modal.vue +++ b/src/client/components/modal.vue @@ -1,10 +1,10 @@ <template> <div class="mk-modal" v-hotkey.global="keymap"> <transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear> - <div class="bg" ref="bg" v-if="show" @click="close()"></div> + <div class="bg" ref="bg" v-if="show" @click="canClose ? close() : () => {}"></div> </transition> <transition :name="$store.state.device.animation ? 'modal' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }"> - <div class="content" ref="content" v-if="show" @click.self="close()"><slot></slot></div> + <div class="content" ref="content" v-if="show" @click.self="canClose ? close() : () => {}"><slot></slot></div> </transition> </div> </template> @@ -14,6 +14,11 @@ import Vue from 'vue'; export default Vue.extend({ props: { + canClose: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { diff --git a/src/client/components/note-header.vue b/src/client/components/note-header.vue index 93cf2cdf39..039287818f 100644 --- a/src/client/components/note-header.vue +++ b/src/client/components/note-header.vue @@ -54,7 +54,6 @@ export default Vue.extend({ margin: 0 .5em 0 0; padding: 0; overflow: hidden; - color: var(--noteHeaderName); font-size: 1em; font-weight: bold; text-decoration: none; diff --git a/src/client/components/note.vue b/src/client/components/note.vue index 118fef661c..badb9f12f3 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -724,61 +724,6 @@ export default Vue.extend({ transition: box-shadow 0.1s ease; overflow: hidden; - &.max-width_500px { - font-size: 0.9em; - } - - &.max-width_450px { - > .renote { - padding: 8px 16px 0 16px; - } - - > .article { - padding: 14px 16px 9px; - - > .avatar { - margin: 0 10px 8px 0; - width: 50px; - height: 50px; - } - } - } - - &.max-width_350px { - > .article { - > .main { - > .footer { - > .button { - &:not(:last-child) { - margin-right: 18px; - } - } - } - } - } - } - - &.max-width_300px { - font-size: 0.825em; - - > .article { - > .avatar { - width: 44px; - height: 44px; - } - - > .main { - > .footer { - > .button { - &:not(:last-child) { - margin-right: 12px; - } - } - } - } - } - } - &:focus { outline: none; box-shadow: 0 0 0 3px var(--focus); @@ -797,10 +742,6 @@ export default Vue.extend({ white-space: pre; color: #d28a3f; - @media (max-width: 450px) { - padding: 8px 16px 0 16px; - } - > [data-icon] { margin-right: 4px; } @@ -985,5 +926,64 @@ export default Vue.extend({ > .reply { border-top: solid 1px var(--divider); } + + &.max-width_500px { + font-size: 0.9em; + } + + &.max-width_450px { + > .renote { + padding: 8px 16px 0 16px; + } + + > .info { + padding: 8px 16px 0 16px; + } + + > .article { + padding: 14px 16px 9px; + + > .avatar { + margin: 0 10px 8px 0; + width: 50px; + height: 50px; + } + } + } + + &.max-width_350px { + > .article { + > .main { + > .footer { + > .button { + &:not(:last-child) { + margin-right: 18px; + } + } + } + } + } + } + + &.max-width_300px { + font-size: 0.825em; + + > .article { + > .avatar { + width: 44px; + height: 44px; + } + + > .main { + > .footer { + > .button { + &:not(:last-child) { + margin-right: 12px; + } + } + } + } + } + } } </style> diff --git a/src/client/components/sidebar.vue b/src/client/components/sidebar.vue new file mode 100644 index 0000000000..3ddef7d127 --- /dev/null +++ b/src/client/components/sidebar.vue @@ -0,0 +1,488 @@ +<template> +<div class="mvcprjjd"> + <transition name="nav-back"> + <div class="nav-back" + v-if="showing" + @click="showing = false" + @touchstart="showing = false" + ></div> + </transition> + + <transition name="nav"> + <nav class="nav" v-show="showing"> + <div> + <button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn"> + <mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/> + </button> + <button class="item _button index active" @click="top()" v-if="$route.name === 'index'"> + <fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span> + </button> + <router-link class="item index" active-class="active" to="/" exact v-else> + <fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span> + </router-link> + <template v-for="item in menu"> + <div v-if="item === '-'" class="divider"></div> + <component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'router-link' : 'button'" class="item _button" :class="item" active-class="active" @click="() => { if (menuDef[item].action) menuDef[item].action() }" :to="menuDef[item].to"> + <fa :icon="menuDef[item].icon" fixed-width/><span class="text">{{ $t(menuDef[item].title) }}</span> + <i v-if="menuDef[item].indicated"><fa :icon="faCircle"/></i> + </component> + </template> + <div class="divider"></div> + <button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)" @click="oepnInstanceMenu"> + <fa :icon="faServer" fixed-width/><span class="text">{{ $t('instance') }}</span> + </button> + <button class="item _button" @click="more"> + <fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span> + <i v-if="otherNavItemIndicated"><fa :icon="faCircle"/></i> + </button> + <router-link class="item" active-class="active" to="/preferences"> + <fa :icon="faCog" fixed-width/><span class="text">{{ $t('settings') }}</span> + </router-link> + </div> + </nav> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; +import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; +import { host, instanceName } from '../config'; +import { search } from '../scripts/search'; + +export default Vue.extend({ + data() { + return { + host: host, + showing: false, + searching: false, + accounts: [], + connection: null, + menuDef: this.$store.getters.nav({ + search: this.search + }), + faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram + }; + }, + + computed: { + menu(): string[] { + return this.$store.state.deviceUser.menu; + }, + + otherNavItemIndicated(): boolean { + if (!this.$store.getters.isSignedIn) return false; + for (const def in this.menuDef) { + if (this.menu.includes(def)) continue; + if (this.menuDef[def].indicated) return true; + } + return false; + }, + }, + + watch: { + $route(to, from) { + this.showing = false; + }, + }, + + methods: { + show() { + this.showing = true; + }, + + search() { + if (this.searching) return; + + this.$root.dialog({ + title: this.$t('search'), + input: true + }).then(async ({ canceled, result: query }) => { + if (canceled || query == null || query === '') return; + + this.searching = true; + search(this, query).finally(() => { + this.searching = false; + }); + }); + }, + + async openAccountMenu(ev) { + const accounts = (await this.$root.api('users/show', { userIds: this.$store.state.device.accounts.map(x => x.id) })).filter(x => x.id !== this.$store.state.i.id); + + const accountItems = accounts.map(account => ({ + type: 'user', + user: account, + action: () => { this.switchAccount(account); } + })); + + this.$root.menu({ + items: [...[{ + type: 'link', + text: this.$t('profile'), + to: `/@${ this.$store.state.i.username }`, + avatar: this.$store.state.i, + }, { + type: 'link', + text: this.$t('accountSettings'), + to: '/my/settings', + icon: faCog, + }, null, ...accountItems, { + icon: faPlus, + text: this.$t('addAcount'), + action: () => { + this.$root.menu({ + items: [{ + text: this.$t('existingAcount'), + action: () => { this.addAcount(); }, + }, { + text: this.$t('createAccount'), + action: () => { this.createAccount(); }, + }], + align: 'left', + fixed: true, + width: 240, + source: ev.currentTarget || ev.target, + }); + }, + }]], + align: 'left', + fixed: true, + width: 240, + source: ev.currentTarget || ev.target, + }); + }, + + oepnInstanceMenu(ev) { + this.$root.menu({ + items: [{ + type: 'link', + text: this.$t('dashboard'), + to: '/instance', + icon: faTachometerAlt, + }, null, { + type: 'link', + text: this.$t('settings'), + to: '/instance/settings', + icon: faCog, + }, { + type: 'link', + text: this.$t('customEmojis'), + to: '/instance/emojis', + icon: faLaugh, + }, { + type: 'link', + text: this.$t('users'), + to: '/instance/users', + icon: faUsers, + }, { + type: 'link', + text: this.$t('files'), + to: '/instance/files', + icon: faCloud, + }, { + type: 'link', + text: this.$t('jobQueue'), + to: '/instance/queue', + icon: faExchangeAlt, + }, { + type: 'link', + text: this.$t('federation'), + to: '/instance/federation', + icon: faGlobe, + }, { + type: 'link', + text: this.$t('relays'), + to: '/instance/relays', + icon: faProjectDiagram, + }, { + type: 'link', + text: this.$t('announcements'), + to: '/instance/announcements', + icon: faBroadcastTower, + }], + align: 'left', + fixed: true, + width: 200, + source: ev.currentTarget || ev.target, + }); + }, + + more(ev) { + const items = Object.keys(this.menuDef).filter(k => !this.menu.includes(k)).map(k => this.menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({ + type: def.to ? 'link' : 'button', + text: this.$t(def.title), + icon: def.icon, + to: def.to, + action: def.action, + indicate: def.indicated, + })); + this.$root.menu({ + items: [...items, null, { + type: 'link', + text: this.$t('help'), + to: '/docs', + icon: faQuestionCircle, + }, { + type: 'link', + text: this.$t('aboutX', { x: instanceName || host }), + to: '/about', + icon: faInfoCircle, + }, { + type: 'link', + text: this.$t('aboutMisskey'), + to: '/about-misskey', + icon: faInfoCircle, + }], + align: 'left', + fixed: true, + width: 200, + source: ev.currentTarget || ev.target, + }); + }, + + async addAcount() { + this.$root.new(await import('./signin-dialog.vue').then(m => m.default)).$once('login', res => { + this.$store.dispatch('addAcount', res); + this.$root.dialog({ + type: 'success', + iconOnly: true, autoClose: true + }); + }); + }, + + async createAccount() { + this.$root.new(await import('./signup-dialog.vue').then(m => m.default)).$once('signup', res => { + this.$store.dispatch('addAcount', res); + this.switchAccountWithToken(res.i); + }); + }, + + async switchAccount(account: any) { + const token = this.$store.state.device.accounts.find((x: any) => x.id === account.id).token; + this.switchAccountWithToken(token); + }, + + switchAccountWithToken(token: string) { + this.$root.dialog({ + type: 'waiting', + iconOnly: true + }); + + this.$root.api('i', {}, token).then((i: any) => { + this.$store.dispatch('switchAccount', { + ...i, + token: token + }).then(() => { + this.$nextTick(() => { + location.reload(); + }); + }); + }); + }, + } +}); +</script> + +<style lang="scss" scoped> +.nav-enter-active, +.nav-leave-active { + opacity: 1; + transform: translateX(0); + transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.nav-enter, +.nav-leave-active { + opacity: 0; + transform: translateX(-240px); +} + +.nav-back-enter-active, +.nav-back-leave-active { + opacity: 1; + transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.nav-back-enter, +.nav-back-leave-active { + opacity: 0; +} + +.mvcprjjd { + $ui-font-size: 1em; // TODO: どこかに集約したい + $nav-width: 250px; // TODO: どこかに集約したい + $nav-icon-only-width: 80px; // TODO: どこかに集約したい + $nav-icon-only-threshold: 1279px; // TODO: どこかに集約したい + $nav-hide-threshold: 650px; // TODO: どこかに集約したい + + > .nav-back { + position: fixed; + top: 0; + left: 0; + z-index: 1001; + width: 100%; + height: 100%; + background: var(--modalBg); + } + + > .nav { + $avatar-size: 32px; + $avatar-margin: 8px; + + flex: 0 0 $nav-width; + width: $nav-width; + box-sizing: border-box; + + @media (max-width: $nav-icon-only-threshold) { + flex: 0 0 $nav-icon-only-width; + width: $nav-icon-only-width; + } + + @media (max-width: $nav-hide-threshold) { + position: fixed; + top: 0; + left: 0; + z-index: 1001; + } + + @media (min-width: $nav-hide-threshold + 1px) { + display: block !important; + } + + > div { + position: fixed; + top: 0; + left: 0; + z-index: 1001; + width: $nav-width; + height: 100vh; + box-sizing: border-box; + overflow: auto; + background: var(--navBg); + border-right: solid 1px var(--divider); + + > .divider { + margin: 16px 0; + border-top: solid 1px var(--divider); + } + + @media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) { + width: $nav-icon-only-width; + + > .divider { + margin: 8px auto; + width: calc(100% - 32px); + } + + > .item { + &:first-child { + margin-bottom: 8px; + } + + &:last-child { + margin-top: 8px; + } + } + } + + > .item { + position: relative; + display: block; + padding-left: 32px; + font-size: $ui-font-size; + line-height: 3.2rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + text-align: left; + box-sizing: border-box; + color: var(--navFg); + + > [data-icon] { + width: 32px; + } + + > [data-icon], + > .avatar { + margin-right: $avatar-margin; + } + + > .avatar { + width: $avatar-size; + height: $avatar-size; + vertical-align: middle; + } + + > i { + position: absolute; + top: 0; + left: 20px; + color: var(--navIndicator); + font-size: 8px; + animation: blink 1s infinite; + } + + &:hover { + text-decoration: none; + color: var(--navHoverFg); + } + + &.active { + color: var(--navActive); + } + + &:first-child, &:last-child { + position: sticky; + z-index: 1; + padding-top: 8px; + padding-bottom: 8px; + background: var(--X14); + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + } + + &:first-child { + top: 0; + margin-bottom: 16px; + border-bottom: solid 1px var(--divider); + } + + &:last-child { + bottom: 0; + margin-top: 16px; + border-top: solid 1px var(--divider); + } + + @media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) { + padding-left: 0; + width: 100%; + text-align: center; + font-size: $ui-font-size * 1.2; + line-height: 3.7rem; + + > [data-icon], + > .avatar { + margin-right: 0; + } + + > i { + left: 10px; + } + + > .text { + display: none; + } + } + } + + @media (max-width: $nav-hide-threshold) { + > .index, + > .notifications { + display: none; + } + } + } + } +} +</style> diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue index bd1901a624..ce0fd95caf 100644 --- a/src/client/components/timeline.vue +++ b/src/client/components/timeline.vue @@ -17,9 +17,11 @@ export default Vue.extend({ required: true }, list: { + type: String, required: false }, antenna: { + type: String, required: false }, sound: { @@ -53,6 +55,8 @@ export default Vue.extend({ const _note = JSON.parse(JSON.stringify(note)); // deepcopy (this.$refs.tl as any).prepend(_note); + this.$emit('note'); + if (this.sound) { this.$root.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note'); } @@ -77,10 +81,10 @@ export default Vue.extend({ if (this.src == 'antenna') { endpoint = 'antennas/notes'; this.query = { - antennaId: this.antenna.id + antennaId: this.antenna }; this.connection = this.$root.stream.connectToChannel('antenna', { - antennaId: this.antenna.id + antennaId: this.antenna }); this.connection.on('note', prepend); } else if (this.src == 'home') { @@ -106,10 +110,10 @@ export default Vue.extend({ } else if (this.src == 'list') { endpoint = 'notes/user-list-timeline'; this.query = { - listId: this.list.id + listId: this.list }; this.connection = this.$root.stream.connectToChannel('userList', { - listId: this.list.id + listId: this.list }); this.connection.on('note', prepend); this.connection.on('userAdded', onUserAdded); diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue index 3fed1f65c7..6a718439aa 100644 --- a/src/client/components/ui/container.vue +++ b/src/client/components/ui/container.vue @@ -1,5 +1,5 @@ <template> -<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader }"> +<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader, scrollable }" v-size="[{ max: 500 }]"> <header v-if="showHeader"> <div class="title"><slot name="header"></slot></div> <slot name="func"></slot> @@ -47,6 +47,11 @@ export default Vue.extend({ required: false, default: true }, + scrollable: { + type: Boolean, + required: false, + default: false + }, }, data() { return { @@ -107,10 +112,19 @@ export default Vue.extend({ box-shadow: none !important; } + &.scrollable { + display: flex; + flex-direction: column; + + > div { + overflow: auto; + } + } + > header { position: relative; box-shadow: 0 1px 0 0 var(--panelHeaderDivider); - z-index: 1; + z-index: 2; background: var(--panelHeaderBg); color: var(--panelHeaderFg); @@ -118,10 +132,6 @@ export default Vue.extend({ margin: 0; padding: 12px 16px; - @media (max-width: 500px) { - padding: 8px 10px; - } - > [data-icon] { margin-right: 6px; } @@ -141,5 +151,21 @@ export default Vue.extend({ height: 100%; } } + + &.max-width_500px { + > header { + > .title { + padding: 8px 10px; + } + } + } +} + +._forceContainerFull_ .ukygtjoj { + > header { + > .title { + padding: 12px 16px !important; + } + } } </style> diff --git a/src/client/components/ui/input.vue b/src/client/components/ui/input.vue index c9f62e3cc0..d5317db7f9 100644 --- a/src/client/components/ui/input.vue +++ b/src/client/components/ui/input.vue @@ -20,6 +20,7 @@ :pattern="pattern" :autocomplete="autocomplete" :spellcheck="spellcheck" + :step="step" @focus="focused = true" @blur="focused = false" @keydown="$emit('keydown', $event)" @@ -36,6 +37,7 @@ :pattern="pattern" :autocomplete="autocomplete" :spellcheck="spellcheck" + :step="step" @focus="focused = true" @blur="focused = false" @keydown="$emit('keydown', $event)" @@ -114,6 +116,9 @@ export default Vue.extend({ spellcheck: { required: false }, + step: { + required: false + }, debounce: { required: false }, @@ -164,7 +169,7 @@ export default Vue.extend({ }, v(v) { if (this.type === 'number') { - this.$emit('input', parseInt(v, 10)); + this.$emit('input', parseFloat(v)); } else { this.$emit('input', v); } @@ -297,7 +302,7 @@ export default Vue.extend({ pointer-events: none; transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition-duration: 0.3s; - font-size: 16px; + font-size: 1em; line-height: 32px; color: var(--inputLabel); pointer-events: none; @@ -312,7 +317,7 @@ export default Vue.extend({ top: -17px; left: 0 !important; pointer-events: none; - font-size: 16px; + font-size: 1em; line-height: 32px; color: var(--inputLabel); pointer-events: none; @@ -343,7 +348,7 @@ export default Vue.extend({ padding: 0; font: inherit; font-weight: normal; - font-size: 16px; + font-size: 1em; line-height: $height; color: var(--inputText); background: transparent; @@ -364,7 +369,7 @@ export default Vue.extend({ position: absolute; z-index: 1; top: 0; - font-size: 16px; + font-size: 1em; line-height: 32px; color: var(--inputLabel); pointer-events: none; diff --git a/src/client/components/ui/select.vue b/src/client/components/ui/select.vue index ce21949713..55f76553a7 100644 --- a/src/client/components/ui/select.vue +++ b/src/client/components/ui/select.vue @@ -135,7 +135,7 @@ export default Vue.extend({ pointer-events: none; transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition-duration: 0.3s; - font-size: 16px; + font-size: 1em; line-height: 32px; pointer-events: none; //will-change transform @@ -150,7 +150,7 @@ export default Vue.extend({ padding: 0; font: inherit; font-weight: normal; - font-size: 16px; + font-size: 1em; height: 32px; background: none; border: none; @@ -170,7 +170,7 @@ export default Vue.extend({ display: block; align-self: center; justify-self: center; - font-size: 16px; + font-size: 1em; line-height: 32px; color: rgba(#000, 0.54); pointer-events: none; diff --git a/src/client/components/ui/switch.vue b/src/client/components/ui/switch.vue index 18a2ec33f1..9652a01024 100644 --- a/src/client/components/ui/switch.vue +++ b/src/client/components/ui/switch.vue @@ -5,7 +5,7 @@ role="switch" :aria-checked="checked" :aria-disabled="disabled" - @click="toggle" + @click.prevent="toggle" > <input type="checkbox" diff --git a/src/client/components/ui/textarea.vue b/src/client/components/ui/textarea.vue index fab307a202..a42813ee64 100644 --- a/src/client/components/ui/textarea.vue +++ b/src/client/components/ui/textarea.vue @@ -133,7 +133,7 @@ export default Vue.extend({ pointer-events: none; transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); transition-duration: 0.3s; - font-size: 16px; + font-size: 1em; line-height: 32px; pointer-events: none; //will-change transform @@ -151,7 +151,7 @@ export default Vue.extend({ box-sizing: border-box; font: inherit; font-weight: normal; - font-size: 16px; + font-size: 1em; background: transparent; border: none; border-radius: 0; diff --git a/src/client/components/window.vue b/src/client/components/window.vue index db13985181..a0bff869b9 100644 --- a/src/client/components/window.vue +++ b/src/client/components/window.vue @@ -1,5 +1,5 @@ <template> -<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }"> +<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }" :can-close="canClose"> <div class="ebkgoccj" :class="{ noPadding }" @keydown="onKeydown" :style="{ width: `${width}px`, height: `${height}px` }"> <div class="header"> <button class="_button" v-if="withOkButton" @click="close()"><fa :icon="faTimes"/></button> @@ -57,6 +57,11 @@ export default Vue.extend({ required: false, default: 400 }, + canClose: { + type: Boolean, + required: false, + default: true, + }, }, data() { |