diff options
Diffstat (limited to 'src/client/components/form')
20 files changed, 828 insertions, 1152 deletions
diff --git a/src/client/components/form/base.vue b/src/client/components/form/base.vue deleted file mode 100644 index 132942d527..0000000000 --- a/src/client/components/form/base.vue +++ /dev/null @@ -1,65 +0,0 @@ -<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 { - // 他のCSSからも参照されるので消さないように - --formXPadding: 32px; - --formYPadding: 32px; - - --formContentHMargin: 16px; - - font-size: 95%; - line-height: 1.3em; - background: var(--bg); - padding: var(--formYPadding) var(--formXPadding); - max-width: 750px; - margin: 0 auto; - - &:not(.wide).max-width_400px { - --formXPadding: 0px; - - > ::v-deep(*) { - ._formPanel { - border: solid 0.5px var(--divider); - border-radius: 0; - border-left: none; - border-right: none; - } - - ._form_group { - > *:not(._formNoConcat) { - &:not(:last-child):not(._formNoConcatPrev) { - &._formPanel, ._formPanel { - border-bottom: solid 0.5px var(--divider); - } - } - - &:not(:first-child):not(._formNoConcatNext) { - &._formPanel, ._formPanel { - border-top: none; - } - } - } - } - } - } -} -</style> diff --git a/src/client/components/form/button.vue b/src/client/components/form/button.vue deleted file mode 100644 index b4f0890945..0000000000 --- a/src/client/components/form/button.vue +++ /dev/null @@ -1,81 +0,0 @@ -<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 deleted file mode 100644 index 00f40df9b1..0000000000 --- a/src/client/components/form/form.scss +++ /dev/null @@ -1,52 +0,0 @@ -._formPanel { - background: var(--panel); - border-radius: var(--radius); - transition: background 0.2s ease; - - &._formClickable { - &:hover { - //background: var(--panelHighlight); - } - - &:active { - background: var(--panelHighlight); - transition: background 0s; - } - } -} - -._formLabel, -._formCaption { - font-size: 80%; - color: var(--fgTransparentWeak); - - &:empty { - display: none; - } -} - -._formLabel { - position: sticky; - top: var(--stickyTop, 0px); - z-index: 2; - margin: -8px calc(var(--formXPadding) * -1) 0 calc(var(--formXPadding) * -1); - padding: 8px calc(var(--formContentHMargin) + var(--formXPadding)) 8px calc(var(--formContentHMargin) + var(--formXPadding)); - background: var(--X17); - -webkit-backdrop-filter: var(--blur, blur(10px)); - backdrop-filter: var(--blur, blur(10px)); -} - -._themeChanging_ ._formLabel { - transition: none !important; - background: transparent; -} - -._formCaption { - padding: 8px var(--formContentHMargin) 0 var(--formContentHMargin); -} - -._formItem { - & + ._formItem { - margin-top: 24px; - } -} diff --git a/src/client/components/form/group.vue b/src/client/components/form/group.vue deleted file mode 100644 index 34ccaeff07..0000000000 --- a/src/client/components/form/group.vue +++ /dev/null @@ -1,78 +0,0 @@ -<template> -<div class="vrtktovg _formItem _formNoConcat" v-size="{ max: [500] }" v-sticky-container> - <div class="_formLabel"><slot name="label"></slot></div> - <div class="main _form_group" ref="child"> - <slot></slot> - </div> - <div class="_formCaption"><slot name="caption"></slot></div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, onMounted, ref } from 'vue'; - -export default defineComponent({ - setup(props, context) { - const child = ref<HTMLElement | null>(null); - - const scanChild = () => { - if (child.value == null) return; - const els = Array.from(child.value.children); - for (let i = 0; i < els.length; i++) { - const el = els[i]; - if (el.classList.contains('_formNoConcat')) { - if (els[i - 1]) els[i - 1].classList.add('_formNoConcatPrev'); - if (els[i + 1]) els[i + 1].classList.add('_formNoConcatNext'); - } - } - }; - - onMounted(() => { - scanChild(); - - const observer = new MutationObserver(records => { - scanChild(); - }); - - observer.observe(child.value, { - childList: true, - subtree: false, - attributes: false, - characterData: false, - }); - }); - - return { - child - }; - } -}); -</script> - -<style lang="scss" scoped> -.vrtktovg { - > .main { - > ::v-deep(*):not(._formNoConcat) { - &:not(._formNoConcatNext) { - margin: 0; - } - - &:not(:last-child):not(._formNoConcatPrev) { - &._formPanel, ._formPanel { - border-bottom: solid 0.5px var(--divider); - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } - } - - &:not(:first-child):not(._formNoConcatNext) { - &._formPanel, ._formPanel { - border-top: none; - border-top-left-radius: 0; - border-top-right-radius: 0; - } - } - } - } -} -</style> diff --git a/src/client/components/form/info.vue b/src/client/components/form/info.vue deleted file mode 100644 index 9fdcbdca62..0000000000 --- a/src/client/components/form/info.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<div class="fzenkabp _formItem"> - <div class="_formPanel" :class="{ warn }"> - <i v-if="warn" class="fas fa-exclamation-triangle"></i> - <i v-else class="fas fa-info-circle"></i> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; - -export default defineComponent({ - props: { - warn: { - type: Boolean, - required: false, - default: false - }, - }, - data() { - return { - }; - } -}); -</script> - -<style lang="scss" scoped> -.fzenkabp { - > div { - padding: 14px 16px; - font-size: 90%; - background: var(--infoBg); - color: var(--infoFg); - - &.warn { - background: var(--infoWarnBg); - color: var(--infoWarnFg); - } - - > i { - margin-right: 4px; - } - } -} -</style> diff --git a/src/client/components/form/input.vue b/src/client/components/form/input.vue index 942ac4dfd2..d7b6f77519 100644 --- a/src/client/components/form/input.vue +++ b/src/client/components/form/input.vue @@ -1,53 +1,49 @@ <template> -<FormGroup class="_formItem"> - <template #label><slot></slot></template> - <div class="ztzhwixg _formItem" :class="{ inline, disabled }"> - <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 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> +<div class="matxzzsk"> + <div class="label" @click="focus"><slot name="label"></slot></div> + <div class="input" :class="{ inline, disabled, focused }"> + <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div> + <input 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> - <template #caption><slot name="desc"></slot></template> + <div class="caption"><slot name="caption"></slot></div> - <FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> -</FormGroup> + <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> +</div> </template> <script lang="ts"> import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; -import './form.scss'; -import FormButton from './button.vue'; -import FormGroup from './group.vue'; +import MkButton from '../ui/button.vue'; +import { debounce } from 'throttle-debounce'; export default defineComponent({ components: { - FormGroup, - FormButton, + MkButton, }, + props: { - value: { - required: false + modelValue: { + required: true }, type: { type: String, @@ -96,16 +92,23 @@ export default defineComponent({ required: false, default: false }, + debounce: { + type: Boolean, + required: false, + default: false + }, manualSave: { type: Boolean, required: false, default: false }, }, - emits: ['change', 'keydown', 'enter'], + + emits: ['change', 'keydown', 'enter', 'update:modelValue'], + setup(props, context) { - const { value, type, autofocus } = toRefs(props); - const v = ref(value.value); + const { modelValue, type, autofocus } = toRefs(props); + const v = ref(modelValue.value); const id = Math.random().toString(); // TODO: uuid? const focused = ref(false); const changed = ref(false); @@ -131,19 +134,25 @@ export default defineComponent({ const updated = () => { changed.value = false; if (type?.value === 'number') { - context.emit('update:value', parseFloat(v.value)); + context.emit('update:modelValue', parseFloat(v.value)); } else { - context.emit('update:value', v.value); + context.emit('update:modelValue', v.value); } }; - watch(value, newValue => { + const debouncedUpdated = debounce(1000, updated); + + watch(modelValue, newValue => { v.value = newValue; }); watch(v, newValue => { if (!props.manualSave) { - updated(); + if (props.debounce) { + debouncedUpdated(); + } else { + updated(); + } } invalid.value = inputEl.value.validity.badInput; @@ -196,59 +205,66 @@ export default defineComponent({ </script> <style lang="scss" scoped> -.ztzhwixg { - position: relative; +.matxzzsk { + > .label { + font-size: 0.85em; + padding: 0 0 8px 12px; + user-select: none; + + &:empty { + display: none; + } + } - > .icon { - position: absolute; - top: 0; - left: 0; - width: 24px; - text-align: center; - line-height: 32px; + > .caption { + font-size: 0.8em; + padding: 8px 0 0 12px; + color: var(--fgTransparentWeak); - &:not(:empty) + .input { - margin-left: 28px; + &:empty { + display: none; } } > .input { - $height: 48px; + $height: 42px; position: relative; > input { + appearance: none; + -webkit-appearance: none; display: block; height: $height; width: 100%; margin: 0; - padding: 0 16px; + padding: 0 12px; font: inherit; font-weight: normal; font-size: 1em; - line-height: $height; - color: var(--inputText); - background: transparent; - border: none; - border-radius: 0; + color: var(--fg); + background: var(--panel); + border: solid 0.5px var(--inputBorder); + border-radius: 6px; outline: none; box-shadow: none; box-sizing: border-box; + transition: border-color 0.1s ease-out; - &[type='file'] { - display: none; + &:hover { + border-color: var(--inputBorderHover); } } > .prefix, > .suffix { - display: block; + display: flex; + align-items: center; position: absolute; z-index: 1; top: 0; - padding: 0 16px; + padding: 0 12px; font-size: 1em; - line-height: $height; - color: var(--inputLabel); + height: $height; pointer-events: none; &:empty { @@ -267,25 +283,32 @@ export default defineComponent({ > .prefix { left: 0; - padding-right: 8px; + padding-right: 6px; } > .suffix { right: 0; - padding-left: 8px; + padding-left: 6px; } - } - &.inline { - display: inline-block; - margin: 0; - } + &.inline { + display: inline-block; + margin: 0; + } + + &.focused { + > input { + border-color: var(--accent); + //box-shadow: 0 0 0 4px var(--focus); + } + } - &.disabled { - opacity: 0.7; + &.disabled { + opacity: 0.7; - &, * { - cursor: not-allowed !important; + &, * { + cursor: not-allowed !important; + } } } } diff --git a/src/client/components/form/key-value-view.vue b/src/client/components/form/key-value-view.vue deleted file mode 100644 index ca4c09867f..0000000000 --- a/src/client/components/form/key-value-view.vue +++ /dev/null @@ -1,38 +0,0 @@ -<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 var(--formContentHMargin); - - > .key { - margin-right: 12px; - white-space: nowrap; - } - - > .value { - margin-left: auto; - opacity: 0.7; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } -} -</style> diff --git a/src/client/components/form/link.vue b/src/client/components/form/link.vue deleted file mode 100644 index e1d13c6431..0000000000 --- a/src/client/components/form/link.vue +++ /dev/null @@ -1,103 +0,0 @@ -<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> - <span class="right"> - <span class="text"><slot name="suffix"></slot></span> - <i class="fas fa-external-link-alt icon"></i> - </span> - </a> - <MkA class="main _button _formPanel _formClickable" :class="{ active }" :to="to" :behavior="behavior" v-else> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot></slot></span> - <span class="right"> - <span class="text"><slot name="suffix"></slot></span> - <i class="fas fa-chevron-right icon"></i> - </span> - </MkA> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import './form.scss'; - -export default defineComponent({ - props: { - to: { - type: String, - required: true - }, - active: { - type: Boolean, - required: false - }, - external: { - type: Boolean, - required: false - }, - behavior: { - type: String, - required: false, - }, - }, - data() { - return { - }; - } -}); -</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); - background: var(--panelHighlight); - } - - > .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; - - > .text:not(:empty) { - margin-right: 0.75em; - } - } - } -} -</style> diff --git a/src/client/components/form/object-view.vue b/src/client/components/form/object-view.vue deleted file mode 100644 index 59fb62b5e6..0000000000 --- a/src/client/components/form/object-view.vue +++ /dev/null @@ -1,102 +0,0 @@ -<template> -<FormGroup class="_formItem"> - <template #label><slot></slot></template> - <div class="drooglns _formItem" :class="{ tall }"> - <div class="input _formPanel"> - <textarea class="_monospace" - v-model="v" - readonly - :spellcheck="false" - ></textarea> - </div> - </div> - <template #caption><slot name="desc"></slot></template> -</FormGroup> -</template> - -<script lang="ts"> -import { defineComponent, ref, toRefs, watch } from 'vue'; -import * as JSON5 from 'json5'; -import './form.scss'; -import FormGroup from './group.vue'; - -export default defineComponent({ - components: { - FormGroup, - }, - props: { - value: { - required: false - }, - tall: { - type: Boolean, - required: false, - default: false - }, - pre: { - type: Boolean, - required: false, - default: false - }, - manualSave: { - type: Boolean, - required: false, - default: false - }, - }, - setup(props, context) { - const { value } = toRefs(props); - const v = ref(''); - - watch(() => value, newValue => { - v.value = JSON5.stringify(newValue.value, null, '\t'); - }, { - immediate: true - }); - - return { - v, - }; - } -}); -</script> - -<style lang="scss" scoped> -.drooglns { - position: relative; - - > .input { - position: relative; - - > textarea { - display: block; - width: 100%; - min-width: 100%; - max-width: 100%; - min-height: 130px; - margin: 0; - padding: 16px var(--formContentHMargin); - 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); - tab-size: 2; - white-space: pre; - } - } - - &.tall { - > .input { - > textarea { - min-height: 200px; - } - } - } -} -</style> diff --git a/src/client/components/form/pagination.vue b/src/client/components/form/pagination.vue deleted file mode 100644 index 0a2f1ff0e1..0000000000 --- a/src/client/components/form/pagination.vue +++ /dev/null @@ -1,42 +0,0 @@ -<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">{{ $ts.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 '@client/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/radio.vue b/src/client/components/form/radio.vue new file mode 100644 index 0000000000..0f31d8fa0a --- /dev/null +++ b/src/client/components/form/radio.vue @@ -0,0 +1,122 @@ +<template> +<div + class="novjtctn" + :class="{ disabled, checked }" + :aria-checked="checked" + :aria-disabled="disabled" + @click="toggle" +> + <input type="radio" + :disabled="disabled" + > + <span class="button"> + <span></span> + </span> + <span class="label"><slot></slot></span> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; + +export default defineComponent({ + props: { + modelValue: { + required: false + }, + value: { + required: false + }, + disabled: { + type: Boolean, + default: false + } + }, + computed: { + checked(): boolean { + return this.modelValue === this.value; + } + }, + methods: { + toggle() { + if (this.disabled) return; + this.$emit('update:modelValue', this.value); + } + } +}); +</script> + +<style lang="scss" scoped> +.novjtctn { + position: relative; + display: inline-block; + margin: 8px 20px 0 0; + text-align: left; + cursor: pointer; + transition: all 0.3s; + + > * { + user-select: none; + } + + &.disabled { + opacity: 0.6; + + &, * { + cursor: not-allowed !important; + } + } + + &.checked { + > .button { + border-color: var(--accent); + + &:after { + background-color: var(--accent); + transform: scale(1); + opacity: 1; + } + } + } + + > input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + margin: 0; + } + + > .button { + position: absolute; + width: 20px; + height: 20px; + background: none; + border: solid 2px 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: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + } + } + + > .label { + margin-left: 28px; + display: block; + font-size: 16px; + line-height: 20px; + cursor: pointer; + } +} +</style> diff --git a/src/client/components/form/radios.vue b/src/client/components/form/radios.vue index b660c37ace..1d3d80172a 100644 --- a/src/client/components/form/radios.vue +++ b/src/client/components/form/radios.vue @@ -1,7 +1,6 @@ <script lang="ts"> import { defineComponent, h } from 'vue'; -import MkRadio from '@client/components/ui/radio.vue'; -import './form.scss'; +import MkRadio from './radio.vue'; export default defineComponent({ components: { @@ -18,9 +17,6 @@ export default defineComponent({ } }, watch: { - modelValue() { - this.value = this.modelValue; - }, value() { this.$emit('update:modelValue', this.value); } @@ -33,80 +29,38 @@ export default defineComponent({ if (options.length === 1 && options[0].props == null) options = options[0].children; return h('div', { - class: 'cnklmpwm _formItem' + class: 'novjtcto' }, [ - h('div', { - class: '_formLabel', - }, label), - ...options.map(option => h('button', { - class: '_button _formPanel _formClickable', + h('div', { class: 'label' }, label), + ...options.map(option => h(MkRadio, { key: option.key, - onClick: () => this.value = option.props.value, - }, [h('span', { - class: ['check', { checked: this.value === option.props.value }], - }), option.children])) + value: option.props.value, + modelValue: this.value, + 'onUpdate:modelValue': value => this.value = 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; - } +.novjtcto { + > .label { + font-size: 0.85em; + padding: 0 0 8px 12px; + user-select: none; - &:not(:last-of-type) { - border-bottom: solid 0.5px var(--divider); - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; + &:empty { + display: none; } + } - > .check { - display: inline-block; - vertical-align: bottom; - position: relative; - width: 16px; - height: 16px; - 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); + &:first-child { + margin-top: 0; + } - &:after { - background-color: var(--accent); - transform: scale(1); - opacity: 1; - } - } - } + &:last-child { + margin-bottom: 0; } } </style> diff --git a/src/client/components/form/range.vue b/src/client/components/form/range.vue index 65d665c70a..4cfe66a8fc 100644 --- a/src/client/components/form/range.vue +++ b/src/client/components/form/range.vue @@ -1,21 +1,20 @@ <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 class="timctyfi" :class="{ focused, disabled }"> + <div class="icon"><slot name="icon"></slot></div> + <span class="label"><slot name="label"></slot></span> + <input + type="range" + ref="input" + v-model="v" + :disabled="disabled" + :min="min" + :max="max" + :step="step" + :autofocus="autofocus" + @focus="focused = true" + @blur="focused = false" + @input="$emit('update:value', $event.target.value)" + /> </div> </template> @@ -49,6 +48,10 @@ export default defineComponent({ required: false, default: 1 }, + autofocus: { + type: Boolean, + required: false + } }, data() { return { @@ -61,61 +64,75 @@ export default defineComponent({ this.v = parseFloat(v); } }, + mounted() { + if (this.autofocus) { + this.$nextTick(() => { + this.$refs.input.focus(); + }); + } + } }); </script> <style lang="scss" scoped> -.ifitouly { +.timctyfi { position: relative; + margin: 8px; - > .main { - padding: 22px 16px; + > .icon { + display: inline-block; + width: 24px; + text-align: center; + } - > 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; + > .title { + pointer-events: none; + font-size: 16px; + color: var(--inputLabel); + overflow: hidden; + } - &.disabled { - opacity: 0.6; - cursor: not-allowed; - } + > input { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: var(--X10); + height: 7px; + margin: 0 8px; + outline: 0; + border: 0; + border-radius: 7px; - &::-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; - } + &.disabled { + opacity: 0.6; + cursor: not-allowed; + } - &::-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); - } + &::-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); } } } diff --git a/src/client/components/form/section.vue b/src/client/components/form/section.vue new file mode 100644 index 0000000000..8eac40a0db --- /dev/null +++ b/src/client/components/form/section.vue @@ -0,0 +1,31 @@ +<template> +<div class="vrtktovh" v-size="{ max: [500] }" v-sticky-container> + <div class="label"><slot name="label"></slot></div> + <div class="main"> + <slot></slot> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; + +export default defineComponent({ + +}); +</script> + +<style lang="scss" scoped> +.vrtktovh { + border-top: solid 0.5px var(--divider); + + > .label { + font-weight: bold; + padding: 24px 0 16px 0; + } + + > .main { + margin-bottom: 32px; + } +} +</style> diff --git a/src/client/components/form/select.vue b/src/client/components/form/select.vue index 1c5a473451..257e2cc990 100644 --- a/src/client/components/form/select.vue +++ b/src/client/components/form/select.vue @@ -1,125 +1,216 @@ <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" +<div class="vblkjoeq"> + <div class="label" @click="focus"><slot name="label"></slot></div> + <div class="input" :class="{ inline, disabled, focused }"> + <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div> + <select ref="inputEl" v-model="v" - :required="required" :disabled="disabled" + :required="required" + :readonly="readonly" + :placeholder="placeholder" @focus="focused = true" @blur="focused = false" + @input="onInput" > <slot></slot> </select> - <div class="suffix"> - <i class="fas fa-chevron-down"></i> - </div> + <div class="suffix" ref="suffixEl"><i class="fas fa-chevron-down"></i></div> </div> - <div class="_formCaption"><slot name="caption"></slot></div> + <div class="caption"><slot name="caption"></slot></div> + + <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> </div> </template> <script lang="ts"> -import { defineComponent } from 'vue'; -import './form.scss'; +import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; +import MkButton from '../ui/button.vue'; export default defineComponent({ + components: { + MkButton, + }, + props: { - value: { - required: false + modelValue: { + required: true }, required: { type: Boolean, required: false }, + readonly: { + type: Boolean, + required: false + }, disabled: { type: Boolean, required: false }, + placeholder: { + type: String, + required: false + }, + autofocus: { + type: Boolean, + required: false, + default: false + }, inline: { type: Boolean, required: false, default: false }, + manualSave: { + type: Boolean, + required: false, + default: false + }, }, - data() { - return { + + emits: ['change', 'update:modelValue'], + + setup(props, context) { + const { modelValue, autofocus } = toRefs(props); + const v = ref(modelValue.value); + 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); }; - }, - computed: { - v: { - get() { - return this.value; - }, - set(v) { - this.$emit('update:value', v); + + const updated = () => { + changed.value = false; + context.emit('update:modelValue', v.value); + }; + + watch(modelValue, newValue => { + v.value = newValue; + }); + + watch(v, newValue => { + if (!props.manualSave) { + updated(); } - }, + + 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 { + v, + focused, + invalid, + changed, + filled, + inputEl, + prefixEl, + suffixEl, + focus, + onInput, + updated, + }; }, - methods: { - focus() { - this.$refs.input.focus(); - } - } }); </script> <style lang="scss" scoped> -.yrtfrpux { - position: relative; +.vblkjoeq { + > .label { + font-size: 0.85em; + padding: 0 0 8px 12px; + user-select: none; - > .icon { - position: absolute; - top: 0; - left: 0; - width: 24px; - text-align: center; - line-height: 32px; + &:empty { + display: none; + } + } - &:not(:empty) + .input { - margin-left: 28px; + > .caption { + font-size: 0.8em; + padding: 8px 0 0 12px; + color: var(--fgTransparentWeak); + + &:empty { + display: none; } } > .input { - display: flex; + $height: 42px; position: relative; > select { + appearance: none; + -webkit-appearance: none; display: block; - flex: 1; + height: $height; width: 100%; - padding: 0 16px; + margin: 0; + padding: 0 12px; font: inherit; font-weight: normal; font-size: 1em; - height: 48px; - background: none; - border: none; - border-radius: 0; + color: var(--fg); + background: var(--panel); + border: solid 1px var(--inputBorder); + border-radius: 6px; outline: none; box-shadow: none; - appearance: none; - -webkit-appearance: none; - color: var(--fg); + box-sizing: border-box; + cursor: pointer; + transition: border-color 0.1s ease-out; - option, - optgroup { - color: var(--fg); - background: var(--bg); + &:hover { + border-color: var(--inputBorderHover); } } > .prefix, > .suffix { - display: block; - align-self: center; - justify-self: center; + display: flex; + align-items: center; + position: absolute; + z-index: 1; + top: 0; + padding: 0 12px; font-size: 1em; - line-height: 32px; - color: var(--inputLabel); + height: $height; pointer-events: none; &:empty { @@ -127,18 +218,42 @@ export default defineComponent({ } > * { - display: block; + display: inline-block; min-width: 16px; + max-width: 150px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } } > .prefix { - padding-right: 4px; + left: 0; + padding-right: 6px; } > .suffix { - padding: 0 16px 0 0; + right: 0; + padding-left: 6px; + } + + &.inline { + display: inline-block; + margin: 0; + } + + &.focused { + > select { + border-color: var(--accent); + } + } + + &.disabled { opacity: 0.7; + + &, * { + cursor: not-allowed !important; + } } } } diff --git a/src/client/components/form/slot.vue b/src/client/components/form/slot.vue new file mode 100644 index 0000000000..8580c1307d --- /dev/null +++ b/src/client/components/form/slot.vue @@ -0,0 +1,50 @@ +<template> +<div class="adhpbeou"> + <div class="label" @click="focus"><slot name="label"></slot></div> + <div class="content"> + <slot></slot> + </div> + <div class="caption"><slot name="caption"></slot></div> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; + +export default defineComponent({ + +}); +</script> + +<style lang="scss" scoped> +.adhpbeou { + margin: 1.5em 0; + + > .label { + font-size: 0.85em; + padding: 0 0 8px 12px; + user-select: none; + + &:empty { + display: none; + } + } + + > .caption { + font-size: 0.8em; + padding: 8px 0 0 12px; + color: var(--fgTransparentWeak); + + &:empty { + display: none; + } + } + + > .content { + position: relative; + background: var(--panel); + border: solid 0.5px var(--inputBorder); + border-radius: 6px; + } +} +</style> diff --git a/src/client/components/form/suspense.vue b/src/client/components/form/suspense.vue deleted file mode 100644 index d04dc07624..0000000000 --- a/src/client/components/form/suspense.vue +++ /dev/null @@ -1,101 +0,0 @@ -<template> -<transition name="fade" mode="out-in"> - <div class="_formItem" v-if="pending"> - <div class="_formPanel"> - <MkLoading/> - </div> - </div> - <div v-else-if="resolved" class="_formItem"> - <slot :result="result"></slot> - </div> - <div class="_formItem" v-else> - <div class="_formPanel eiurkvay"> - <div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div> - <MkButton inline @click="retry" class="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton> - </div> - </div> -</transition> -</template> - -<script lang="ts"> -import { defineComponent, PropType, ref, watch } from 'vue'; -import './form.scss'; -import MkButton from '@client/components/ui/button.vue'; - -export default defineComponent({ - components: { - MkButton - }, - - props: { - p: { - type: Function as PropType<() => Promise<any>>, - required: true, - } - }, - - setup(props, context) { - const pending = ref(true); - const resolved = ref(false); - const rejected = ref(false); - const result = ref(null); - - const process = () => { - if (props.p == null) { - return; - } - const promise = props.p(); - pending.value = true; - resolved.value = false; - rejected.value = false; - promise.then((_result) => { - pending.value = false; - resolved.value = true; - result.value = _result; - }); - promise.catch(() => { - pending.value = false; - rejected.value = true; - }); - }; - - watch(() => props.p, () => { - process(); - }, { - immediate: true - }); - - const retry = () => { - process(); - }; - - return { - pending, - resolved, - rejected, - result, - retry, - }; - } -}); -</script> - -<style lang="scss" scoped> -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.125s ease; -} -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} - -.eiurkvay { - padding: 16px; - text-align: center; - - > .retry { - margin-top: 16px; - } -} -</style> diff --git a/src/client/components/form/switch.vue b/src/client/components/form/switch.vue index e7ef714c49..85f8b7c870 100644 --- a/src/client/components/form/switch.vue +++ b/src/client/components/form/switch.vue @@ -1,35 +1,34 @@ <template> -<div class="ijnpvmgr _formItem"> - <div class="main _formPanel _formClickable" - :class="{ disabled, checked }" - :aria-checked="checked" - :aria-disabled="disabled" - @click.prevent="toggle" +<div + class="ziffeoms" + :class="{ disabled, checked }" + role="switch" + :aria-checked="checked" + :aria-disabled="disabled" + @click.prevent="toggle" +> + <input + type="checkbox" + ref="input" + :disabled="disabled" + @keydown.enter="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> + <span class="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff"> + <span class="handle"></span> + </span> + <span class="label"> + <span><slot></slot></span> + <p><slot name="caption"></slot></p> + </span> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import './form.scss'; export default defineComponent({ props: { - value: { + modelValue: { type: Boolean, default: false }, @@ -40,91 +39,110 @@ export default defineComponent({ }, computed: { checked(): boolean { - return this.value; + return this.modelValue; } }, methods: { toggle() { if (this.disabled) return; - this.$emit('update:value', !this.checked); + this.$emit('update:modelValue', !this.checked); } } }); </script> <style lang="scss" scoped> -.ijnpvmgr { - > .main { - position: relative; - display: flex; - padding: 14px 16px; - cursor: pointer; +.ziffeoms { + position: relative; + display: flex; + cursor: pointer; + transition: all 0.3s; - > * { - user-select: none; - } + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + + > * { + user-select: none; + } + + > input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + margin: 0; + } - &.disabled { - opacity: 0.6; - cursor: not-allowed; + > .button { + position: relative; + display: inline-block; + flex-shrink: 0; + margin: 0; + width: 36px; + height: 26px; + background: var(--switchBg); + outline: none; + border-radius: 999px; + transition: inherit; + + > .handle { + position: absolute; + top: 0; + bottom: 0; + left: 5px; + margin: auto 0; + border-radius: 100%; + transition: background-color 0.3s, transform 0.3s; + width: 16px; + height: 16px; + background-color: #fff; } + } - &.checked { - > .button { - background-color: var(--X10); - border-color: var(--X10); + > .label { + margin-left: 16px; + margin-top: 2px; + display: block; + cursor: pointer; + transition: inherit; + color: var(--fg); - > * { - background-color: var(--accent); - transform: translateX(14px); - } - } + > span { + display: block; + line-height: 20px; + transition: inherit; } - > input { - position: absolute; - width: 0; - height: 0; - opacity: 0; + > p { margin: 0; + color: var(--fgTransparentWeak); + font-size: 90%; } + } + &:hover { > .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); - } + background-color: var(--accentedBg); } + } - > .label { - margin-left: 12px; - display: block; - transition: inherit; - color: var(--fg); + &.disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &.checked { + > .button { + background-color: var(--accent); + border-color: var(--accent); - > span { - display: block; - line-height: 20px; - transition: inherit; + > .handle { + transform: translateX(10px); } } } diff --git a/src/client/components/form/textarea.vue b/src/client/components/form/textarea.vue index 8f42581a9b..50be69f930 100644 --- a/src/client/components/form/textarea.vue +++ b/src/client/components/form/textarea.vue @@ -1,40 +1,45 @@ <template> -<FormGroup class="_formItem"> - <template #label><slot></slot></template> - <div class="rivhosbp _formItem" :class="{ tall, pre }"> - <div class="input _formPanel"> - <textarea ref="input" :class="{ code, _monospace: code }" - v-model="v" - :required="required" - :readonly="readonly" - :pattern="pattern" - :autocomplete="autocomplete" - :spellcheck="!code" - @input="onInput" - @focus="focused = true" - @blur="focused = false" - ></textarea> - </div> +<div class="adhpbeos"> + <div class="label" @click="focus"><slot name="label"></slot></div> + <div class="input" :class="{ disabled, focused, tall, pre }"> + <textarea ref="inputEl" + :class="{ code, _monospace: code }" + v-model="v" + :disabled="disabled" + :required="required" + :readonly="readonly" + :placeholder="placeholder" + :pattern="pattern" + :autocomplete="autocomplete" + :spellcheck="spellcheck" + @focus="focused = true" + @blur="focused = false" + @keydown="onKeydown($event)" + @input="onInput" + ></textarea> </div> - <template #caption><slot name="desc"></slot></template> + <div class="caption"><slot name="caption"></slot></div> - <FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> -</FormGroup> + <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> +</div> </template> <script lang="ts"> -import { defineComponent, ref, toRefs, watch } from 'vue'; -import './form.scss'; -import FormButton from './button.vue'; -import FormGroup from './group.vue'; +import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; +import MkButton from '../ui/button.vue'; +import { debounce } from 'throttle-debounce'; export default defineComponent({ components: { - FormGroup, - FormButton, + MkButton, }, + props: { - value: { + modelValue: { + required: true + }, + type: { + type: String, required: false }, required: { @@ -45,14 +50,29 @@ export default defineComponent({ type: Boolean, required: false }, + disabled: { + type: Boolean, + required: false + }, pattern: { type: String, required: false }, - autocomplete: { + placeholder: { type: String, required: false }, + autofocus: { + type: Boolean, + required: false, + default: false + }, + autocomplete: { + required: false + }, + spellcheck: { + required: false + }, code: { type: Boolean, required: false @@ -67,91 +87,162 @@ export default defineComponent({ required: false, default: false }, + debounce: { + type: Boolean, + required: false, + default: false + }, manualSave: { type: Boolean, required: false, default: false }, }, + + emits: ['change', 'keydown', 'enter', 'update:modelValue'], + setup(props, context) { - const { value } = toRefs(props); - const v = ref(value.value); + const { modelValue, autofocus } = toRefs(props); + const v = ref(modelValue.value); + 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 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'); + } + }; const updated = () => { changed.value = false; - context.emit('update:value', v.value); + context.emit('update:modelValue', v.value); }; - watch(value, newValue => { + const debouncedUpdated = debounce(1000, updated); + + watch(modelValue, newValue => { v.value = newValue; }); watch(v, newValue => { if (!props.manualSave) { - updated(); + if (props.debounce) { + debouncedUpdated(); + } else { + updated(); + } } + + invalid.value = inputEl.value.validity.badInput; }); - + + onMounted(() => { + nextTick(() => { + if (autofocus.value) { + focus(); + } + }); + }); + return { v, - updated, + focused, + invalid, changed, + filled, + inputEl, focus, onInput, + onKeydown, + updated, }; - } + }, }); </script> <style lang="scss" scoped> -.rivhosbp { - position: relative; +.adhpbeos { + > .label { + font-size: 0.85em; + padding: 0 0 8px 12px; + user-select: none; + + &:empty { + display: none; + } + } + + > .caption { + font-size: 0.8em; + padding: 8px 0 0 12px; + color: var(--fgTransparentWeak); + + &:empty { + display: none; + } + } > .input { position: relative; - + > textarea { + appearance: none; + -webkit-appearance: none; display: block; width: 100%; min-width: 100%; max-width: 100%; min-height: 130px; margin: 0; - padding: 16px; - box-sizing: border-box; + padding: 12px; font: inherit; font-weight: normal; font-size: 1em; - background: transparent; - border: none; - border-radius: 0; + color: var(--fg); + background: var(--panel); + border: solid 0.5px var(--inputBorder); + border-radius: 6px; outline: none; box-shadow: none; - color: var(--fg); + box-sizing: border-box; + transition: border-color 0.1s ease-out; - &.code { - tab-size: 2; + &:hover { + border-color: var(--inputBorderHover); + } + } + + &.focused { + > textarea { + border-color: var(--accent); } } - } - &.tall { - > .input { + &.disabled { + opacity: 0.7; + + &, * { + cursor: not-allowed !important; + } + } + + &.tall { > textarea { min-height: 200px; } } - } - &.pre { - > .input { + &.pre { > textarea { white-space: pre; } diff --git a/src/client/components/form/tuple.vue b/src/client/components/form/tuple.vue deleted file mode 100644 index 6c8a22d189..0000000000 --- a/src/client/components/form/tuple.vue +++ /dev/null @@ -1,36 +0,0 @@ -<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> |