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/form | |
| parent | フォントレンダリングを調整 (diff) | |
| download | misskey-014440850014ee86d766bb07467c2970b17a1fc6.tar.gz misskey-014440850014ee86d766bb07467c2970b17a1fc6.tar.bz2 misskey-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/form')
| -rw-r--r-- | src/client/components/form/base.vue | 56 | ||||
| -rw-r--r-- | src/client/components/form/button.vue | 81 | ||||
| -rw-r--r-- | src/client/components/form/form.scss | 34 | ||||
| -rw-r--r-- | src/client/components/form/group.vue | 42 | ||||
| -rw-r--r-- | src/client/components/form/input.vue | 306 | ||||
| -rw-r--r-- | src/client/components/form/key-value-view.vue | 30 | ||||
| -rw-r--r-- | src/client/components/form/link.vue | 90 | ||||
| -rw-r--r-- | src/client/components/form/pagination.vue | 42 | ||||
| -rw-r--r-- | src/client/components/form/radios.vue | 106 | ||||
| -rw-r--r-- | src/client/components/form/range.vue | 122 | ||||
| -rw-r--r-- | src/client/components/form/select.vue | 147 | ||||
| -rw-r--r-- | src/client/components/form/switch.vue | 132 | ||||
| -rw-r--r-- | src/client/components/form/textarea.vue | 136 | ||||
| -rw-r--r-- | src/client/components/form/tuple.vue | 36 |
14 files changed, 1360 insertions, 0 deletions
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> |