diff --git a/package.json b/package.json index 4b207d5e5a..b1b8888b5c 100644 --- a/package.json +++ b/package.json @@ -211,7 +211,6 @@ "vue-js-modal": "1.3.13", "vue-json-tree-view": "2.1.4", "vue-loader": "15.2.1", - "vue-material": "^1.0.0-beta-10.2", "vue-router": "3.0.1", "vue-template-compiler": "2.5.16", "vuedraggable": "2.16.0", diff --git a/src/client/app/app.styl b/src/client/app/app.styl index ba694b73ae..431b9daa65 100644 --- a/src/client/app/app.styl +++ b/src/client/app/app.styl @@ -7,11 +7,6 @@ html cursor progress !important body - // for md - font-size 16px !important - line-height initial !important - letter-spacing initial !important - overflow-wrap break-word #error 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> diff --git a/src/client/app/config.ts b/src/client/app/config.ts index 70c085de1c..e4a7ff6d38 100644 --- a/src/client/app/config.ts +++ b/src/client/app/config.ts @@ -1,6 +1,8 @@ declare const _HOST_: string; declare const _HOSTNAME_: string; declare const _URL_: string; +declare const _NAME_: string; +declare const _DESCRIPTION_: string; declare const _API_URL_: string; declare const _WS_URL_: string; declare const _DOCS_URL_: string; @@ -21,6 +23,8 @@ declare const _GOOGLE_MAPS_API_KEY_: string; export const host = _HOST_; export const hostname = _HOSTNAME_; export const url = _URL_; +export const name = _NAME_; +export const description = _DESCRIPTION_; export const apiUrl = _API_URL_; export const wsUrl = _WS_URL_; export const docsUrl = _DOCS_URL_; diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts index d505b38dcc..8ee078d621 100644 --- a/src/client/app/mobile/script.ts +++ b/src/client/app/mobile/script.ts @@ -2,17 +2,11 @@ * Mobile Client */ -import Vue from 'vue'; import VueRouter from 'vue-router'; -import { MdCard, MdButton, MdField, MdMenu, MdList, MdSwitch, MdSubheader, MdDialog, MdDialogAlert, MdRadio } from 'vue-material/dist/components'; -import 'vue-material/dist/vue-material.min.css'; -import 'vue-material/dist/theme/default.css'; - // Style import './style.styl'; import '../../element.scss'; -import '../../md.scss'; import init from '../init'; @@ -44,17 +38,6 @@ import MkSettings from './views/pages/settings.vue'; import MkOthello from './views/pages/othello.vue'; import MkTag from './views/pages/tag.vue'; -Vue.use(MdCard); -Vue.use(MdButton); -Vue.use(MdField); -Vue.use(MdMenu); -Vue.use(MdList); -Vue.use(MdSwitch); -Vue.use(MdSubheader); -Vue.use(MdDialog); -Vue.use(MdDialogAlert); -Vue.use(MdRadio); - /** * init */ diff --git a/src/client/app/mobile/style.styl b/src/client/app/mobile/style.styl index d1ab044eaf..df8f4a8fae 100644 --- a/src/client/app/mobile/style.styl +++ b/src/client/app/mobile/style.styl @@ -10,9 +10,6 @@ html height 100% background #ececed !important - // for md - transition none !important - &[data-darkmode] background #191B22 !important diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index 8da7a76633..1c5a43ede4 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -1,132 +1,84 @@ <template> <mk-ui> <span slot="header">%fa:cog%%i18n:@settings%</span> - <main> - <p v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p> + <main :data-darkmode="$store.state.device.darkmode"> + <div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></div> + <div> <x-profile/> - <md-card> - <md-card-header> - <div class="md-title">%fa:palette% %i18n:@design%</div> - </md-card-header> + <ui-card> + <div slot="title">%fa:palette% %i18n:@design%</div> - <md-card-content> - <div> - <md-switch v-model="darkmode">%i18n:@dark-mode%</md-switch> - </div> + <ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch> + <ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch> - <div> - <md-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</md-switch> - </div> + <div> + <div>%i18n:@timeline%</div> + <ui-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</ui-switch> + <ui-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</ui-switch> + <ui-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch> + </div> - <div> - <div class="md-body-2">%i18n:@timeline%</div> + <div> + <div>%i18n:@post-style%</div> + <ui-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</ui-radio> + <ui-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</ui-radio> + </div> + </ui-card> - <div> - <md-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</md-switch> - </div> + <ui-card> + <div slot="title">%fa:cog% %i18n:@behavior%</div> + <ui-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch> + <ui-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</ui-switch> + <ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch> + <ui-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</ui-switch> + <ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch> + </ui-card> - <div> - <md-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</md-switch> - </div> + <ui-card> + <div slot="title">%fa:language% %i18n:@lang%</div> - <div> - <md-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</md-switch> - </div> - </div> + <ui-select v-model="lang" placeholder="%i18n:@auto%"> + <optgroup label="%i18n:@recommended%"> + <option value="">%i18n:@auto%</option> + </optgroup> - <div> - <div class="md-body-2">%i18n:@post-style%</div> + <optgroup label="%i18n:@specify-language%"> + <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> + </optgroup> + </ui-select> + <span>%fa:info-circle% %i18n:@lang-tip%</span> + </ui-card> - <md-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</md-radio> - <md-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</md-radio> - </div> - </md-card-content> - </md-card> + <ui-card> + <div slot="title">%fa:B twitter% %i18n:@twitter%</div> - <md-card> - <md-card-header> - <div class="md-title">%fa:cog% %i18n:@behavior%</div> - </md-card-header> + <p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p> + <p> + <a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a> + <span v-if="$store.state.i.twitter"> or </span> + <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a> + </p> + </ui-card> - <md-card-content> - <div> - <md-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</md-switch> - </div> + <ui-card> + <div slot="title">%fa:sync-alt% %i18n:@update%</div> - <div> - <md-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</md-switch> - </div> - - <div> - <md-switch v-model="loadRawImages">%i18n:@load-raw-images%</md-switch> - </div> - - <div> - <md-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</md-switch> - </div> - - <div> - <md-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</md-switch> - </div> - </md-card-content> - </md-card> - - <md-card> - <md-card-header> - <div class="md-title">%fa:language% %i18n:@lang%</div> - </md-card-header> - - <md-card-content> - <md-field> - <md-select v-model="lang" placeholder="%i18n:@auto%"> - <md-optgroup label="%i18n:@recommended%"> - <md-option value="">%i18n:@auto%</md-option> - </md-optgroup> - - <md-optgroup label="%i18n:@specify-language%"> - <md-option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</md-option> - </md-optgroup> - </md-select> - </md-field> - <span class="md-helper-text">%fa:info-circle% %i18n:@lang-tip%</span> - </md-card-content> - </md-card> - - <md-card> - <md-card-header> - <div class="md-title">%fa:B twitter% %i18n:@twitter%</div> - </md-card-header> - - <md-card-content> - <p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p> - <p> - <a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a> - <span v-if="$store.state.i.twitter"> or </span> - <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a> - </p> - </md-card-content> - </md-card> - - <md-card> - <md-card-header> - <div class="md-title">%fa:sync-alt% %i18n:@update%</div> - </md-card-header> - - <md-card-content> - <div>%i18n:@version% <i>{{ version }}</i></div> - <template v-if="latestVersion !== undefined"> - <div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div> - </template> - <md-button class="md-raised md-primary" @click="checkForUpdate" :disabled="checkingForUpdate"> - <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template> - <template v-else>%i18n:@check-for-updates%</template> - </md-button> - </md-card-content> - </md-card> + <div>%i18n:@version% <i>{{ version }}</i></div> + <template v-if="latestVersion !== undefined"> + <div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div> + </template> + <ui-button @click="checkForUpdate" :disabled="checkingForUpdate"> + <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template> + <template v-else>%i18n:@check-for-updates%</template> + </ui-button> + </ui-card> </div> - <p><small>ver {{ version }} ({{ codename }})</small></p> + + <footer> + <small>ver {{ version }} ({{ codename }})</small> + </footer> </main> </mk-ui> </template> @@ -267,20 +219,22 @@ export default Vue.extend({ <style lang="stylus" scoped> root(isDark) - padding 0 16px margin 0 auto max-width 500px width 100% - > div - > * - margin-bottom 16px - - > p - display block - margin 24px + > .signin-as + margin 16px + padding 16px text-align center - color isDark ? #cad2da : #a2a9b1 + color isDark ? #49ab63 : #2c662d + background isDark ? #273c34 : #fcfff5 + 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) + + > footer + margin 16px + text-align center + color isDark ? #c9d2e0 : #888 main[data-darkmode] root(true) diff --git a/src/client/app/mobile/views/pages/settings/settings.profile.vue b/src/client/app/mobile/views/pages/settings/settings.profile.vue index f3444eb1f0..da97cbebd7 100644 --- a/src/client/app/mobile/views/pages/settings/settings.profile.vue +++ b/src/client/app/mobile/views/pages/settings/settings.profile.vue @@ -1,62 +1,49 @@ <template> - <md-card> - <md-card-header> - <div class="md-title">%fa:pencil-alt% %i18n:@title%</div> - </md-card-header> +<ui-card> + <div slot="title">%fa:user% %i18n:@title%</div> - <md-card-content> - <md-field> - <label>%i18n:@name%</label> - <md-input v-model="name" :disabled="saving" md-counter="30"/> - </md-field> + <ui-form :disabled="saving"> + <ui-input v-model="name" :max="30"> + <span>%i18n:@name%</span> + </ui-input> - <md-field> - <label>%i18n:@account%</label> - <span class="md-prefix">@</span> - <md-input v-model="username" readonly></md-input> - <span class="md-suffix">@{{ host }}</span> - </md-field> + <ui-input v-model="username" readonly> + <span>%i18n:@account%</span> + <span slot="prefix">@</span> + <span slot="suffix">@{{ host }}</span> + </ui-input> - <md-field> - <md-icon>%fa:map-marker-alt%</md-icon> - <label>%i18n:@location%</label> - <md-input v-model="location" :disabled="saving"/> - </md-field> + <ui-input v-model="location"> + <span>%i18n:@location%</span> + <span slot="prefix">%fa:map-marker-alt%</span> + </ui-input> - <md-field> - <md-icon>%fa:birthday-cake%</md-icon> - <label>%i18n:@birthday%</label> - <md-input type="date" v-model="birthday" :disabled="saving"/> - </md-field> + <ui-input v-model="birthday" type="date"> + <span>%i18n:@birthday%</span> + <span slot="prefix">%fa:birthday-cake%</span> + </ui-input> - <md-field> - <label>%i18n:@description%</label> - <md-textarea v-model="description" :disabled="saving" md-counter="500"/> - </md-field> + <ui-textarea v-model="description" :max="500"> + <span>%i18n:@description%</span> + </ui-textarea> - <md-field> - <label>%i18n:@avatar%</label> - <md-file @md-change="onAvatarChange"/> - </md-field> + <ui-input type="file" @change="onAvatarChange"> + <span>%i18n:@avatar%</span> + <span slot="icon">%fa:image%</span> + <span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span> + </ui-input> - <md-field> - <label>%i18n:@banner%</label> - <md-file @md-change="onBannerChange"/> - </md-field> + <ui-input type="file" @change="onBannerChange"> + <span>%i18n:@banner%</span> + <span slot="icon">%fa:image%</span> + <span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span> + </ui-input> - <md-dialog-alert - :md-active.sync="uploading" - md-content="%18n:!@uploading%"/> + <ui-switch v-model="isCat">%i18n:@is-cat%</ui-switch> - <div> - <md-switch v-model="isCat">%i18n:@is-cat%</md-switch> - </div> - </md-card-content> - - <md-card-actions> - <md-button class="md-primary" :disabled="saving" @click="save">%i18n:@save%</md-button> - </md-card-actions> - </md-card> + <ui-button @click="save">%i18n:@save%</ui-button> + </ui-form> +</ui-card> </template> <script lang="ts"> @@ -77,7 +64,8 @@ export default Vue.extend({ isBot: false, isCat: false, saving: false, - uploading: false + avatarUploading: false, + bannerUploading: false }; }, @@ -95,7 +83,7 @@ export default Vue.extend({ methods: { onAvatarChange([file]) { - this.uploading = true; + this.avatarUploading = true; const data = new FormData(); data.append('file', file); @@ -108,16 +96,16 @@ export default Vue.extend({ .then(response => response.json()) .then(f => { this.avatarId = f.id; - this.uploading = false; + this.avatarUploading = false; }) .catch(e => { - this.uploading = false; + this.avatarUploading = false; alert('%18n:!@upload-failed%'); }); }, onBannerChange([file]) { - this.uploading = true; + this.bannerUploading = true; const data = new FormData(); data.append('file', file); @@ -130,10 +118,10 @@ export default Vue.extend({ .then(response => response.json()) .then(f => { this.bannerId = f.id; - this.uploading = false; + this.bannerUploading = false; }) .catch(e => { - this.uploading = false; + this.bannerUploading = false; alert('%18n:!@upload-failed%'); }); }, diff --git a/src/client/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue index b8245beb00..47384e2b3c 100644 --- a/src/client/app/mobile/views/pages/signup.vue +++ b/src/client/app/mobile/views/pages/signup.vue @@ -1,57 +1,26 @@ <template> <div class="signup"> <h1>Misskeyをはじめる</h1> - <p>いつでも、どこからでもMisskeyを利用できます。もちろん、無料です。</p> - <div class="form"> - <p>新規登録</p> - <div> - <mk-signup/> - </div> - </div> + <mk-signup/> </div> </template> <script lang="ts"> import Vue from 'vue'; -export default Vue.extend({ - mounted() { - document.documentElement.style.background = '#293946'; - } -}); +export default Vue.extend({}); </script> <style lang="stylus" scoped> .signup - padding 16px + padding 32px margin 0 auto max-width 500px h1 margin 0 - padding 8px + padding 8px 0 0 0 font-size 1.5em - font-weight normal - color #c3c6ca - - & + p - margin 0 0 16px 0 - padding 0 8px 0 8px - color #949fa9 - - .form - background #fff - border solid 1px rgba(#000, 0.2) - border-radius 8px - overflow hidden - - > p - margin 0 - padding 12px 20px - color #555 - background #f5f5f5 - border-bottom solid 1px #ddd - - > div - padding 16px + font-weight bold + color #444 </style> diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue index 64cfa5a46c..3bf2a0af9f 100644 --- a/src/client/app/mobile/views/pages/welcome.vue +++ b/src/client/app/mobile/views/pages/welcome.vue @@ -1,29 +1,31 @@ <template> <div class="welcome"> <div> - <h1><b>Misskey</b>へようこそ</h1> - <p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p> - <div class="form"> - <p>%fa:lock% ログイン</p> + <img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="Misskey"> + <p class="host">{{ host }}</p> + <div class="about"> + <h2>{{ name || 'unidentified' }}</h2> + <p v-html="description || '%i18n:common.about%'"></p> + <router-link class="signup" to="/signup">新規登録</router-link> + </div> + <div class="login"> + <form @submit.prevent="onSubmit"> + <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" autofocus required @change="onUsernameChange"> + <span>ユーザー名</span> + <span slot="prefix">@</span> + <span slot="suffix">@{{ host }}</span> + </ui-input> + <ui-input v-model="password" type="password" required> + <span>パスワード</span> + <span slot="prefix">%fa:lock%</span> + </ui-input> + <ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/> + <ui-button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</ui-button> + </form> <div> - <form @submit.prevent="onSubmit"> - <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/> - <input v-model="password" type="password" placeholder="パスワード" required/> - <input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/> - <button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button> - </form> - <div> - <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> - </div> + <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> </div> </div> - <div class="tl"> - <p>%fa:comments R% タイムラインを見てみる</p> - <mk-welcome-timeline/> - </div> - <div class="users"> - <mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/> - </div> <footer> <small>{{ copyright }}</small> </footer> @@ -33,7 +35,7 @@ <script lang="ts"> import Vue from 'vue'; -import { apiUrl, copyright } from '../../../config'; +import { apiUrl, copyright, host, name, description } from '../../../config'; export default Vue.extend({ data() { @@ -45,7 +47,10 @@ export default Vue.extend({ token: '', apiUrl, copyright, - users: [] + users: [], + host, + name, + description }; }, mounted() { @@ -84,112 +89,74 @@ export default Vue.extend({ <style lang="stylus" scoped> .welcome - background linear-gradient(to bottom, #1e1d65, #bd6659) + text-align center + //background #fff > div - padding 16px + padding 32px margin 0 auto max-width 500px - h1 - margin 0 - padding 8px - font-size 1.5em - font-weight normal - color #cacac3 + > img + display block + max-width 200px + margin 0 auto - & + p - margin 0 0 16px 0 - padding 0 8px 0 8px - color #949fa9 + > .host + display block + text-align center + padding 6px 12px + line-height 32px + font-weight bold + color #333 + background rgba(#000, 0.035) + border-radius 6px - .form - margin-bottom 16px + > .about + margin-top 16px + padding 16px + color #555 background #fff - border solid 1px rgba(#000, 0.2) - border-radius 8px - overflow hidden + border-radius 6px + + > h2 + margin 0 > p - margin 0 - padding 12px 20px - color #555 - background #f5f5f5 - border-bottom solid 1px #ddd + margin 8px - > div + > .signup + font-weight bold - > form - padding 16px - border-bottom solid 1px #ddd + > .login + margin 16px 0 - input - display block - padding 12px - margin 0 0 16px 0 - width 100% - font-size 1em - color rgba(#000, 0.7) - background #fff - outline none - border solid 1px #ddd - border-radius 4px + > form - button - display block - width 100% - padding 10px - margin 0 - color #333 - font-size 1em - text-align center - text-decoration none - text-shadow 0 1px 0 rgba(255, 255, 255, 0.9) - background-image linear-gradient(#fafafa, #eaeaea) - border 1px solid #ddd - border-bottom-color #cecece - border-radius 4px - - &:active - background-color #767676 - background-image none - border-color #444 - box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2) - - > div - padding 16px + button + display block + width 100% + padding 10px + margin 0 + color #333 + font-size 1em text-align center + text-decoration none + text-shadow 0 1px 0 rgba(255, 255, 255, 0.9) + background-image linear-gradient(#fafafa, #eaeaea) + border 1px solid #ddd + border-bottom-color #cecece + border-radius 4px - > .tl - background #fff - border solid 1px rgba(#000, 0.2) - border-radius 8px - overflow hidden - - > p - margin 0 - padding 12px 20px - color #555 - background #f5f5f5 - border-bottom solid 1px #ddd - - > .mk-welcome-timeline - max-height 300px - overflow auto - - > .users - margin 12px 0 0 0 - - > * - display inline-block - margin 4px - width 38px - height 38px - border-radius 6px + &:active + background-color #767676 + background-image none + border-color #444 + box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2) > footer text-align center - color #fff + color #444 > small display block diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue index beae1ffa36..a94f7e94b8 100644 --- a/src/client/app/mobile/views/widgets/profile.vue +++ b/src/client/app/mobile/views/widgets/profile.vue @@ -56,7 +56,7 @@ export default define({ left 92px margin 0 line-height 100px - color #fff !important // !important is for md + color #fff font-weight bold text-shadow 0 0 8px rgba(#000, 0.5) diff --git a/src/client/md.scss b/src/client/md.scss deleted file mode 100644 index 8368365885..0000000000 --- a/src/client/md.scss +++ /dev/null @@ -1,13 +0,0 @@ -/* SEE: https://vuematerial.io/themes/configuration */ - -@import '../const.json'; - -@import "~vue-material/dist/theme/engine"; - -@include md-register-theme("default", ( - primary: $themeColor, - accent: $themeColor -)); - -@import "~vue-material/dist/components/MdButton/theme"; -@import "~vue-material/dist/components/MdField/theme"; diff --git a/src/config/types.ts b/src/config/types.ts index 910c03c2c1..d557f2afde 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -15,6 +15,8 @@ export type Source = { */ url: string; }; + name?: string; + description?: string; url: string; port: number; https?: { [x: string]: string }; diff --git a/webpack.config.ts b/webpack.config.ts index 67fb929449..8376cd9c40 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -79,6 +79,8 @@ const consts = { _DEV_URL_: config.dev_url, _LANG_: '%lang%', _LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]), + _NAME_: config.name, + _DESCRIPTION_: config.description, _HOST_: config.host, _HOSTNAME_: config.hostname, _URL_: config.url,