summaryrefslogtreecommitdiff
path: root/src/client/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2020-11-25 21:31:34 +0900
committerGitHub <noreply@github.com>2020-11-25 21:31:34 +0900
commit014440850014ee86d766bb07467c2970b17a1fc6 (patch)
treeffb652fe1db3365d430ed72ec2c62aaacfbe21fb /src/client/components
parentフォントレンダリングを調整 (diff)
downloadsharkey-014440850014ee86d766bb07467c2970b17a1fc6.tar.gz
sharkey-014440850014ee86d766bb07467c2970b17a1fc6.tar.bz2
sharkey-014440850014ee86d766bb07467c2970b17a1fc6.zip
nanka iroiro (#6853)
* wip * Update maps.ts * wip * wip * wip * wip * Update base.vue * wip * wip * wip * wip * Update link.vue * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update privacy.vue * wip * wip * wip * wip * Update range.vue * wip * wip * wip * wip * Update profile.vue * wip * Update a.vue * Update index.vue * wip * Update sidebar.vue * wip * wip * Update account-info.vue * Update a.vue * wip * wip * Update sounds.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update account-info.vue * Update account-info.vue * wip * wip * wip * Update d-persimmon.json5 * wip
Diffstat (limited to 'src/client/components')
-rw-r--r--src/client/components/form-dialog.vue63
-rw-r--r--src/client/components/form/base.vue56
-rw-r--r--src/client/components/form/button.vue81
-rw-r--r--src/client/components/form/form.scss34
-rw-r--r--src/client/components/form/group.vue42
-rw-r--r--src/client/components/form/input.vue306
-rw-r--r--src/client/components/form/key-value-view.vue30
-rw-r--r--src/client/components/form/link.vue90
-rw-r--r--src/client/components/form/pagination.vue42
-rw-r--r--src/client/components/form/radios.vue106
-rw-r--r--src/client/components/form/range.vue122
-rw-r--r--src/client/components/form/select.vue147
-rw-r--r--src/client/components/form/switch.vue132
-rw-r--r--src/client/components/form/textarea.vue136
-rw-r--r--src/client/components/form/tuple.vue36
-rw-r--r--src/client/components/media-image.vue2
-rw-r--r--src/client/components/media-video.vue2
-rw-r--r--src/client/components/taskmanager.api-window.vue5
-rw-r--r--src/client/components/taskmanager.vue3
-rw-r--r--src/client/components/timeline.vue3
-rw-r--r--src/client/components/ui/range.vue4
-rw-r--r--src/client/components/ui/switch.vue6
-rw-r--r--src/client/components/ui/textarea.vue3
23 files changed, 1410 insertions, 41 deletions
diff --git a/src/client/components/form-dialog.vue b/src/client/components/form-dialog.vue
index 0dc02258af..add6b230d3 100644
--- a/src/client/components/form-dialog.vue
+++ b/src/client/components/form-dialog.vue
@@ -1,6 +1,6 @@
<template>
<XModalWindow ref="dialog"
- :width="400"
+ :width="450"
:can-close="false"
:with-ok-button="true"
:ok-button-disabled="false"
@@ -12,42 +12,61 @@
<template #header>
{{ title }}
</template>
- <div class="xkpnjxcv _section">
- <label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item">
- <MkInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1">
+ <FormBase class="xkpnjxcv">
+ <template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
+ <FormInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
- </MkInput>
- <MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text">
+ </FormInput>
+ <FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
- </MkInput>
- <MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]">
+ </FormInput>
+ <FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
- </MkTextarea>
- <MkSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">
+ </FormTextarea>
+ <FormSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
- </MkSwitch>
- </label>
- </div>
+ </FormSwitch>
+ <FormSelect v-else-if="form[item].type === 'enum'" v-model:value="values[item]">
+ <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span></template>
+ <option v-for="item in form[item].enum" :value="item.value" :key="item.value">{{ item.label }}</option>
+ </FormSelect>
+ <FormRange v-else-if="form[item].type === 'range'" v-model:value="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step">
+ <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span></template>
+ <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
+ </FormRange>
+ <FormButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
+ <span v-text="form[item].content || item"></span>
+ </FormButton>
+ </template>
+ </FormBase>
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
-import MkInput from './ui/input.vue';
-import MkTextarea from './ui/textarea.vue';
-import MkSwitch from './ui/switch.vue';
+import FormBase from './form/base.vue';
+import FormInput from './form/input.vue';
+import FormTextarea from './form/textarea.vue';
+import FormSwitch from './form/switch.vue';
+import FormSelect from './form/select.vue';
+import FormRange from './form/range.vue';
+import FormButton from './form/button.vue';
export default defineComponent({
components: {
XModalWindow,
- MkInput,
- MkTextarea,
- MkSwitch,
+ FormBase,
+ FormInput,
+ FormTextarea,
+ FormSwitch,
+ FormSelect,
+ FormRange,
+ FormButton,
},
props: {
@@ -95,12 +114,6 @@ export default defineComponent({
<style lang="scss" scoped>
.xkpnjxcv {
- > label {
- display: block;
- &:not(:last-child) {
- margin-bottom: 32px;
- }
- }
}
</style>
diff --git a/src/client/components/form/base.vue b/src/client/components/form/base.vue
new file mode 100644
index 0000000000..249b49c675
--- /dev/null
+++ b/src/client/components/form/base.vue
@@ -0,0 +1,56 @@
+<template>
+<div class="rbusrurv" :class="{ wide: forceWide }" v-size="{ max: [400] }">
+ <slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+ props: {
+ forceWide: {
+ type: Boolean,
+ required: false,
+ default: false,
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.rbusrurv {
+ line-height: 1.4em;
+ background: var(--bg);
+ padding: 32px;
+
+ &:not(.wide).max-width_400px {
+ padding: 32px 0;
+
+ > ::v-deep(*) {
+ ._formPanel {
+ border: solid 0.5px var(--divider);
+ border-radius: 0;
+ border-left: none;
+ border-right: none;
+ }
+
+ ._form_group {
+ > * {
+ &:not(:first-child) {
+ &._formPanel, ._formPanel {
+ border-top: none;
+ }
+ }
+
+ &:not(:last-child) {
+ &._formPanel, ._formPanel {
+ border-bottom: solid 0.5px var(--divider);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/button.vue b/src/client/components/form/button.vue
new file mode 100644
index 0000000000..b4f0890945
--- /dev/null
+++ b/src/client/components/form/button.vue
@@ -0,0 +1,81 @@
+<template>
+<div class="yzpgjkxe _formItem">
+ <div class="_formLabel"><slot name="label"></slot></div>
+ <button class="main _button _formPanel _formClickable" :class="{ center, primary, danger }">
+ <slot></slot>
+ <div class="suffix">
+ <slot name="suffix"></slot>
+ <div class="icon">
+ <slot name="suffixIcon"></slot>
+ </div>
+ </div>
+ </button>
+ <div class="_formCaption"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './form.scss';
+
+export default defineComponent({
+ props: {
+ primary: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ danger: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ center: {
+ type: Boolean,
+ required: false,
+ default: true,
+ }
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.yzpgjkxe {
+ > .main {
+ display: flex;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 14px 16px;
+ text-align: left;
+ align-items: center;
+
+ &.center {
+ display: block;
+ text-align: center;
+ }
+
+ &.primary {
+ color: var(--accent);
+ }
+
+ &.danger {
+ color: #ff2a2a;
+ }
+
+ > .suffix {
+ display: inline-flex;
+ margin-left: auto;
+ opacity: 0.7;
+
+ > .icon {
+ margin-left: 1em;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/form.scss b/src/client/components/form/form.scss
new file mode 100644
index 0000000000..b541bf826d
--- /dev/null
+++ b/src/client/components/form/form.scss
@@ -0,0 +1,34 @@
+._formPanel {
+ background: var(--panel);
+ border-radius: var(--radius);
+
+ &._formClickable {
+ &:hover {
+ background: var(--panelHighlight);
+ }
+ }
+}
+
+._formLabel {
+ font-size: 80%;
+ padding: 0 16px 8px 16px;
+
+ &:empty {
+ display: none;
+ }
+}
+
+._formCaption {
+ font-size: 80%;
+ padding: 8px 16px 0 16px;
+
+ &:empty {
+ display: none;
+ }
+}
+
+._formItem {
+ & + ._formItem {
+ margin-top: 24px;
+ }
+}
diff --git a/src/client/components/form/group.vue b/src/client/components/form/group.vue
new file mode 100644
index 0000000000..d07852155a
--- /dev/null
+++ b/src/client/components/form/group.vue
@@ -0,0 +1,42 @@
+<template>
+<div class="vrtktovg _formItem" v-size="{ max: [500] }">
+ <div class="_formLabel"><slot name="label"></slot></div>
+ <div class="main _form_group">
+ <slot></slot>
+ </div>
+ <div class="_formCaption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+});
+</script>
+
+<style lang="scss" scoped>
+.vrtktovg {
+ > .main {
+ > ::v-deep(*) {
+ margin: 0;
+
+ &:not(:first-child) {
+ &._formPanel, ._formPanel {
+ border-top: none;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ }
+
+ &:not(:last-child) {
+ &._formPanel, ._formPanel {
+ border-bottom: solid 0.5px var(--divider);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/input.vue b/src/client/components/form/input.vue
new file mode 100644
index 0000000000..89551a5fc2
--- /dev/null
+++ b/src/client/components/form/input.vue
@@ -0,0 +1,306 @@
+<template>
+<div class="ztzhwixg _formItem" :class="{ inline, disabled }">
+ <div class="_formLabel"><slot></slot></div>
+ <div class="icon" ref="icon"><slot name="icon"></slot></div>
+ <div class="input _formPanel">
+ <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
+ <input v-if="debounce" ref="inputEl"
+ v-debounce="500"
+ :type="type"
+ v-model.lazy="v"
+ :disabled="disabled"
+ :required="required"
+ :readonly="readonly"
+ :placeholder="placeholder"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="spellcheck"
+ :step="step"
+ @focus="focused = true"
+ @blur="focused = false"
+ @keydown="onKeydown($event)"
+ @input="onInput"
+ :list="id"
+ >
+ <input v-else ref="inputEl"
+ :type="type"
+ v-model="v"
+ :disabled="disabled"
+ :required="required"
+ :readonly="readonly"
+ :placeholder="placeholder"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="spellcheck"
+ :step="step"
+ @focus="focused = true"
+ @blur="focused = false"
+ @keydown="onKeydown($event)"
+ @input="onInput"
+ :list="id"
+ >
+ <datalist :id="id" v-if="datalist">
+ <option v-for="data in datalist" :value="data"/>
+ </datalist>
+ <div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
+ </div>
+ <button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
+ <div class="_formCaption"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
+import debounce from 'v-debounce';
+import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
+import './form.scss';
+
+export default defineComponent({
+ directives: {
+ debounce
+ },
+ props: {
+ value: {
+ required: false
+ },
+ type: {
+ type: String,
+ required: false
+ },
+ required: {
+ type: Boolean,
+ required: false
+ },
+ readonly: {
+ type: Boolean,
+ required: false
+ },
+ disabled: {
+ type: Boolean,
+ required: false
+ },
+ pattern: {
+ type: String,
+ required: false
+ },
+ placeholder: {
+ type: String,
+ required: false
+ },
+ autofocus: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ autocomplete: {
+ required: false
+ },
+ spellcheck: {
+ required: false
+ },
+ step: {
+ required: false
+ },
+ debounce: {
+ required: false
+ },
+ datalist: {
+ type: Array,
+ required: false,
+ },
+ inline: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ save: {
+ type: Function,
+ required: false,
+ },
+ },
+ emits: ['change', 'keydown', 'enter'],
+ setup(props, context) {
+ const { value, type, autofocus } = toRefs(props);
+ const v = ref(value.value);
+ const id = Math.random().toString(); // TODO: uuid?
+ const focused = ref(false);
+ 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 focus = () => inputEl.value.focus();
+ const onInput = (ev) => {
+ changed.value = true;
+ context.emit('change', ev);
+ };
+ const onKeydown = (ev: KeyboardEvent) => {
+ context.emit('keydown', ev);
+
+ if (ev.code === 'Enter') {
+ context.emit('enter');
+ }
+ };
+
+ watch(value, newValue => {
+ v.value = newValue;
+ });
+
+ watch(v, newValue => {
+ if (type?.value === 'number') {
+ context.emit('update:value', parseFloat(newValue));
+ } else {
+ context.emit('update:value', newValue);
+ }
+
+ invalid.value = inputEl.value.validity.badInput;
+ });
+
+ onMounted(() => {
+ nextTick(() => {
+ if (autofocus.value) {
+ focus();
+ }
+
+ // このコンポーネントが作成された時、非表示状態である場合がある
+ // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
+ const clock = setInterval(() => {
+ if (prefixEl.value) {
+ if (prefixEl.value.offsetWidth) {
+ inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
+ }
+ }
+ if (suffixEl.value) {
+ if (suffixEl.value.offsetWidth) {
+ inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
+ }
+ }
+ }, 100);
+
+ onUnmounted(() => {
+ clearInterval(clock);
+ });
+ });
+ });
+
+ return {
+ id,
+ v,
+ focused,
+ invalid,
+ changed,
+ filled,
+ inputEl,
+ prefixEl,
+ suffixEl,
+ focus,
+ onInput,
+ onKeydown,
+ faExclamationCircle,
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.ztzhwixg {
+ position: relative;
+
+ > .icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 24px;
+ text-align: center;
+ line-height: 32px;
+
+ &:not(:empty) + .input {
+ margin-left: 28px;
+ }
+ }
+
+ > .input {
+ $height: 52px;
+ position: relative;
+
+ > input {
+ display: block;
+ height: $height;
+ width: 100%;
+ margin: 0;
+ padding: 0 16px;
+ font: inherit;
+ font-weight: normal;
+ font-size: 1em;
+ line-height: $height;
+ color: var(--inputText);
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ outline: none;
+ box-shadow: none;
+ box-sizing: border-box;
+
+ &[type='file'] {
+ display: none;
+ }
+ }
+
+ > .prefix,
+ > .suffix {
+ display: block;
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ padding: 0 16px;
+ font-size: 1em;
+ line-height: $height;
+ color: var(--inputLabel);
+ pointer-events: none;
+
+ &:empty {
+ display: none;
+ }
+
+ > * {
+ display: inline-block;
+ min-width: 16px;
+ max-width: 150px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+
+ > .prefix {
+ left: 0;
+ padding-right: 8px;
+ }
+
+ > .suffix {
+ right: 0;
+ padding-left: 8px;
+ }
+ }
+
+ > .save {
+ margin: 6px 0 0 0;
+ font-size: 0.8em;
+ }
+
+ &.inline {
+ display: inline-block;
+ margin: 0;
+ }
+
+ &.disabled {
+ opacity: 0.7;
+
+ &, * {
+ cursor: not-allowed !important;
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/key-value-view.vue b/src/client/components/form/key-value-view.vue
new file mode 100644
index 0000000000..eadc675f89
--- /dev/null
+++ b/src/client/components/form/key-value-view.vue
@@ -0,0 +1,30 @@
+<template>
+<div class="_formItem">
+ <div class="_formPanel anocepby">
+ <span class="key"><slot name="key"></slot></span>
+ <span class="value"><slot name="value"></slot></span>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './form.scss';
+
+export default defineComponent({
+
+});
+</script>
+
+<style lang="scss" scoped>
+.anocepby {
+ display: flex;
+ align-items: center;
+ padding: 14px 16px;
+
+ > .value {
+ margin-left: auto;
+ opacity: 0.7;
+ }
+}
+</style>
diff --git a/src/client/components/form/link.vue b/src/client/components/form/link.vue
new file mode 100644
index 0000000000..01c46e851a
--- /dev/null
+++ b/src/client/components/form/link.vue
@@ -0,0 +1,90 @@
+<template>
+<div class="qmfkfnzi _formItem">
+ <a class="main _button _formPanel _formClickable" :href="to" target="_blank" v-if="external">
+ <span class="icon"><slot name="icon"></slot></span>
+ <span class="text"><slot></slot></span>
+ <Fa :icon="faExternalLinkAlt" class="right"/>
+ </a>
+ <MkA class="main _button _formPanel _formClickable" :class="{ active }" :to="to" v-else>
+ <span class="icon"><slot name="icon"></slot></span>
+ <span class="text"><slot></slot></span>
+ <Fa :icon="faChevronRight" class="right"/>
+ </MkA>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faChevronRight, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
+import './form.scss';
+
+export default defineComponent({
+ props: {
+ to: {
+ type: String,
+ required: true
+ },
+ active: {
+ type: Boolean,
+ required: false
+ },
+ external: {
+ type: Boolean,
+ required: false
+ },
+ },
+ data() {
+ return {
+ faChevronRight, faExternalLinkAlt
+ };
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.qmfkfnzi {
+ > .main {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 14px 16px 14px 14px;
+
+ &:hover {
+ text-decoration: none;
+ }
+
+ &.active {
+ color: var(--accent);
+ }
+
+ > .icon {
+ width: 32px;
+ margin-right: 2px;
+ 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;
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/pagination.vue b/src/client/components/form/pagination.vue
new file mode 100644
index 0000000000..7dcaedf9bf
--- /dev/null
+++ b/src/client/components/form/pagination.vue
@@ -0,0 +1,42 @@
+<template>
+<FormGroup class="uljviswt _formItem">
+ <template #label><slot name="label"></slot></template>
+ <slot :items="items"></slot>
+ <div class="empty" v-if="empty" key="_empty_">
+ <slot name="empty"></slot>
+ </div>
+ <FormButton v-show="more" class="button" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
+ <template v-if="!moreFetching">{{ $t('loadMore') }}</template>
+ <template v-if="moreFetching"><MkLoading inline/></template>
+ </FormButton>
+</FormGroup>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormButton from './button.vue';
+import FormGroup from './group.vue';
+import paging from '@/scripts/paging';
+
+export default defineComponent({
+ components: {
+ FormButton,
+ FormGroup,
+ },
+
+ mixins: [
+ paging({}),
+ ],
+
+ props: {
+ pagination: {
+ required: true
+ },
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.uljviswt {
+}
+</style>
diff --git a/src/client/components/form/radios.vue b/src/client/components/form/radios.vue
new file mode 100644
index 0000000000..4c7f405cac
--- /dev/null
+++ b/src/client/components/form/radios.vue
@@ -0,0 +1,106 @@
+<script lang="ts">
+import { defineComponent, h } from 'vue';
+import MkRadio from '@/components/ui/radio.vue';
+import './form.scss';
+
+export default defineComponent({
+ components: {
+ MkRadio
+ },
+ props: {
+ modelValue: {
+ required: false
+ },
+ },
+ data() {
+ return {
+ value: this.modelValue,
+ }
+ },
+ watch: {
+ value() {
+ this.$emit('update:modelValue', this.value);
+ }
+ },
+ render() {
+ const label = this.$slots.desc();
+ const options = this.$slots.default();
+
+ return h('div', {
+ class: 'cnklmpwm _formItem'
+ }, [
+ h('div', {
+ class: '_formLabel',
+ }, label),
+ ...options.map(option => h('button', {
+ class: '_button _formPanel _formClickable',
+ key: option.props.value,
+ onClick: () => this.value = option.props.value,
+ }, [h('span', {
+ class: ['check', { checked: this.value === option.props.value }],
+ }), option.children]))
+ ]);
+ }
+});
+</script>
+
+<style lang="scss">
+.cnklmpwm {
+ > button {
+ display: block;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 14px 18px;
+ text-align: left;
+
+ &:not(:first-of-type) {
+ border-top: none !important;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ &:not(:last-of-type) {
+ border-bottom: solid 0.5px var(--divider);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ > .check {
+ display: inline-block;
+ vertical-align: bottom;
+ position: relative;
+ width: 20px;
+ height: 20px;
+ margin-right: 8px;
+ background: none;
+ border: 2px solid var(--inputBorder);
+ border-radius: 100%;
+ transition: inherit;
+
+ &:after {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 3px;
+ right: 3px;
+ bottom: 3px;
+ left: 3px;
+ border-radius: 100%;
+ opacity: 0;
+ transform: scale(0);
+ transition: .4s cubic-bezier(.25,.8,.25,1);
+ }
+
+ &.checked {
+ border-color: var(--accent);
+
+ &:after {
+ background-color: var(--accent);
+ transform: scale(1);
+ opacity: 1;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/range.vue b/src/client/components/form/range.vue
new file mode 100644
index 0000000000..3452184c55
--- /dev/null
+++ b/src/client/components/form/range.vue
@@ -0,0 +1,122 @@
+<template>
+<div class="ifitouly _formItem" :class="{ focused, disabled }">
+ <div class="_formLabel"><slot name="label"></slot></div>
+ <div class="_formPanel main">
+ <input
+ type="range"
+ ref="input"
+ v-model="v"
+ :disabled="disabled"
+ :min="min"
+ :max="max"
+ :step="step"
+ @focus="focused = true"
+ @blur="focused = false"
+ @input="$emit('update:value', $event.target.value)"
+ />
+ </div>
+ <div class="_formCaption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+ props: {
+ value: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ min: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ max: {
+ type: Number,
+ required: false,
+ default: 100
+ },
+ step: {
+ type: Number,
+ required: false,
+ default: 1
+ },
+ },
+ data() {
+ return {
+ v: this.value,
+ focused: false
+ };
+ },
+ watch: {
+ value(v) {
+ this.v = parseFloat(v);
+ }
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.ifitouly {
+ position: relative;
+
+ > .main {
+ padding: 24px 16px;
+
+ > input {
+ display: block;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background: var(--X10);
+ height: 4px;
+ width: 100%;
+ box-sizing: border-box;
+ margin: 0;
+ outline: 0;
+ border: 0;
+ border-radius: 7px;
+
+ &.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ &::-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;
+ }
+
+ &::-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);
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/select.vue b/src/client/components/form/select.vue
new file mode 100644
index 0000000000..b865372f56
--- /dev/null
+++ b/src/client/components/form/select.vue
@@ -0,0 +1,147 @@
+<template>
+<div class="yrtfrpux _formItem" :class="{ disabled, inline }">
+ <div class="_formLabel"><slot name="label"></slot></div>
+ <div class="icon" ref="icon"><slot name="icon"></slot></div>
+ <div class="input _formPanel _formClickable" @click="focus">
+ <div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
+ <select ref="input"
+ v-model="v"
+ :required="required"
+ :disabled="disabled"
+ @focus="focused = true"
+ @blur="focused = false"
+ >
+ <slot></slot>
+ </select>
+ <div class="suffix">
+ <Fa :icon="faChevronDown"/>
+ </div>
+ </div>
+ <div class="_formCaption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
+import './form.scss';
+
+export default defineComponent({
+ props: {
+ value: {
+ required: false
+ },
+ required: {
+ type: Boolean,
+ required: false
+ },
+ disabled: {
+ type: Boolean,
+ required: false
+ },
+ inline: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ },
+ data() {
+ return {
+ faChevronDown,
+ };
+ },
+ computed: {
+ v: {
+ get() {
+ return this.value;
+ },
+ set(v) {
+ this.$emit('update:value', v);
+ }
+ },
+ },
+ methods: {
+ focus() {
+ this.$refs.input.focus();
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.yrtfrpux {
+ position: relative;
+
+ > .icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 24px;
+ text-align: center;
+ line-height: 32px;
+
+ &:not(:empty) + .input {
+ margin-left: 28px;
+ }
+ }
+
+ > .input {
+ display: flex;
+ position: relative;
+
+ > select {
+ display: block;
+ flex: 1;
+ width: 100%;
+ padding: 0 16px;
+ font: inherit;
+ font-weight: normal;
+ font-size: 1em;
+ height: 52px;
+ background: none;
+ border: none;
+ border-radius: 0;
+ outline: none;
+ box-shadow: none;
+ appearance: none;
+ -webkit-appearance: none;
+ color: var(--fg);
+
+ option,
+ optgroup {
+ color: var(--fg);
+ background: var(--bg);
+ }
+ }
+
+ > .prefix,
+ > .suffix {
+ display: block;
+ align-self: center;
+ justify-self: center;
+ font-size: 1em;
+ line-height: 32px;
+ color: var(--inputLabel);
+ pointer-events: none;
+
+ &:empty {
+ display: none;
+ }
+
+ > * {
+ display: block;
+ min-width: 16px;
+ }
+ }
+
+ > .prefix {
+ padding-right: 4px;
+ }
+
+ > .suffix {
+ padding: 0 16px 0 0;
+ opacity: 0.7;
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/switch.vue b/src/client/components/form/switch.vue
new file mode 100644
index 0000000000..a2941c5996
--- /dev/null
+++ b/src/client/components/form/switch.vue
@@ -0,0 +1,132 @@
+<template>
+<div class="ijnpvmgr _formItem">
+ <div class="main _formPanel _formClickable"
+ :class="{ disabled, checked }"
+ :aria-checked="checked"
+ :aria-disabled="disabled"
+ @click.prevent="toggle"
+ >
+ <input
+ type="checkbox"
+ ref="input"
+ :disabled="disabled"
+ @keydown.enter="toggle"
+ >
+ <span class="button">
+ <span></span>
+ </span>
+ <span class="label">
+ <span><slot></slot></span>
+ </span>
+ </div>
+ <div class="_formCaption"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './form.scss';
+
+export default defineComponent({
+ props: {
+ value: {
+ type: Boolean,
+ default: false
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ }
+ },
+ computed: {
+ checked(): boolean {
+ return this.value;
+ }
+ },
+ methods: {
+ toggle() {
+ if (this.disabled) return;
+ this.$emit('update:value', !this.checked);
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.ijnpvmgr {
+ > .main {
+ position: relative;
+ display: flex;
+ padding: 16px;
+ cursor: pointer;
+
+ > * {
+ user-select: none;
+ }
+
+ &.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ &.checked {
+ > .button {
+ background-color: var(--X10);
+ border-color: var(--X10);
+
+ > * {
+ background-color: var(--accent);
+ transform: translateX(14px);
+ }
+ }
+ }
+
+ > input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ margin: 0;
+ }
+
+ > .button {
+ position: relative;
+ display: inline-block;
+ flex-shrink: 0;
+ margin: 3px 0 0 0;
+ width: 34px;
+ height: 14px;
+ background: var(--X6);
+ outline: none;
+ border-radius: 14px;
+ transition: all 0.3s;
+ cursor: pointer;
+
+ > * {
+ position: absolute;
+ top: -3px;
+ left: 0;
+ border-radius: 100%;
+ transition: background-color 0.3s, transform 0.3s;
+ width: 20px;
+ height: 20px;
+ background-color: #fff;
+ box-shadow: 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12);
+ }
+ }
+
+ > .label {
+ margin-left: 12px;
+ display: block;
+ transition: inherit;
+ color: var(--fg);
+
+ > span {
+ display: block;
+ line-height: 20px;
+ transition: inherit;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/textarea.vue b/src/client/components/form/textarea.vue
new file mode 100644
index 0000000000..d84b48197a
--- /dev/null
+++ b/src/client/components/form/textarea.vue
@@ -0,0 +1,136 @@
+<template>
+<div class="rivhosbp _formItem" :class="{ tall, pre }">
+ <div class="_formLabel"><slot></slot></div>
+ <div class="input _formPanel">
+ <textarea ref="input" :class="{ code, _monospace: code }"
+ :value="value"
+ :required="required"
+ :readonly="readonly"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="!code"
+ @input="onInput"
+ @focus="focused = true"
+ @blur="focused = false"
+ ></textarea>
+ </div>
+ <button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
+ <div class="_formCaption"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './form.scss';
+
+export default defineComponent({
+ props: {
+ value: {
+ required: false
+ },
+ required: {
+ type: Boolean,
+ required: false
+ },
+ readonly: {
+ type: Boolean,
+ required: false
+ },
+ pattern: {
+ type: String,
+ required: false
+ },
+ autocomplete: {
+ type: String,
+ required: false
+ },
+ code: {
+ type: Boolean,
+ required: false
+ },
+ tall: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ pre: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ save: {
+ type: Function,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ changed: false,
+ }
+ },
+ methods: {
+ focus() {
+ this.$refs.input.focus();
+ },
+ onInput(ev) {
+ this.changed = true;
+ this.$emit('update:value', ev.target.value);
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.rivhosbp {
+ position: relative;
+
+ > .input {
+ position: relative;
+
+ > textarea {
+ display: block;
+ width: 100%;
+ min-width: 100%;
+ max-width: 100%;
+ min-height: 130px;
+ margin: 0;
+ padding: 16px;
+ box-sizing: border-box;
+ font: inherit;
+ font-weight: normal;
+ font-size: 1em;
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ outline: none;
+ box-shadow: none;
+ color: var(--fg);
+
+ &.code {
+ tab-size: 2;
+ }
+ }
+ }
+
+ > .save {
+ margin: 6px 0 0 0;
+ font-size: 0.8em;
+ }
+
+ &.tall {
+ > .input {
+ > textarea {
+ min-height: 200px;
+ }
+ }
+ }
+
+ &.pre {
+ > .input {
+ > textarea {
+ white-space: pre;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/tuple.vue b/src/client/components/form/tuple.vue
new file mode 100644
index 0000000000..6c8a22d189
--- /dev/null
+++ b/src/client/components/form/tuple.vue
@@ -0,0 +1,36 @@
+<template>
+<div class="wthhikgt _formItem" v-size="{ max: [500] }">
+ <slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+});
+</script>
+
+<style lang="scss" scoped>
+.wthhikgt {
+ position: relative;
+ display: flex;
+
+ > ::v-deep(*) {
+ flex: 1;
+ margin: 0;
+
+ &:not(:last-child) {
+ margin-right: 16px;
+ }
+ }
+
+ &.max-width_500px {
+ display: block;
+
+ > ::v-deep(*) {
+ margin: inherit;
+ }
+ }
+}
+</style>
diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue
index 64e3efab31..a9d0023cc2 100644
--- a/src/client/components/media-image.vue
+++ b/src/client/components/media-image.vue
@@ -68,7 +68,7 @@ export default defineComponent({
created() {
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
this.$watch('image', () => {
- this.hide = this.image.isSensitive && !this.$store.state.device.alwaysShowNsfw;
+ this.hide = (this.$store.state.device.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.device.nsfw !== 'ignore');
if (this.image.blurhash) {
this.color = extractAvgColorFromBlurhash(this.image.blurhash);
}
diff --git a/src/client/components/media-video.vue b/src/client/components/media-video.vue
index 21faddf73b..3dfd60c87f 100644
--- a/src/client/components/media-video.vue
+++ b/src/client/components/media-video.vue
@@ -48,7 +48,7 @@ export default defineComponent({
}
},
created() {
- this.hide = this.video.isSensitive && !this.$store.state.device.alwaysShowNsfw;
+ this.hide = (this.$store.state.device.nsfw === 'force') ? true : this.video.isSensitive && (this.$store.state.device.nsfw !== 'ignore');
},
});
</script>
diff --git a/src/client/components/taskmanager.api-window.vue b/src/client/components/taskmanager.api-window.vue
index 0df3f75fa2..ec685462c9 100644
--- a/src/client/components/taskmanager.api-window.vue
+++ b/src/client/components/taskmanager.api-window.vue
@@ -14,8 +14,8 @@
<option value="res">Response</option>
</MkTab>
- <code v-if="tab === 'req'">{{ reqStr }}</code>
- <code v-if="tab === 'res'">{{ resStr }}</code>
+ <code v-if="tab === 'req'" class="_monospace">{{ reqStr }}</code>
+ <code v-if="tab === 'res'" class="_monospace">{{ resStr }}</code>
</div>
</XWindow>
</template>
@@ -67,7 +67,6 @@ export default defineComponent({
font-size: 0.9em;
tab-size: 2;
white-space: pre;
- font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
}
</style>
diff --git a/src/client/components/taskmanager.vue b/src/client/components/taskmanager.vue
index 92c56442c3..1ed8c8bd5e 100644
--- a/src/client/components/taskmanager.vue
+++ b/src/client/components/taskmanager.vue
@@ -3,7 +3,7 @@
<template #header>
<Fa :icon="faTerminal" style="margin-right: 0.5em;"/>Task Manager
</template>
- <div class="qljqmnzj">
+ <div class="qljqmnzj _monospace">
<MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);">
<option value="windows">Windows</option>
<option value="stream">Stream</option>
@@ -150,7 +150,6 @@ export default defineComponent({
display: flex;
flex-direction: column;
height: 100%;
- font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
> .content {
flex: 1;
diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue
index 930f47b1a5..df9424d8ed 100644
--- a/src/client/components/timeline.vue
+++ b/src/client/components/timeline.vue
@@ -6,6 +6,7 @@
import { defineComponent } from 'vue';
import XNotes from './notes.vue';
import * as os from '@/os';
+import * as sound from '@/scripts/sound';
export default defineComponent({
components: {
@@ -65,7 +66,7 @@ export default defineComponent({
this.$emit('note');
if (this.sound) {
- os.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
+ sound.play(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
}
};
diff --git a/src/client/components/ui/range.vue b/src/client/components/ui/range.vue
index c6e585cf50..4cfe66a8fc 100644
--- a/src/client/components/ui/range.vue
+++ b/src/client/components/ui/range.vue
@@ -1,7 +1,7 @@
<template>
<div class="timctyfi" :class="{ focused, disabled }">
<div class="icon"><slot name="icon"></slot></div>
- <span class="title"><slot name="title"></slot></span>
+ <span class="label"><slot name="label"></slot></span>
<input
type="range"
ref="input"
@@ -19,7 +19,7 @@
</template>
<script lang="ts">
-import { defineComponent } from 'vue';import * as os from '@/os';
+import { defineComponent } from 'vue';
export default defineComponent({
props: {
diff --git a/src/client/components/ui/switch.vue b/src/client/components/ui/switch.vue
index f738257232..762fba6d99 100644
--- a/src/client/components/ui/switch.vue
+++ b/src/client/components/ui/switch.vue
@@ -17,10 +17,8 @@
<span></span>
</span>
<span class="label">
- <span :aria-hidden="!checked"><slot></slot></span>
- <p :aria-hidden="!checked">
- <slot name="desc"></slot>
- </p>
+ <span><slot></slot></span>
+ <p><slot name="desc"></slot></p>
</span>
</div>
</template>
diff --git a/src/client/components/ui/textarea.vue b/src/client/components/ui/textarea.vue
index 7d3250cc45..d49d7e8342 100644
--- a/src/client/components/ui/textarea.vue
+++ b/src/client/components/ui/textarea.vue
@@ -2,7 +2,7 @@
<div class="adhpbeos" :class="{ focused, filled, tall, pre }">
<div class="input">
<span class="label" ref="label"><slot></slot></span>
- <textarea ref="input" :class="{ code }"
+ <textarea ref="input" :class="{ code, _monospace: code }"
:value="value"
:required="required"
:readonly="readonly"
@@ -166,7 +166,6 @@ export default defineComponent({
&.code {
tab-size: 2;
- font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
}
}