diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2018-06-15 07:58:58 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-06-15 07:58:58 +0900 |
| commit | 39b1978ff358986281f4abc28e31a1ded91fe990 (patch) | |
| tree | 4ea8a507adb275b6a29890fe8502c8e0ce6759e2 /src/client/app/common | |
| parent | 2.38.3 (diff) | |
| parent | wip (diff) | |
| download | misskey-39b1978ff358986281f4abc28e31a1ded91fe990.tar.gz misskey-39b1978ff358986281f4abc28e31a1ded91fe990.tar.bz2 misskey-39b1978ff358986281f4abc28e31a1ded91fe990.zip | |
Merge pull request #1713 from syuilo/without-vue-material
Without vue material
Diffstat (limited to 'src/client/app/common')
| -rw-r--r-- | src/client/app/common/views/components/index.ts | 16 | ||||
| -rw-r--r-- | src/client/app/common/views/components/signup.vue | 192 | ||||
| -rw-r--r-- | src/client/app/common/views/components/ui/button.vue | 82 | ||||
| -rw-r--r-- | src/client/app/common/views/components/ui/card.vue | 46 | ||||
| -rw-r--r-- | src/client/app/common/views/components/ui/form.vue | 30 | ||||
| -rw-r--r-- | src/client/app/common/views/components/ui/input.vue | 321 | ||||
| -rw-r--r-- | src/client/app/common/views/components/ui/radio.vue | 120 | ||||
| -rw-r--r-- | src/client/app/common/views/components/ui/select.vue | 215 | ||||
| -rw-r--r-- | src/client/app/common/views/components/ui/switch.vue | 135 | ||||
| -rw-r--r-- | src/client/app/common/views/components/ui/textarea.vue | 174 |
10 files changed, 1174 insertions, 157 deletions
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index 803854468e..b91008f718 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -29,6 +29,14 @@ import fileTypeIcon from './file-type-icon.vue'; import Switch from './switch.vue'; import Othello from './othello.vue'; import welcomeTimeline from './welcome-timeline.vue'; +import uiInput from './ui/input.vue'; +import uiButton from './ui/button.vue'; +import uiCard from './ui/card.vue'; +import uiForm from './ui/form.vue'; +import uiTextarea from './ui/textarea.vue'; +import uiSwitch from './ui/switch.vue'; +import uiRadio from './ui/radio.vue'; +import uiSelect from './ui/select.vue'; Vue.component('mk-analog-clock', analogClock); Vue.component('mk-menu', menu); @@ -59,3 +67,11 @@ Vue.component('mk-file-type-icon', fileTypeIcon); Vue.component('mk-switch', Switch); Vue.component('mk-othello', Othello); Vue.component('mk-welcome-timeline', welcomeTimeline); +Vue.component('ui-input', uiInput); +Vue.component('ui-button', uiButton); +Vue.component('ui-card', uiCard); +Vue.component('ui-form', uiForm); +Vue.component('ui-textarea', uiTextarea); +Vue.component('ui-switch', uiSwitch); +Vue.component('ui-radio', uiRadio); +Vue.component('ui-select', uiSelect); diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue index f8bf7dd798..987cc7e52d 100644 --- a/src/client/app/common/views/components/signup.vue +++ b/src/client/app/common/views/components/signup.vue @@ -1,60 +1,58 @@ <template> -<form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off"> - <label class="username"> - <p class="caption">%fa:at%%i18n:@username%</p> - <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/> - <p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/@${username}` }}</p> - <p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:@checking%</p> - <p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:@available%</p> - <p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@unavailable%</p> - <p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@error%</p> - <p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@invalid-format%</p> - <p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-short%</p> - <p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-long%</p> - </label> - <label class="password"> - <p class="caption">%fa:lock%%i18n:@password%</p> - <input v-model="password" type="password" placeholder="%i18n:@password-placeholder%" autocomplete="off" required @input="onChangePassword"/> - <div class="meter" v-show="passwordStrength != ''" :data-strength="passwordStrength"> - <div class="value" ref="passwordMetar"></div> +<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()"> + <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" required @input="onChangeUsername"> + <span>%i18n:@username%</span> + <span slot="prefix">@</span> + <span slot="suffix">@{{ host }}</span> + <p slot="text" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw% %i18n:@checking%</p> + <p slot="text" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw% %i18n:@available%</p> + <p slot="text" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@unavailable%</p> + <p slot="text" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@error%</p> + <p slot="text" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@invalid-format%</p> + <p slot="text" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-short%</p> + <p slot="text" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-long%</p> + </ui-input> + <ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true"> + <span>%i18n:@password%</span> + <span slot="prefix">%fa:lock%</span> + <div slot="text"> + <p slot="text" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@weak-password%</p> + <p slot="text" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw% %i18n:@normal-password%</p> + <p slot="text" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw% %i18n:@strong-password%</p> </div> - <p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@weak-password%</p> - <p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:@normal-password%</p> - <p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:@strong-password%</p> - </label> - <label class="retype-password"> - <p class="caption">%fa:lock%%i18n:@password%(%i18n:@retype%)</p> - <input v-model="retypedPassword" type="password" placeholder="%i18n:@retype-placeholder%" autocomplete="off" required @input="onChangePasswordRetype"/> - <p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:@password-matched%</p> - <p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@password-not-matched%</p> - </label> - <label class="recaptcha"> - <p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:@recaptcha%</p> - <div class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div> - </label> - <label class="agree-tou"> - <input name="agree-tou" type="checkbox" autocomplete="off" required/> + </ui-input> + <ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype"> + <span>%i18n:@password% (%i18n:@retype%)</span> + <span slot="prefix">%fa:lock%</span> + <div slot="text"> + <p slot="text" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw% %i18n:@password-matched%</p> + <p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@password-not-matched%</p> + </div> + </ui-input> + <div class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div> + <label class="agree-tou" style="display: block; margin: 16px 0;"> + <input name="agree-tou" type="checkbox" required/> <p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p> </label> - <button type="submit">%i18n:@create%</button> + <ui-button type="submit">%i18n:@create%</ui-button> </form> </template> <script lang="ts"> import Vue from 'vue'; const getPasswordStrength = require('syuilo-password-strength'); -import { url, docsUrl, lang, recaptchaSitekey } from '../../../config'; +import { host, url, docsUrl, lang, recaptchaSitekey } from '../../../config'; export default Vue.extend({ data() { return { + host, username: '', password: '', retypedPassword: '', url, touUrl: `${docsUrl}/${lang}/tou`, recaptchaSitekey, - recaptchaed: false, usernameState: null, passwordStrength: '', passwordRetypeState: null @@ -104,7 +102,6 @@ export default Vue.extend({ const strength = getPasswordStrength(this.password); this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; - (this.$refs.passwordMetar as any).style.width = `${strength * 100}%`; }, onChangePasswordRetype() { if (this.retypedPassword == '') { @@ -130,19 +127,9 @@ export default Vue.extend({ alert('%i18n:@some-error%'); (window as any).grecaptcha.reset(); - this.recaptchaed = false; }); } }, - created() { - (window as any).onRecaptchaed = () => { - this.recaptchaed = true; - }; - - (window as any).onRecaptchaExpired = () => { - this.recaptchaed = false; - }; - }, mounted() { const head = document.getElementsByTagName('head')[0]; const script = document.createElement('script'); @@ -158,100 +145,6 @@ export default Vue.extend({ .mk-signup min-width 302px - label - display block - margin 0 0 16px 0 - - > .caption - margin 0 0 4px 0 - color #828888 - font-size 0.95em - - > [data-fa] - margin-right 0.25em - color #96adac - - > .info - display block - margin 4px 0 - font-size 0.8em - - > [data-fa] - margin-right 0.3em - - &.username - .profile-page-url-preview - display block - margin 4px 8px 0 4px - font-size 0.8em - color #888 - - &:empty - display none - - &:not(:empty) + .info - margin-top 0 - - &.password - .meter - display block - margin-top 8px - width 100% - height 8px - - &[data-strength=''] - display none - - &[data-strength='low'] - > .value - background #d73612 - - &[data-strength='medium'] - > .value - background #d7ca12 - - &[data-strength='high'] - > .value - background #61bb22 - - > .value - display block - width 0% - height 100% - background transparent - border-radius 4px - transition all 0.1s ease - - [type=text], [type=password] - user-select text - display inline-block - cursor auto - padding 0 12px - margin 0 - width 100% - line-height 44px - font-size 1em - color #333 !important - background #fff !important - outline none - border solid 1px rgba(#000, 0.1) - border-radius 4px - box-shadow 0 0 0 114514px #fff inset - transition all .3s ease - - &:hover - border-color rgba(#000, 0.2) - transition all .1s ease - - &:focus - color $theme-color !important - border-color $theme-color - box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%) - transition all 0s ease - - &:disabled - opacity 0.5 - .agree-tou padding 4px border-radius 4px @@ -269,19 +162,4 @@ export default Vue.extend({ display inline color #555 - button - margin 0 - padding 16px - width 100% - font-size 1em - color #fff - background $theme-color - border-radius 3px - - &:hover - background lighten($theme-color, 5%) - - &:active - background darken($theme-color, 5%) - </style> diff --git a/src/client/app/common/views/components/ui/button.vue b/src/client/app/common/views/components/ui/button.vue new file mode 100644 index 0000000000..e778750354 --- /dev/null +++ b/src/client/app/common/views/components/ui/button.vue @@ -0,0 +1,82 @@ +<template> +<div class="ui-button" :class="[styl]"> + <button :type="type" @click="$emit('click')"> + <slot></slot> + </button> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: { + type: { + type: String, + required: false + } + }, + data() { + return { + styl: 'fill' + }; + }, + inject: { + isCardChild: { default: false } + }, + created() { + if (this.isCardChild) { + this.styl = 'line'; + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark, fill) + > button + display block + width 100% + margin 0 + padding 0 + font-weight bold + font-size 16px + line-height 44px + border none + border-radius 6px + outline none + box-shadow none + + if fill + color $theme-color-foreground + background $theme-color + + &:hover + background lighten($theme-color, 5%) + + &:active + background darken($theme-color, 5%) + else + color $theme-color + background none + + &:hover + color darken($theme-color, 5%) + + &:active + background rgba($theme-color, 0.3) + +.ui-button[data-darkmode] + &.fill + root(true, true) + &:not(.fill) + root(true, false) + +.ui-button:not([data-darkmode]) + &.fill + root(false, true) + &:not(.fill) + root(false, false) + +</style> diff --git a/src/client/app/common/views/components/ui/card.vue b/src/client/app/common/views/components/ui/card.vue new file mode 100644 index 0000000000..05c51bca6b --- /dev/null +++ b/src/client/app/common/views/components/ui/card.vue @@ -0,0 +1,46 @@ +<template> +<div class="ui-card"> + <header> + <slot name="title"></slot> + </header> + + <slot></slot> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + provide() { + return { + isCardChild: true + }; + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark) + margin 16px + padding 16px + color isDark ? #fff : #000 + background isDark ? #282C37 : #fff + box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) + + @media (min-width 500px) + padding 32px + + > header + font-weight normal + font-size 24px + color isDark ? #fff : #444 + +.ui-card[data-darkmode] + root(true) + +.ui-card:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/common/views/components/ui/form.vue b/src/client/app/common/views/components/ui/form.vue new file mode 100644 index 0000000000..fc8fdad9c4 --- /dev/null +++ b/src/client/app/common/views/components/ui/form.vue @@ -0,0 +1,30 @@ +<template> +<div class="ui-form"> + <fieldset :disabled="disabled"> + <slot></slot> + </fieldset> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: { + disabled: { + type: Boolean, + required: false + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.ui-form + > fieldset + margin 0 + padding 0 + border none + +</style> diff --git a/src/client/app/common/views/components/ui/input.vue b/src/client/app/common/views/components/ui/input.vue new file mode 100644 index 0000000000..ec91ca364c --- /dev/null +++ b/src/client/app/common/views/components/ui/input.vue @@ -0,0 +1,321 @@ +<template> +<div class="ui-input" :class="[{ focused, filled }, styl]"> + <div class="icon" ref="icon"><slot name="icon"></slot></div> + <div class="input" @click="focus" @mousedown="focus"> + <div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength"> + <div class="value" ref="passwordMetar"></div> + </div> + <span class="label" ref="label"><slot></slot></span> + <div class="prefix" ref="prefix"><slot name="prefix"></slot></div> + <template v-if="type != 'file'"> + <input ref="input" + :type="type" + :value="v" + :required="required" + :readonly="readonly" + :pattern="pattern" + :autocomplete="autocomplete" + @input="$emit('input', $event.target.value)" + @focus="focused = true" + @blur="focused = false"> + </template> + <template v-else> + <input ref="input" + type="text" + :value="placeholder" + readonly + @click="chooseFile"> + <input ref="file" + type="file" + :value="value" + @change="onChangeFile"> + </template> + <div class="suffix"><slot name="suffix"></slot></div> + </div> + <div class="text"><slot name="text"></slot></div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +const getPasswordStrength = require('syuilo-password-strength'); + +export default Vue.extend({ + props: { + value: { + required: false + }, + type: { + type: String, + required: false + }, + required: { + type: Boolean, + required: false + }, + readonly: { + type: Boolean, + required: false + }, + pattern: { + type: String, + required: false + }, + autocomplete: { + required: false + }, + withPasswordMeter: { + type: Boolean, + required: false, + default: false + } + }, + data() { + return { + v: this.value, + focused: false, + passwordStrength: '', + styl: 'fill' + }; + }, + computed: { + filled(): boolean { + return this.v != '' && this.v != null; + }, + placeholder(): string { + if (this.type != 'file') return null; + if (this.v == null) return null; + + if (typeof this.v == 'string') return this.v; + + if (Array.isArray(this.v)) { + return this.v.map(file => file.name).join(', '); + } else { + return this.v.name; + } + } + }, + watch: { + value(v) { + this.v = v; + }, + v(v) { + if (this.withPasswordMeter) { + if (v == '') { + this.passwordStrength = ''; + return; + } + + const strength = getPasswordStrength(v); + this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; + (this.$refs.passwordMetar as any).style.width = `${strength * 100}%`; + } + } + }, + inject: { + isCardChild: { default: false } + }, + created() { + if (this.isCardChild) { + this.styl = 'line'; + } + }, + mounted() { + if (this.$refs.prefix) { + this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; + } + }, + methods: { + focus() { + this.$refs.input.focus(); + }, + chooseFile() { + this.$refs.file.click(); + }, + onChangeFile() { + this.v = Array.from((this.$refs.file as any).files); + this.$emit('input', this.v); + this.$emit('change', this.v); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark, fill) + margin 32px 0 + + > .icon + position absolute + top 0 + left 0 + width 24px + text-align center + line-height 32px + color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54) + + &:not(:empty) + .input + margin-left 28px + + > .input + display flex + cursor text + + if fill + padding 6px 12px + background rgba(#000, 0.035) + border-radius 6px + else + &:before + content '' + display block + position absolute + bottom 0 + left 0 + right 0 + height 1px + background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42) + + &:after + content '' + display block + position absolute + bottom 0 + left 0 + right 0 + height 2px + background $theme-color + opacity 0 + transform scaleX(0.12) + transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) + will-change border opacity transform + + > .password-meter + position absolute + top 0 + left 0 + width 100% + height 100% + border-radius 6px + overflow hidden + opacity 0.3 + + &[data-strength=''] + display none + + &[data-strength='low'] + > .value + background #d73612 + + &[data-strength='medium'] + > .value + background #d7ca12 + + &[data-strength='high'] + > .value + background #61bb22 + + > .value + display block + width 0% + height 100% + background transparent + border-radius 6px + transition all 0.1s ease + + > .label + position absolute + top fill ? 6px : 0 + left 0 + pointer-events none + transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) + transition-duration 0.3s + font-size 16px + line-height 32px + color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54) + pointer-events none + //will-change transform + transform-origin top left + transform scale(1) + + > input + display block + flex 1 + width 100% + padding 0 + font inherit + font-weight fill ? bold : normal + font-size 16px + line-height 32px + color isDark ? #fff : #000 + background transparent + border none + border-radius 0 + outline none + box-shadow none + + &[type='file'] + display none + + > .prefix + > .suffix + display block + align-self center + justify-self center + font-size 16px + line-height 32px + color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54) + pointer-events none + + > * + display block + min-width 16px + + > .prefix + padding-right 4px + + > .suffix + padding-left 4px + + > .text + margin 6px 0 + font-size 13px + + * + margin 0 + + &.focused + > .input + if fill + background rgba(#000, 0.05) + else + &:after + opacity 1 + transform scaleX(1) + + > .label + color $theme-color + + &.focused + &.filled + > .input + > .label + top fill ? -24px : -17px + left 0 !important + transform scale(0.75) + +.ui-input[data-darkmode] + &.fill + root(true, true) + &:not(.fill) + root(true, false) + +.ui-input:not([data-darkmode]) + &.fill + root(false, true) + &:not(.fill) + root(false, false) + +</style> diff --git a/src/client/app/common/views/components/ui/radio.vue b/src/client/app/common/views/components/ui/radio.vue new file mode 100644 index 0000000000..04a46c5a96 --- /dev/null +++ b/src/client/app/common/views/components/ui/radio.vue @@ -0,0 +1,120 @@ +<template> +<div + class="ui-radio" + :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 Vue from 'vue'; +export default Vue.extend({ + model: { + prop: 'model', + event: 'change' + }, + props: { + model: { + type: String, + required: false + }, + value: { + type: String, + required: false + }, + disabled: { + type: Boolean, + default: false + } + }, + computed: { + checked(): boolean { + return this.model === this.value; + } + }, + methods: { + toggle() { + this.$emit('change', this.value); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark) + display inline-block + margin 32px 32px 32px 0 + cursor pointer + transition all 0.3s + + > * + user-select none + + &.disabled + opacity 0.6 + cursor not-allowed + + &.checked + > .button + border-color $theme-color + + &:after + background-color $theme-color + 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 isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54) + 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 + +.ui-radio[data-darkmode] + root(true) + +.ui-radio:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/common/views/components/ui/select.vue b/src/client/app/common/views/components/ui/select.vue new file mode 100644 index 0000000000..4273a4a0de --- /dev/null +++ b/src/client/app/common/views/components/ui/select.vue @@ -0,0 +1,215 @@ +<template> +<div class="ui-select" :class="[{ focused, filled }, styl]"> + <div class="icon" ref="icon"><slot name="icon"></slot></div> + <div class="input" @click="focus"> + <span class="label" ref="label"><slot name="label"></slot></span> + <div class="prefix" ref="prefix"><slot name="prefix"></slot></div> + <select ref="input" + :value="v" + :required="required" + @input="$emit('input', $event.target.value)" + @focus="focused = true" + @blur="focused = false"> + <slot></slot> + </select> + <div class="suffix"><slot name="suffix"></slot></div> + </div> + <div class="text"><slot name="text"></slot></div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + value: { + required: false + }, + required: { + type: Boolean, + required: false + } + }, + data() { + return { + v: this.value, + focused: false, + styl: 'fill' + }; + }, + computed: { + filled(): boolean { + return this.v != '' && this.v != null; + } + }, + watch: { + value(v) { + this.v = v; + } + }, + inject: { + isCardChild: { default: false } + }, + created() { + if (this.isCardChild) { + this.styl = 'line'; + } + }, + mounted() { + if (this.$refs.prefix) { + this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; + } + }, + methods: { + focus() { + this.$refs.input.focus(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark, fill) + margin 32px 0 + + > .icon + position absolute + top 0 + left 0 + width 24px + text-align center + line-height 32px + color rgba(#000, 0.54) + + &:not(:empty) + .input + margin-left 28px + + > .input + display flex + + if fill + padding 6px 12px + background rgba(#000, 0.035) + border-radius 6px + else + &:before + content '' + display block + position absolute + bottom 0 + left 0 + right 0 + height 1px + background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42) + + &:after + content '' + display block + position absolute + bottom 0 + left 0 + right 0 + height 2px + background $theme-color + opacity 0 + transform scaleX(0.12) + transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) + will-change border opacity transform + + > .label + position absolute + top fill ? 6px : 0 + left 0 + pointer-events none + transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) + transition-duration 0.3s + font-size 16px + line-height 32px + color rgba(#000, 0.54) + pointer-events none + //will-change transform + transform-origin top left + transform scale(1) + + > select + display block + flex 1 + width 100% + padding 0 + font inherit + font-weight fill ? bold : normal + font-size 16px + height 32px + color isDark ? #fff : #000 + background transparent + border none + border-radius 0 + outline none + box-shadow none + + * + color #000 + + > .prefix + > .suffix + display block + align-self center + justify-self center + font-size 16px + line-height 32px + color rgba(#000, 0.54) + pointer-events none + + > * + display block + min-width 16px + + > .prefix + padding-right 4px + + > .suffix + padding-left 4px + + > .text + margin 6px 0 + font-size 13px + + * + margin 0 + + &.focused + > .input + if fill + background rgba(#000, 0.05) + else + &:after + opacity 1 + transform scaleX(1) + + > .label + color $theme-color + + &.focused + &.filled + > .input + > .label + top fill ? -24px : -17px + left 0 !important + transform scale(0.75) + +.ui-select[data-darkmode] + &.fill + root(true, true) + &:not(.fill) + root(true, false) + +.ui-select:not([data-darkmode]) + &.fill + root(false, true) + &:not(.fill) + root(false, false) + +</style> diff --git a/src/client/app/common/views/components/ui/switch.vue b/src/client/app/common/views/components/ui/switch.vue new file mode 100644 index 0000000000..a9e00d73d2 --- /dev/null +++ b/src/client/app/common/views/components/ui/switch.vue @@ -0,0 +1,135 @@ +<template> +<div + class="ui-switch" + :class="{ disabled, checked }" + role="switch" + :aria-checked="checked" + :aria-disabled="disabled" + @click="toggle" +> + <input + type="checkbox" + ref="input" + :disabled="disabled" + @keydown.enter="toggle" + > + <span class="button"> + <span></span> + </span> + <span class="label"> + <span :aria-hidden="!checked"><slot></slot></span> + <p :aria-hidden="!checked"> + <slot name="text"></slot> + </p> + </span> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + model: { + prop: 'value', + event: 'change' + }, + props: { + value: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + } + }, + computed: { + checked(): boolean { + return this.value; + } + }, + methods: { + toggle() { + this.$emit('change', !this.checked); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark) + display flex + margin 32px 0 + cursor pointer + transition all 0.3s + + > * + user-select none + + &.disabled + opacity 0.6 + cursor not-allowed + + &.checked + > .button + background-color rgba($theme-color, 0.4) + border-color rgba($theme-color, 0.4) + + > * + background-color $theme-color + transform translateX(14px) + + > input + position absolute + width 0 + height 0 + opacity 0 + margin 0 + + > .button + display inline-block + margin 3px 0 0 0 + width 34px + height 14px + background isDark ? rgba(#fff, 0.15) : rgba(#000, 0.25) + outline none + border-radius 14px + transition inherit + + > * + 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 8px + display block + font-size 16px + cursor pointer + transition inherit + + > span + display block + line-height 20px + color isDark ? #c4ccd2 : rgba(#000, 0.75) + transition inherit + + > p + margin 0 + //font-size 90% + color isDark ? #78858e : #9daab3 + +.ui-switch[data-darkmode] + root(true) + +.ui-switch:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/common/views/components/ui/textarea.vue b/src/client/app/common/views/components/ui/textarea.vue new file mode 100644 index 0000000000..cc6b376ead --- /dev/null +++ b/src/client/app/common/views/components/ui/textarea.vue @@ -0,0 +1,174 @@ +<template> +<div class="ui-textarea" :class="{ focused, filled }"> + <div class="input"> + <span class="label" ref="label"><slot></slot></span> + <textarea ref="input" + :value="value" + :required="required" + :readonly="readonly" + :pattern="pattern" + :autocomplete="autocomplete" + @input="$emit('input', $event.target.value)" + @focus="focused = true" + @blur="focused = false"> + </textarea> + </div> + <div class="text"><slot name="text"></slot></div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +const getPasswordStrength = require('syuilo-password-strength'); + +export default Vue.extend({ + props: { + value: { + required: false + }, + required: { + type: Boolean, + required: false + }, + readonly: { + type: Boolean, + required: false + }, + pattern: { + type: String, + required: false + }, + autocomplete: { + type: String, + required: false + } + }, + data() { + return { + focused: false, + passwordStrength: '' + } + }, + computed: { + filled(): boolean { + return this.value != '' && this.value != null; + } + }, + methods: { + focus() { + this.$refs.input.focus(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +root(isDark, fill) + margin 32px 0 + + > .input + padding 12px + + if fill + background rgba(#000, 0.035) + border-radius 6px + else + &:before + content '' + display block + position absolute + top 0 + bottom 0 + left 0 + right 0 + background none + border solid 1px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42) + border-radius 3px + pointer-events none + + &:after + content '' + display block + position absolute + top 0 + bottom 0 + left 0 + right 0 + background none + border solid 2px $theme-color + border-radius 3px + opacity 0 + transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1) + pointer-events none + + > .label + position absolute + top 6px + left 12px + pointer-events none + transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) + transition-duration 0.3s + font-size 16px + line-height 32px + color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54) + pointer-events none + //will-change transform + transform-origin top left + transform scale(1) + + > textarea + display block + width 100% + min-height 100px + padding 0 + font inherit + font-weight fill ? bold : normal + font-size 16px + color isDark ? #fff : #000 + background transparent + border none + border-radius 0 + outline none + box-shadow none + + > .text + margin 6px 0 + font-size 13px + + * + margin 0 + + &.focused + > .input + if fill + background rgba(#000, 0.05) + else + &:after + opacity 1 + + > .label + color $theme-color + + &.focused + &.filled + > .input + > .label + top -24px + left 0 !important + transform scale(0.75) + +.ui-textarea[data-darkmode] + &.fill + root(true, true) + &:not(.fill) + root(true, false) + +.ui-textarea:not([data-darkmode]) + &.fill + root(false, true) + &:not(.fill) + root(false, false) + +</style> |