diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2020-11-25 21:31:34 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-11-25 21:31:34 +0900 |
| commit | 014440850014ee86d766bb07467c2970b17a1fc6 (patch) | |
| tree | ffb652fe1db3365d430ed72ec2c62aaacfbe21fb /src/client/components | |
| parent | フォントレンダリングを調整 (diff) | |
| download | sharkey-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')
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; } } } |