summaryrefslogtreecommitdiff
path: root/packages/client/src/components/form
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-11-28 20:07:37 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-11-28 20:07:37 +0900
commite8005c8d3a6edf2c8cdce3fe098fb9acff8a57c6 (patch)
tree4283a0a36f5cb03f0fb3a534142c06783f8ff725 /packages/client/src/components/form
parent/antennas/notes API で日付による絞り込みができるようにする... (diff)
downloadsharkey-e8005c8d3a6edf2c8cdce3fe098fb9acff8a57c6.tar.gz
sharkey-e8005c8d3a6edf2c8cdce3fe098fb9acff8a57c6.tar.bz2
sharkey-e8005c8d3a6edf2c8cdce3fe098fb9acff8a57c6.zip
client: refine ui
Diffstat (limited to 'packages/client/src/components/form')
-rw-r--r--packages/client/src/components/form/group.vue35
-rw-r--r--packages/client/src/components/form/input.vue22
-rw-r--r--packages/client/src/components/form/link.vue112
-rw-r--r--packages/client/src/components/form/pagination.vue44
-rw-r--r--packages/client/src/components/form/radio.vue22
-rw-r--r--packages/client/src/components/form/radios.vue49
-rw-r--r--packages/client/src/components/form/range.vue272
-rw-r--r--packages/client/src/components/form/section.vue26
-rw-r--r--packages/client/src/components/form/select.vue11
-rw-r--r--packages/client/src/components/form/slot.vue15
-rw-r--r--packages/client/src/components/form/suspense.vue98
-rw-r--r--packages/client/src/components/form/switch.vue12
-rw-r--r--packages/client/src/components/form/textarea.vue16
13 files changed, 597 insertions, 137 deletions
diff --git a/packages/client/src/components/form/group.vue b/packages/client/src/components/form/group.vue
new file mode 100644
index 0000000000..2fc203f1b9
--- /dev/null
+++ b/packages/client/src/components/form/group.vue
@@ -0,0 +1,35 @@
+<template>
+<div v-sticky-container v-panel class="adfeebaf _formBlock">
+ <div class="label"><slot name="label"></slot></div>
+ <div class="main _formRoot">
+ <slot></slot>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+});
+</script>
+
+<style lang="scss" scoped>
+.adfeebaf {
+ padding: 24px 24px;
+ border-radius: var(--radius);
+
+ > .label {
+ font-weight: bold;
+ padding: 0 0 16px 0;
+
+ &:empty {
+ display: none;
+ }
+ }
+
+ > .main {
+
+ }
+}
+</style>
diff --git a/packages/client/src/components/form/input.vue b/packages/client/src/components/form/input.vue
index 99267f9231..c990b693f1 100644
--- a/packages/client/src/components/form/input.vue
+++ b/packages/client/src/components/form/input.vue
@@ -5,6 +5,7 @@
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
<input ref="inputEl"
v-model="v"
+ v-panel
:type="type"
:disabled="disabled"
:required="required"
@@ -27,7 +28,7 @@
</div>
<div class="caption"><slot name="caption"></slot></div>
- <MkButton v-if="manualSave && changed" primary @click="updated"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+ <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="fas fa-check"></i> {{ $ts.save }}</MkButton>
</div>
</template>
@@ -114,9 +115,9 @@ export default defineComponent({
const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
- const inputEl = ref(null);
- const prefixEl = ref(null);
- const suffixEl = ref(null);
+ const inputEl = ref<HTMLElement>();
+ const prefixEl = ref<HTMLElement>();
+ const suffixEl = ref<HTMLElement>();
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
@@ -208,7 +209,7 @@ export default defineComponent({
.matxzzsk {
> .label {
font-size: 0.85em;
- padding: 0 0 8px 12px;
+ padding: 0 0 8px 0;
user-select: none;
&:empty {
@@ -217,8 +218,8 @@ export default defineComponent({
}
> .caption {
- font-size: 0.8em;
- padding: 8px 0 0 12px;
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
&:empty {
@@ -242,8 +243,7 @@ export default defineComponent({
font-weight: normal;
font-size: 1em;
color: var(--fg);
- background: var(--panel);
- border: solid 0.5px var(--inputBorder);
+ border: solid 0.5px var(--panel);
border-radius: 6px;
outline: none;
box-shadow: none;
@@ -311,5 +311,9 @@ export default defineComponent({
}
}
}
+
+ > .save {
+ margin: 8px 0 0 0;
+ }
}
</style>
diff --git a/packages/client/src/components/form/link.vue b/packages/client/src/components/form/link.vue
new file mode 100644
index 0000000000..3eb74425b0
--- /dev/null
+++ b/packages/client/src/components/form/link.vue
@@ -0,0 +1,112 @@
+<template>
+<div class="ffcbddfc" :class="{ inline }">
+ <a v-if="external" class="main _button" :href="to" target="_blank">
+ <span class="icon"><slot name="icon"></slot></span>
+ <span class="text"><slot></slot></span>
+ <span class="right">
+ <span class="text"><slot name="suffix"></slot></span>
+ <i class="fas fa-external-link-alt icon"></i>
+ </span>
+ </a>
+ <MkA v-else class="main _button" :class="{ active }" :to="to" :behavior="behavior">
+ <span class="icon"><slot name="icon"></slot></span>
+ <span class="text"><slot></slot></span>
+ <span class="right">
+ <span class="text"><slot name="suffix"></slot></span>
+ <i class="fas fa-chevron-right icon"></i>
+ </span>
+ </MkA>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+ props: {
+ to: {
+ type: String,
+ required: true
+ },
+ active: {
+ type: Boolean,
+ required: false
+ },
+ external: {
+ type: Boolean,
+ required: false
+ },
+ behavior: {
+ type: String,
+ required: false,
+ },
+ inline: {
+ type: Boolean,
+ required: false
+ },
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.ffcbddfc {
+ display: block;
+
+ &.inline {
+ display: inline-block;
+ }
+
+ > .main {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 12px 14px 12px 14px;
+ background: var(--buttonBg);
+ border-radius: 6px;
+ font-size: 0.9em;
+
+ &:hover {
+ text-decoration: none;
+ background: var(--buttonHoverBg);
+ }
+
+ &.active {
+ color: var(--accent);
+ background: var(--buttonHoverBg);
+ }
+
+ > .icon {
+ margin-right: 0.75em;
+ flex-shrink: 0;
+ text-align: center;
+ opacity: 0.8;
+
+ &:empty {
+ display: none;
+
+ & + .text {
+ padding-left: 4px;
+ }
+ }
+ }
+
+ > .text {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ padding-right: 12px;
+ }
+
+ > .right {
+ margin-left: auto;
+ opacity: 0.7;
+ white-space: nowrap;
+
+ > .text:not(:empty) {
+ margin-right: 0.75em;
+ }
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/components/form/pagination.vue b/packages/client/src/components/form/pagination.vue
new file mode 100644
index 0000000000..3d3b40a783
--- /dev/null
+++ b/packages/client/src/components/form/pagination.vue
@@ -0,0 +1,44 @@
+<template>
+<FormSlot>
+ <template #label><slot name="label"></slot></template>
+ <div class="abcaccfa">
+ <slot :items="items"></slot>
+ <div v-if="empty" key="_empty_" class="empty">
+ <slot name="empty"></slot>
+ </div>
+ <MkButton v-show="more" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
+ <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
+ <template v-if="moreFetching"><MkLoading inline/></template>
+ </MkButton>
+ </div>
+</FormSlot>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import FormSlot from './slot.vue';
+import paging from '@/scripts/paging';
+
+export default defineComponent({
+ components: {
+ MkButton,
+ FormSlot,
+ },
+
+ mixins: [
+ paging({}),
+ ],
+
+ props: {
+ pagination: {
+ required: true
+ },
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.abcaccfa {
+}
+</style>
diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue
index 0f31d8fa0a..f0b8c71376 100644
--- a/packages/client/src/components/form/radio.vue
+++ b/packages/client/src/components/form/radio.vue
@@ -1,5 +1,6 @@
<template>
<div
+ v-panel
class="novjtctn"
:class="{ disabled, checked }"
:aria-checked="checked"
@@ -50,9 +51,10 @@ export default defineComponent({
.novjtctn {
position: relative;
display: inline-block;
- margin: 8px 20px 0 0;
text-align: left;
cursor: pointer;
+ padding: 11px 14px;
+ border-radius: 6px;
transition: all 0.3s;
> * {
@@ -68,6 +70,14 @@ export default defineComponent({
}
&.checked {
+ background: var(--accentedBg) !important;
+ border-color: var(--accent);
+ color: var(--accent);
+
+ &, * {
+ cursor: default !important;
+ }
+
> .button {
border-color: var(--accent);
@@ -79,6 +89,11 @@ export default defineComponent({
}
}
+ &:hover {
+ border-color: var(--inputBorderHover);
+ color: var(--accent);
+ }
+
> input {
position: absolute;
width: 0;
@@ -89,8 +104,8 @@ export default defineComponent({
> .button {
position: absolute;
- width: 20px;
- height: 20px;
+ width: 14px;
+ height: 14px;
background: none;
border: solid 2px var(--inputBorder);
border-radius: 100%;
@@ -114,7 +129,6 @@ export default defineComponent({
> .label {
margin-left: 28px;
display: block;
- font-size: 16px;
line-height: 20px;
cursor: pointer;
}
diff --git a/packages/client/src/components/form/radios.vue b/packages/client/src/components/form/radios.vue
index 998a738202..ff5d51f9c7 100644
--- a/packages/client/src/components/form/radios.vue
+++ b/packages/client/src/components/form/radios.vue
@@ -23,6 +23,8 @@ export default defineComponent({
},
render() {
let options = this.$slots.default();
+ const label = this.$slots.label && this.$slots.label();
+ const caption = this.$slots.caption && this.$slots.caption();
// なぜかFragmentになることがあるため
if (options.length === 1 && options[0].props == null) options = options[0].children;
@@ -30,12 +32,21 @@ export default defineComponent({
return h('div', {
class: 'novjtcto'
}, [
- ...options.map(option => h(MkRadio, {
- key: option.key,
- value: option.props.value,
- modelValue: this.value,
- 'onUpdate:modelValue': value => this.value = value,
- }, option.children))
+ ...(label ? [h('div', {
+ class: 'label'
+ }, [label])] : []),
+ h('div', {
+ class: 'body'
+ }, options.map(option => h(MkRadio, {
+ key: option.key,
+ value: option.props.value,
+ modelValue: this.value,
+ 'onUpdate:modelValue': value => this.value = value,
+ }, option.children)),
+ ),
+ ...(caption ? [h('div', {
+ class: 'caption'
+ }, [caption])] : []),
]);
}
});
@@ -43,12 +54,30 @@ export default defineComponent({
<style lang="scss">
.novjtcto {
- &:first-child {
- margin-top: 0;
+ > .label {
+ font-size: 0.85em;
+ padding: 0 0 8px 0;
+ user-select: none;
+
+ &:empty {
+ display: none;
+ }
+ }
+
+ > .body {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ grid-gap: 12px;
}
- &:last-child {
- margin-bottom: 0;
+ > .caption {
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
+
+ &:empty {
+ display: none;
+ }
}
}
</style>
diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue
index dd771abfe2..79a83d6a93 100644
--- a/packages/client/src/components/form/range.vue
+++ b/packages/client/src/components/form/range.vue
@@ -1,29 +1,27 @@
<template>
-<div class="timctyfi" :class="{ focused, disabled }">
- <div class="icon"><slot name="icon"></slot></div>
- <span class="label"><slot name="label"></slot></span>
- <input
- ref="input"
- v-model="v"
- type="range"
- :disabled="disabled"
- :min="min"
- :max="max"
- :step="step"
- :autofocus="autofocus"
- @focus="focused = true"
- @blur="focused = false"
- @input="$emit('update:value', $event.target.value)"
- />
+<div class="timctyfi" :class="{ disabled }">
+ <div class="label"><slot name="label"></slot></div>
+ <div v-panel class="body">
+ <div ref="containerEl" class="container">
+ <div class="track">
+ <div class="highlight" :style="{ width: (steppedValue * 100) + '%' }"></div>
+ </div>
+ <div v-if="steps" class="ticks">
+ <div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
+ </div>
+ <div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
+ </div>
+ </div>
</div>
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent, ref, watch } from 'vue';
+import * as os from '@/os';
export default defineComponent({
props: {
- value: {
+ modelValue: {
type: Number,
required: false,
default: 0
@@ -51,88 +49,198 @@ export default defineComponent({
autofocus: {
type: Boolean,
required: false
- }
+ },
+ textConverter: {
+ type: Function,
+ required: false,
+ default: (v) => v.toString(),
+ },
},
- data() {
+
+ setup(props, context) {
+ const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
+ const steppedValue = computed(() => {
+ if (props.step) {
+ const step = props.step / (props.max - props.min);
+ return (step * Math.round(rawValue.value / step));
+ } else {
+ return rawValue.value;
+ }
+ });
+ const finalValue = computed(() => {
+ return (steppedValue.value * (props.max - props.min)) + props.min;
+ });
+ watch(finalValue, () => {
+ context.emit('update:modelValue', finalValue.value);
+ });
+
+ const thumbWidth = computed(() => {
+ if (thumbEl.value == null) return 0;
+ return thumbEl.value!.offsetWidth;
+ });
+ const thumbPosition = computed(() => {
+ if (containerEl.value == null) return 0;
+ return (containerEl.value.offsetWidth - thumbWidth.value) * steppedValue.value;
+ });
+ const steps = computed(() => {
+ if (props.step) {
+ return (props.max - props.min) / props.step;
+ } else {
+ return 0;
+ }
+ });
+ const containerEl = ref<HTMLElement>();
+ const thumbEl = ref<HTMLElement>();
+
+ const onMousedown = (ev: MouseEvent | TouchEvent) => {
+ ev.preventDefault();
+
+ const tooltipShowing = ref(true);
+ os.popup(import('@/components/ui/tooltip.vue'), {
+ showing: tooltipShowing,
+ text: computed(() => {
+ return props.textConverter(finalValue.value);
+ }),
+ source: thumbEl,
+ }, {}, 'closed');
+
+ const style = document.createElement('style');
+ style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
+ document.head.appendChild(style);
+
+ const onDrag = (ev: MouseEvent | TouchEvent) => {
+ ev.preventDefault();
+ const containerRect = containerEl.value!.getBoundingClientRect();
+ const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX;
+ const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth.value / 2));
+ rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value)));
+ };
+
+ const onMouseup = () => {
+ document.head.removeChild(style);
+ tooltipShowing.value = false;
+ window.removeEventListener('mousemove', onDrag);
+ window.removeEventListener('touchmove', onDrag);
+ window.removeEventListener('mouseup', onMouseup);
+ window.removeEventListener('touchend', onMouseup);
+ };
+
+ window.addEventListener('mousemove', onDrag);
+ window.addEventListener('touchmove', onDrag);
+ window.addEventListener('mouseup', onMouseup, { once: true });
+ window.addEventListener('touchend', onMouseup, { once: true });
+ };
+
return {
- v: this.value,
- focused: false
+ rawValue,
+ finalValue,
+ steppedValue,
+ onMousedown,
+ containerEl,
+ thumbEl,
+ thumbPosition,
+ steps,
};
},
- watch: {
- value(v) {
- this.v = parseFloat(v);
- }
- },
- mounted() {
- if (this.autofocus) {
- this.$nextTick(() => {
- this.$refs.input.focus();
- });
- }
- }
});
</script>
<style lang="scss" scoped>
+@use "sass:math";
+
.timctyfi {
position: relative;
- margin: 8px;
- > .icon {
- display: inline-block;
- width: 24px;
- text-align: center;
- }
+ > .label {
+ font-size: 0.85em;
+ padding: 0 0 8px 0;
+ user-select: none;
- > .title {
- pointer-events: none;
- font-size: 16px;
- color: var(--inputLabel);
- overflow: hidden;
+ &:empty {
+ display: none;
+ }
}
- > input {
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
- background: var(--X10);
- height: 7px;
- margin: 0 8px;
- outline: 0;
- border: 0;
- border-radius: 7px;
+ > .caption {
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
- &.disabled {
- opacity: 0.6;
- cursor: not-allowed;
+ &:empty {
+ display: none;
}
+ }
- &::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- cursor: pointer;
- width: 20px;
- height: 20px;
- display: block;
- border-radius: 50%;
- border: none;
- background: var(--accent);
- box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
- box-sizing: content-box;
- }
+ $thumbHeight: 20px;
+ $thumbWidth: 20px;
+
+ > .body {
+ padding: 12px;
+ border-radius: 6px;
+
+ > .container {
+ position: relative;
+ height: $thumbHeight;
+
+ > .track {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ margin: auto;
+ width: calc(100% - #{$thumbWidth});
+ height: 3px;
+ background: rgba(0, 0, 0, 0.1);
+ border-radius: 999px;
+ overflow: clip;
+
+ > .highlight {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ background: var(--accent);
+ opacity: 0.5;
+ transition: width 0.2s cubic-bezier(0,0,0,1);
+ }
+ }
+
+ > .ticks {
+ $tickWidth: 3px;
+
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ margin: auto;
+ width: calc(100% - #{$thumbWidth});
+
+ > .tick {
+ position: absolute;
+ bottom: 0;
+ width: $tickWidth;
+ height: 3px;
+ margin-left: - math.div($tickWidth, 2);
+ background: var(--divider);
+ border-radius: 999px;
+ }
+ }
+
+ > .thumb {
+ position: absolute;
+ width: $thumbWidth;
+ height: $thumbHeight;
+ cursor: grab;
+ background: var(--accent);
+ border-radius: 999px;
+ transition: left 0.2s cubic-bezier(0,0,0,1);
- &::-moz-range-thumb {
- -moz-appearance: none;
- appearance: none;
- cursor: pointer;
- width: 20px;
- height: 20px;
- display: block;
- border-radius: 50%;
- border: none;
- background: var(--accent);
- box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
+ &:hover {
+ background: var(--accentLighten);
+ }
+ }
}
}
}
diff --git a/packages/client/src/components/form/section.vue b/packages/client/src/components/form/section.vue
index 76db7ac5c3..bc2ab966b8 100644
--- a/packages/client/src/components/form/section.vue
+++ b/packages/client/src/components/form/section.vue
@@ -1,7 +1,7 @@
<template>
-<div v-size="{ max: [500] }" v-sticky-container class="vrtktovh">
+<div v-size="{ max: [500] }" v-sticky-container class="vrtktovh _formBlock">
<div class="label"><slot name="label"></slot></div>
- <div class="main">
+ <div class="main _formRoot">
<slot></slot>
</div>
</div>
@@ -17,15 +17,33 @@ export default defineComponent({
<style lang="scss" scoped>
.vrtktovh {
+ margin: 0;
border-top: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--divider);
+ padding: 24px 0;
+
+ & + .vrtktovh {
+ border-top: none;
+ }
+
+ &:first-child {
+ border-top: none;
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
> .label {
font-weight: bold;
- padding: 24px 0 16px 0;
+ padding: 0 0 16px 0;
+
+ &:empty {
+ display: none;
+ }
}
> .main {
- margin-bottom: 32px;
}
}
</style>
diff --git a/packages/client/src/components/form/select.vue b/packages/client/src/components/form/select.vue
index fe2a4e3a7d..9ecff1aa6f 100644
--- a/packages/client/src/components/form/select.vue
+++ b/packages/client/src/components/form/select.vue
@@ -3,7 +3,7 @@
<div class="label" @click="focus"><slot name="label"></slot></div>
<div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick">
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
- <select ref="inputEl" v-model="v"
+ <select ref="inputEl" v-model="v" v-panel
class="select"
:disabled="disabled"
:required="required"
@@ -201,7 +201,7 @@ export default defineComponent({
.vblkjoeq {
> .label {
font-size: 0.85em;
- padding: 0 0 8px 12px;
+ padding: 0 0 8px 0;
user-select: none;
&:empty {
@@ -210,8 +210,8 @@ export default defineComponent({
}
> .caption {
- font-size: 0.8em;
- padding: 8px 0 0 12px;
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
&:empty {
@@ -242,8 +242,7 @@ export default defineComponent({
font-weight: normal;
font-size: 1em;
color: var(--fg);
- background: var(--panel);
- border: solid 1px var(--inputBorder);
+ border: solid 1px var(--panel);
border-radius: 6px;
outline: none;
box-shadow: none;
diff --git a/packages/client/src/components/form/slot.vue b/packages/client/src/components/form/slot.vue
index 8580c1307d..d031b2effc 100644
--- a/packages/client/src/components/form/slot.vue
+++ b/packages/client/src/components/form/slot.vue
@@ -18,11 +18,9 @@ export default defineComponent({
<style lang="scss" scoped>
.adhpbeou {
- margin: 1.5em 0;
-
> .label {
font-size: 0.85em;
- padding: 0 0 8px 12px;
+ padding: 0 0 8px 0;
user-select: none;
&:empty {
@@ -31,20 +29,13 @@ export default defineComponent({
}
> .caption {
- font-size: 0.8em;
- padding: 8px 0 0 12px;
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
&:empty {
display: none;
}
}
-
- > .content {
- position: relative;
- background: var(--panel);
- border: solid 0.5px var(--inputBorder);
- border-radius: 6px;
- }
}
</style>
diff --git a/packages/client/src/components/form/suspense.vue b/packages/client/src/components/form/suspense.vue
new file mode 100644
index 0000000000..4d5debe604
--- /dev/null
+++ b/packages/client/src/components/form/suspense.vue
@@ -0,0 +1,98 @@
+<template>
+<transition name="fade" mode="out-in">
+ <div v-if="pending">
+ <MkLoading/>
+ </div>
+ <div v-else-if="resolved">
+ <slot :result="result"></slot>
+ </div>
+ <div v-else>
+ <div class="wszdbhzo">
+ <div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div>
+ <MkButton inline class="retry" @click="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton>
+ </div>
+ </div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType, ref, watch } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+
+export default defineComponent({
+ components: {
+ MkButton
+ },
+
+ props: {
+ p: {
+ type: Function as PropType<() => Promise<any>>,
+ required: true,
+ }
+ },
+
+ setup(props, context) {
+ const pending = ref(true);
+ const resolved = ref(false);
+ const rejected = ref(false);
+ const result = ref(null);
+
+ const process = () => {
+ if (props.p == null) {
+ return;
+ }
+ const promise = props.p();
+ pending.value = true;
+ resolved.value = false;
+ rejected.value = false;
+ promise.then((_result) => {
+ pending.value = false;
+ resolved.value = true;
+ result.value = _result;
+ });
+ promise.catch(() => {
+ pending.value = false;
+ rejected.value = true;
+ });
+ };
+
+ watch(() => props.p, () => {
+ process();
+ }, {
+ immediate: true
+ });
+
+ const retry = () => {
+ process();
+ };
+
+ return {
+ pending,
+ resolved,
+ rejected,
+ result,
+ retry,
+ };
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.125s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
+
+.wszdbhzo {
+ padding: 16px;
+ text-align: center;
+
+ > .retry {
+ margin-top: 16px;
+ }
+}
+</style>
diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue
index d6df68a07f..239303a55a 100644
--- a/packages/client/src/components/form/switch.vue
+++ b/packages/client/src/components/form/switch.vue
@@ -18,7 +18,7 @@
</span>
<span class="label">
<span><slot></slot></span>
- <p><slot name="caption"></slot></p>
+ <p class="caption"><slot name="caption"></slot></p>
</span>
</div>
</template>
@@ -118,10 +118,14 @@ export default defineComponent({
transition: inherit;
}
- > p {
- margin: 0;
+ > .caption {
+ margin: 8px 0 0 0;
color: var(--fgTransparentWeak);
- font-size: 90%;
+ font-size: 0.85em;
+
+ &:empty {
+ display: none;
+ }
}
}
diff --git a/packages/client/src/components/form/textarea.vue b/packages/client/src/components/form/textarea.vue
index f3a2c394f1..98fd0da94b 100644
--- a/packages/client/src/components/form/textarea.vue
+++ b/packages/client/src/components/form/textarea.vue
@@ -4,6 +4,7 @@
<div class="input" :class="{ disabled, focused, tall, pre }">
<textarea ref="inputEl"
v-model="v"
+ v-panel
:class="{ code, _monospace: code }"
:disabled="disabled"
:required="required"
@@ -20,7 +21,7 @@
</div>
<div class="caption"><slot name="caption"></slot></div>
- <MkButton v-if="manualSave && changed" primary @click="updated"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+ <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
</div>
</template>
@@ -174,7 +175,7 @@ export default defineComponent({
.adhpbeos {
> .label {
font-size: 0.85em;
- padding: 0 0 8px 12px;
+ padding: 0 0 8px 0;
user-select: none;
&:empty {
@@ -183,8 +184,8 @@ export default defineComponent({
}
> .caption {
- font-size: 0.8em;
- padding: 8px 0 0 12px;
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
&:empty {
@@ -209,8 +210,7 @@ export default defineComponent({
font-weight: normal;
font-size: 1em;
color: var(--fg);
- background: var(--panel);
- border: solid 0.5px var(--inputBorder);
+ border: solid 0.5px var(--panel);
border-radius: 6px;
outline: none;
box-shadow: none;
@@ -248,5 +248,9 @@ export default defineComponent({
}
}
}
+
+ > .save {
+ margin: 8px 0 0 0;
+ }
}
</style>