├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── assets ├── application.js ├── boot.js ├── css │ ├── styles.css │ └── styles.min.css ├── devtools.js ├── media │ ├── icon.svg │ └── logo.svg └── worker.js ├── changelog.md ├── client.js ├── components ├── devtools.js ├── examples │ ├── caesar.riot │ ├── chat.riot │ ├── example.riot │ ├── random.riot │ ├── routes │ │ └── routes.riot │ └── todo │ │ ├── todo-list.riot │ │ └── todo-list.style.js ├── gist.riot ├── server.js ├── store.js ├── validators │ ├── login.validate.js │ └── register.validate.js ├── verifier.js └── webworker │ ├── boot.js │ └── index.js ├── config ├── browser.js ├── environ.env └── server.js ├── gulpfile.js ├── index.js ├── package-lock.json ├── package.json ├── pages ├── errors │ ├── 400.riot │ └── 404.riot ├── examples │ ├── chat.riot │ ├── forms.riot │ ├── index.riot │ ├── random.riot │ └── todo.riot ├── index.js ├── index.riot ├── layout │ └── base.ejs ├── login.riot ├── profile.riot └── register.riot ├── plugins.js ├── readme.md ├── services ├── examples │ ├── caesar.js │ ├── chat.js │ ├── form.js │ ├── random.js │ └── todos.js ├── index.js └── users.js ├── specs ├── e2e │ └── loadtest.js ├── render.js └── render │ ├── delay.js │ ├── page.riot │ ├── test.riot │ └── with-jss.riot └── styles.scss /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-detectable=false 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at arch.nesterov@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | out/ 3 | .build/ 4 | .idea/ 5 | .DS_Store 6 | node_modules/ 7 | yarn.lock 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.*" 4 | 5 | branches: 6 | only: 7 | - master 8 | - develop 9 | 10 | notifications: 11 | email: false 12 | 13 | sudo: false 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.jss": "javascript" 4 | }, 5 | "search.exclude": { 6 | "**/node_modules": false 7 | } 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anton Nesterov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/boot.js: -------------------------------------------------------------------------------- 1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i.column{padding-right:0;padding-left:0}.columns.col-oneline{overflow-x:auto;flex-wrap:nowrap}.column{max-width:100%;padding-right:.4rem;padding-left:.4rem;flex:1}.column.col-1,.column.col-10,.column.col-11,.column.col-12,.column.col-2,.column.col-3,.column.col-4,.column.col-5,.column.col-6,.column.col-7,.column.col-8,.column.col-9,.column.col-auto{flex:none}.col-12{width:100%}.col-11{width:91.66666667%}.col-10{width:83.33333333%}.col-9{width:75%}.col-8{width:66.66666667%}.col-7{width:58.33333333%}.col-6{width:50%}.col-5{width:41.66666667%}.col-4{width:33.33333333%}.col-3{width:25%}.col-2{width:16.66666667%}.col-1{width:8.33333333%}.col-auto{width:auto;max-width:none;flex:0 0 auto}.col-mx-auto{margin-right:auto;margin-left:auto}.col-ml-auto{margin-left:auto}.col-mr-auto{margin-right:auto}@media (max-width:1280px){.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{flex:none}.col-xl-12{width:100%}.col-xl-11{width:91.66666667%}.col-xl-10{width:83.33333333%}.col-xl-9{width:75%}.col-xl-8{width:66.66666667%}.col-xl-7{width:58.33333333%}.col-xl-6{width:50%}.col-xl-5{width:41.66666667%}.col-xl-4{width:33.33333333%}.col-xl-3{width:25%}.col-xl-2{width:16.66666667%}.col-xl-1{width:8.33333333%}.col-xl-auto{width:auto}.hide-xl{display:none!important}.show-xl{display:block!important}}@media (max-width:960px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto{flex:none}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-auto{width:auto}.hide-lg{display:none!important}.show-lg{display:block!important}}@media (max-width:840px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto{flex:none}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-auto{width:auto}.hide-md{display:none!important}.show-md{display:block!important}}@media (max-width:600px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto{flex:none}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-auto{width:auto}.hide-sm{display:none!important}.show-sm{display:block!important}}@media (max-width:480px){.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-auto{flex:none}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-auto{width:auto}.hide-xs{display:none!important}.show-xs{display:block!important}}.hero{display:flex;flex-direction:column;padding-top:4rem;padding-bottom:4rem;justify-content:space-between}.hero.hero-sm{padding-top:2rem;padding-bottom:2rem}.hero.hero-lg{padding-top:8rem;padding-bottom:8rem}.hero .hero-body{padding:.4rem}.navbar{display:flex;align-items:stretch;flex-wrap:wrap;justify-content:space-between}.navbar .navbar-section{display:flex;align-items:center;flex:1 0 0}.navbar .navbar-section:not(:first-child):last-child{justify-content:flex-end}.navbar .navbar-center{display:flex;align-items:center;flex:0 0 auto}.navbar .navbar-brand{font-size:.9rem;text-decoration:none}.accordion input:checked~.accordion-header .icon,.accordion[open] .accordion-header .icon{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.accordion input:checked~.accordion-body,.accordion[open] .accordion-body{max-height:50rem}.accordion .accordion-header{display:block;padding:.2rem .4rem}.accordion .accordion-header .icon{transition:-webkit-transform .25s;transition:transform .25s;transition:transform .25s,-webkit-transform .25s}.accordion .accordion-body{overflow:hidden;max-height:0;margin-bottom:.4rem;transition:max-height .25s}summary.accordion-header::-webkit-details-marker{display:none}.avatar{font-size:.8rem;font-weight:300;line-height:1.25;position:relative;display:inline-block;width:1.6rem;height:1.6rem;margin:0;vertical-align:middle;color:rgba(255,255,255,.85);border-radius:50%;background:#5755d9}.avatar.avatar-xs{font-size:.4rem;width:.8rem;height:.8rem}.avatar.avatar-sm{font-size:.6rem;width:1.2rem;height:1.2rem}.avatar.avatar-lg{font-size:1.2rem;width:2.4rem;height:2.4rem}.avatar.avatar-xl{font-size:1.6rem;width:3.2rem;height:3.2rem}.avatar img{position:relative;z-index:1;width:100%;height:100%;border-radius:50%}.avatar .avatar-icon,.avatar .avatar-presence{position:absolute;z-index:2;right:14.64%;bottom:14.64%;width:50%;height:50%;padding:.1rem;-webkit-transform:translate(50%,50%);transform:translate(50%,50%);background:#fff}.avatar .avatar-presence{width:.5em;height:.5em;border-radius:50%;background:#bcc3ce;box-shadow:0 0 0 .1rem #fff}.avatar .avatar-presence.online{background:#32b643}.avatar .avatar-presence.busy{background:#e85600}.avatar .avatar-presence.away{background:#ffb700}.avatar[data-initial]::before{position:absolute;z-index:1;top:50%;left:50%;content:attr(data-initial);-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);color:currentColor}.badge{position:relative;white-space:nowrap}.badge:not([data-badge])::after,.badge[data-badge]::after{display:inline-block;content:attr(data-badge);-webkit-transform:translate(-.05rem,-.5rem);transform:translate(-.05rem,-.5rem);color:#fff;border-radius:.5rem;background:#5755d9;background-clip:padding-box;box-shadow:0 0 0 .1rem #fff}.badge[data-badge]::after{font-size:.7rem;line-height:1;min-width:.9rem;height:.9rem;padding:.1rem .2rem;text-align:center;white-space:nowrap}.badge:not([data-badge])::after,.badge[data-badge='']::after{width:6px;min-width:6px;height:6px;padding:0}.badge.btn::after{position:absolute;top:0;right:0;-webkit-transform:translate(50%,-50%);transform:translate(50%,-50%)}.badge.avatar::after{position:absolute;z-index:100;top:14.64%;right:14.64%;-webkit-transform:translate(50%,-50%);transform:translate(50%,-50%)}.breadcrumb{margin:.2rem 0;padding:.2rem 0;list-style:none}.breadcrumb .breadcrumb-item{display:inline-block;margin:0;padding:.2rem 0;color:#66758c}.breadcrumb .breadcrumb-item:not(:last-child){margin-right:.2rem}.breadcrumb .breadcrumb-item:not(:last-child) a{color:#66758c}.breadcrumb .breadcrumb-item:not(:first-child)::before{padding-right:.4rem;content:'/';color:#66758c}.bar{display:flex;width:100%;height:.8rem;border-radius:.1rem;background:#eef0f3;flex-wrap:nowrap}.bar.bar-sm{height:.2rem}.bar .bar-item{font-size:.7rem;line-height:.8rem;position:relative;display:block;width:0;height:100%;text-align:center;color:#fff;background:#5755d9;flex-shrink:0}.bar .bar-item:first-child{border-top-left-radius:.1rem;border-bottom-left-radius:.1rem}.bar .bar-item:last-child{border-top-right-radius:.1rem;border-bottom-right-radius:.1rem;flex-shrink:1}.bar-slider{position:relative;height:.1rem;margin:.4rem 0}.bar-slider .bar-item{position:absolute;left:0;padding:0}.bar-slider .bar-item:not(:last-child):first-child{z-index:1;background:#eef0f3}.bar-slider .bar-slider-btn{position:absolute;top:50%;right:0;width:.6rem;height:.6rem;padding:0;-webkit-transform:translate(50%,-50%);transform:translate(50%,-50%);border:0;border-radius:50%;background:#5755d9}.bar-slider .bar-slider-btn:active{box-shadow:0 0 0 .1rem #5755d9}.card{display:flex;flex-direction:column;border:.05rem solid #dadee4;border-radius:.1rem;background:#fff}.card .card-body,.card .card-footer,.card .card-header{padding:.8rem;padding-bottom:0}.card .card-body:last-child,.card .card-footer:last-child,.card .card-header:last-child{padding-bottom:.8rem}.card .card-body{flex:1 1 auto}.card .card-image{padding-top:.8rem}.card .card-image:first-child{padding-top:0}.card .card-image:first-child img{border-top-left-radius:.1rem;border-top-right-radius:.1rem}.card .card-image:last-child img{border-bottom-right-radius:.1rem;border-bottom-left-radius:.1rem}.chip{font-size:90%;line-height:.8rem;display:inline-flex;overflow:hidden;max-width:320px;height:1.2rem;margin:.1rem;padding:.2rem .4rem;vertical-align:middle;white-space:nowrap;text-decoration:none;text-overflow:ellipsis;border-radius:5rem;background:#eef0f3;align-items:center}.chip.active{color:#fff;background:#5755d9}.chip .avatar{margin-right:.2rem;margin-left:-.4rem}.chip .btn-clear{-webkit-transform:scale(.75);transform:scale(.75);border-radius:50%}.dropdown{position:relative;display:inline-block}.dropdown .menu{position:absolute;top:100%;left:0;display:none;overflow-y:auto;max-height:50vh;-webkit-animation:slide-down .15s ease 1;animation:slide-down .15s ease 1}.dropdown.dropdown-right .menu{right:0;left:auto}.dropdown .dropdown-toggle:focus+.menu,.dropdown .menu:hover,.dropdown.active .menu{display:block}.dropdown .btn-group .dropdown-toggle:nth-last-child(2){border-top-right-radius:.1rem;border-bottom-right-radius:.1rem}.empty{padding:3.2rem 1.6rem;text-align:center;color:#66758c;border-radius:.1rem;background:#f7f8f9}.empty .empty-icon{margin-bottom:.8rem}.empty .empty-subtitle,.empty .empty-title{margin:.4rem auto}.empty .empty-action{margin-top:.8rem}.menu{z-index:300;min-width:180px;margin:0;padding:.4rem;list-style:none;-webkit-transform:translateY(.2rem);transform:translateY(.2rem);border-radius:.1rem;background:#fff;box-shadow:0 .05rem .2rem rgba(48,55,66,.3)}.menu.menu-nav{background:0 0;box-shadow:none}.menu .menu-item{position:relative;margin-top:0;padding:0 .4rem;text-decoration:none}.menu .menu-item>a{display:block;margin:0 -.4rem;padding:.2rem .4rem;text-decoration:none;color:inherit;border-radius:.1rem}.menu .menu-item>a:focus,.menu .menu-item>a:hover{color:#5755d9;background:#f1f1fc}.menu .menu-item>a.active,.menu .menu-item>a:active{color:#5755d9;background:#f1f1fc}.menu .menu-item .form-checkbox,.menu .menu-item .form-radio,.menu .menu-item .form-switch{margin:.1rem 0}.menu .menu-item+.menu-item{margin-top:.2rem}.menu .menu-badge{position:absolute;top:0;right:0;display:flex;height:100%;align-items:center}.menu .menu-badge .label{margin-right:.4rem}.modal{position:fixed;top:0;right:0;bottom:0;left:0;display:none;overflow:hidden;padding:.4rem;opacity:0;align-items:center;justify-content:center}.modal.active,.modal:target{z-index:400;display:flex;opacity:1}.modal.active .modal-overlay,.modal:target .modal-overlay{position:absolute;top:0;right:0;bottom:0;left:0;display:block;cursor:default;background:rgba(247,248,249,.75)}.modal.active .modal-container,.modal:target .modal-container{z-index:1;-webkit-animation:slide-down .2s ease 1;animation:slide-down .2s ease 1}.modal.modal-sm .modal-container{max-width:320px;padding:0 .4rem}.modal.modal-lg .modal-overlay{background:#fff}.modal.modal-lg .modal-container{max-width:960px;box-shadow:none}.modal-container{display:flex;flex-direction:column;width:100%;max-width:640px;max-height:75vh;padding:0 .8rem;border-radius:.1rem;background:#fff;box-shadow:0 .2rem .5rem rgba(48,55,66,.3)}.modal-container.modal-fullheight{max-height:100vh}.modal-container .modal-header{padding:.8rem;color:#303742}.modal-container .modal-body{position:relative;overflow-y:auto;padding:.8rem}.modal-container .modal-footer{padding:.8rem;text-align:right}.nav{display:flex;flex-direction:column;margin:.2rem 0;list-style:none}.nav .nav-item a{padding:.2rem .4rem;text-decoration:none;color:#66758c}.nav .nav-item a:focus,.nav .nav-item a:hover{color:#5755d9}.nav .nav-item.active>a{font-weight:700;color:#505c6e}.nav .nav-item.active>a:focus,.nav .nav-item.active>a:hover{color:#5755d9}.nav .nav{margin-bottom:.4rem;margin-left:.8rem}.pagination{display:flex;margin:.2rem 0;padding:.2rem 0;list-style:none}.pagination .page-item{margin:.2rem .05rem}.pagination .page-item span{display:inline-block;padding:.2rem .2rem}.pagination .page-item a{display:inline-block;padding:.2rem .4rem;text-decoration:none;border-radius:.1rem}.pagination .page-item a:focus,.pagination .page-item a:hover{color:#5755d9}.pagination .page-item.disabled a{cursor:default;pointer-events:none;opacity:.5}.pagination .page-item.active a{color:#fff;background:#5755d9}.pagination .page-item.page-next,.pagination .page-item.page-prev{flex:1 0 50%}.pagination .page-item.page-next{text-align:right}.pagination .page-item .page-item-title{margin:0}.pagination .page-item .page-item-subtitle{margin:0;opacity:.5}.panel{display:flex;flex-direction:column;border:.05rem solid #dadee4;border-radius:.1rem}.panel .panel-footer,.panel .panel-header{padding:.8rem;flex:0 0 auto}.panel .panel-nav{flex:0 0 auto}.panel .panel-body{overflow-y:auto;padding:0 .8rem;flex:1 1 auto}.popover{position:relative;display:inline-block}.popover .popover-container{position:absolute;z-index:300;top:0;left:50%;width:320px;padding:.4rem;transition:-webkit-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s;-webkit-transform:translate(-50%,-50%) scale(0);transform:translate(-50%,-50%) scale(0);opacity:0}.popover :focus+.popover-container,.popover:hover .popover-container{display:block;-webkit-transform:translate(-50%,-100%) scale(1);transform:translate(-50%,-100%) scale(1);opacity:1}.popover.popover-right .popover-container{top:50%;left:100%}.popover.popover-right :focus+.popover-container,.popover.popover-right:hover .popover-container{-webkit-transform:translate(0,-50%) scale(1);transform:translate(0,-50%) scale(1)}.popover.popover-bottom .popover-container{top:100%;left:50%}.popover.popover-bottom :focus+.popover-container,.popover.popover-bottom:hover .popover-container{-webkit-transform:translate(-50%,0) scale(1);transform:translate(-50%,0) scale(1)}.popover.popover-left .popover-container{top:50%;left:0}.popover.popover-left :focus+.popover-container,.popover.popover-left:hover .popover-container{-webkit-transform:translate(-100%,-50%) scale(1);transform:translate(-100%,-50%) scale(1)}.popover .card{border:0;box-shadow:0 .2rem .5rem rgba(48,55,66,.3)}.step{display:flex;width:100%;margin:.2rem 0;list-style:none;flex-wrap:nowrap}.step .step-item{position:relative;min-height:1rem;margin-top:0;text-align:center;flex:1 1 0}.step .step-item:not(:first-child)::before{position:absolute;top:9px;left:-50%;width:100%;height:2px;content:'';background:#5755d9}.step .step-item a{display:inline-block;padding:20px 10px 0;text-decoration:none;color:#5755d9}.step .step-item a::before{position:absolute;z-index:1;top:.2rem;left:50%;display:block;width:.6rem;height:.6rem;content:'';-webkit-transform:translateX(-50%);transform:translateX(-50%);border:.1rem solid #fff;border-radius:50%;background:#5755d9}.step .step-item.active a::before{border:.1rem solid #5755d9;background:#fff}.step .step-item.active~.step-item::before{background:#dadee4}.step .step-item.active~.step-item a{color:#bcc3ce}.step .step-item.active~.step-item a::before{background:#dadee4}.tab{display:flex;margin:.2rem 0 .15rem 0;list-style:none;border-bottom:.05rem solid #dadee4;align-items:center;flex-wrap:wrap}.tab .tab-item{margin-top:0}.tab .tab-item a{display:block;margin:0 .4rem 0 0;padding:.4rem .2rem .3rem .2rem;text-decoration:none;color:inherit;border-bottom:.1rem solid transparent}.tab .tab-item a:focus,.tab .tab-item a:hover{color:#5755d9}.tab .tab-item a.active,.tab .tab-item.active a{color:#5755d9;border-bottom-color:#5755d9}.tab .tab-item.tab-action{text-align:right;flex:1 0 auto}.tab .tab-item .btn-clear{margin-top:-.2rem}.tab.tab-block .tab-item{text-align:center;flex:1 0 0}.tab.tab-block .tab-item a{margin:0}.tab.tab-block .tab-item .badge[data-badge]::after{position:absolute;top:.1rem;right:.1rem;-webkit-transform:translate(0,0);transform:translate(0,0)}.tab:not(.tab-block) .badge{padding-right:0}.tile{display:flex;align-content:space-between;align-items:flex-start}.tile .tile-action,.tile .tile-icon{flex:0 0 auto}.tile .tile-content{flex:1 1 auto}.tile .tile-content:not(:first-child){padding-left:.4rem}.tile .tile-content:not(:last-child){padding-right:.4rem}.tile .tile-subtitle,.tile .tile-title{line-height:1.2rem}.tile.tile-centered{align-items:center}.tile.tile-centered .tile-content{overflow:hidden}.tile.tile-centered .tile-subtitle,.tile.tile-centered .tile-title{overflow:hidden;margin-bottom:0;white-space:nowrap;text-overflow:ellipsis}.toast{display:block;width:100%;padding:.4rem;color:#fff;border:.05rem solid #303742;border-color:#303742;border-radius:.1rem;background:rgba(48,55,66,.95)}.toast.toast-primary{border-color:#5755d9;background:rgba(87,85,217,.95)}.toast.toast-success{border-color:#32b643;background:rgba(50,182,67,.95)}.toast.toast-warning{border-color:#ffb700;background:rgba(255,183,0,.95)}.toast.toast-error{border-color:#e85600;background:rgba(232,86,0,.95)}.toast a{text-decoration:underline;color:#fff}.toast a.active,.toast a:active,.toast a:focus,.toast a:hover{opacity:.75}.toast .btn-clear{margin:.1rem}.toast p:last-child{margin-bottom:0}.tooltip{position:relative}.tooltip::after{font-size:.7rem;position:absolute;z-index:300;bottom:100%;left:50%;display:block;overflow:hidden;max-width:320px;padding:.2rem .4rem;content:attr(data-tooltip);transition:opacity .2s,-webkit-transform .2s;transition:opacity .2s,transform .2s;transition:opacity .2s,transform .2s,-webkit-transform .2s;-webkit-transform:translate(-50%,.4rem);transform:translate(-50%,.4rem);white-space:pre;text-overflow:ellipsis;pointer-events:none;opacity:0;color:#fff;border-radius:.1rem;background:rgba(48,55,66,.95)}.tooltip:focus::after,.tooltip:hover::after{-webkit-transform:translate(-50%,-.2rem);transform:translate(-50%,-.2rem);opacity:1}.tooltip.disabled,.tooltip[disabled]{pointer-events:auto}.tooltip.tooltip-right::after{bottom:50%;left:100%;-webkit-transform:translate(-.2rem,50%);transform:translate(-.2rem,50%)}.tooltip.tooltip-right:focus::after,.tooltip.tooltip-right:hover::after{-webkit-transform:translate(.2rem,50%);transform:translate(.2rem,50%)}.tooltip.tooltip-bottom::after{top:100%;bottom:auto;-webkit-transform:translate(-50%,-.4rem);transform:translate(-50%,-.4rem)}.tooltip.tooltip-bottom:focus::after,.tooltip.tooltip-bottom:hover::after{-webkit-transform:translate(-50%,.2rem);transform:translate(-50%,.2rem)}.tooltip.tooltip-left::after{right:100%;bottom:50%;left:auto;-webkit-transform:translate(.4rem,50%);transform:translate(.4rem,50%)}.tooltip.tooltip-left:focus::after,.tooltip.tooltip-left:hover::after{-webkit-transform:translate(-.2rem,50%);transform:translate(-.2rem,50%)}@-webkit-keyframes loading{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes loading{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes slide-down{0%{-webkit-transform:translateY(-1.6rem);transform:translateY(-1.6rem);opacity:0}100%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes slide-down{0%{-webkit-transform:translateY(-1.6rem);transform:translateY(-1.6rem);opacity:0}100%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}.text-primary{color:#5755d9!important}a.text-primary:focus,a.text-primary:hover{color:#4240d4}a.text-primary:visited{color:#6c6ade}.text-secondary{color:#e5e5f9!important}a.text-secondary:focus,a.text-secondary:hover{color:#d1d0f4}a.text-secondary:visited{color:#fafafe}.text-gray{color:#bcc3ce!important}a.text-gray:focus,a.text-gray:hover{color:#adb6c4}a.text-gray:visited{color:#cbd0d9}.text-light{color:#fff!important}a.text-light:focus,a.text-light:hover{color:#f2f2f2}a.text-light:visited{color:#fff}.text-dark{color:#3b4351!important}a.text-dark:focus,a.text-dark:hover{color:#303742}a.text-dark:visited{color:#455060}.text-success{color:#32b643!important}a.text-success:focus,a.text-success:hover{color:#2da23c}a.text-success:visited{color:#39c94b}.text-warning{color:#ffb700!important}a.text-warning:focus,a.text-warning:hover{color:#e6a500}a.text-warning:visited{color:#ffbe1a}.text-error{color:#e85600!important}a.text-error:focus,a.text-error:hover{color:#cf4d00}a.text-error:visited{color:#ff6003}.bg-primary{color:#fff;background:#5755d9!important}.bg-secondary{background:#f1f1fc!important}.bg-dark{color:#fff;background:#303742!important}.bg-gray{background:#f7f8f9!important}.bg-success{color:#fff;background:#32b643!important}.bg-warning{color:#fff;background:#ffb700!important}.bg-error{color:#fff;background:#e85600!important}.c-hand{cursor:pointer}.c-move{cursor:move}.c-zoom-in{cursor:zoom-in}.c-zoom-out{cursor:zoom-out}.c-not-allowed{cursor:not-allowed}.c-auto{cursor:auto}.d-block{display:block}.d-inline{display:inline}.d-inline-block{display:inline-block}.d-flex{display:flex}.d-inline-flex{display:inline-flex}.d-hide,.d-none{display:none!important}.d-visible{visibility:visible}.d-invisible{visibility:hidden}.text-hide{font-size:0;line-height:0;color:transparent;border:0;background:0 0;text-shadow:none}.text-assistive{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border:0}.divider,.divider-vert{position:relative;display:block}.divider-vert[data-content]::after,.divider[data-content]::after{font-size:.7rem;display:inline-block;padding:0 .4rem;content:attr(data-content);-webkit-transform:translateY(-.65rem);transform:translateY(-.65rem);color:#bcc3ce;background:#fff}.divider{height:.05rem;margin:.4rem 0;border-top:.05rem solid #f1f3f5}.divider[data-content]{margin:.8rem 0}.divider-vert{display:block;padding:.8rem}.divider-vert::before{position:absolute;top:.4rem;bottom:.4rem;left:50%;display:block;content:'';-webkit-transform:translateX(-50%);transform:translateX(-50%);border-left:.05rem solid #dadee4}.divider-vert[data-content]::after{position:absolute;top:50%;left:50%;padding:.2rem 0;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.loading{position:relative;min-height:.8rem;pointer-events:none;color:transparent!important}.loading::after{position:absolute;z-index:1;top:50%;left:50%;display:block;width:.8rem;height:.8rem;margin-top:-.4rem;margin-left:-.4rem;content:'';-webkit-animation:loading .5s infinite linear;animation:loading .5s infinite linear;border:.1rem solid #5755d9;border-top-color:transparent;border-right-color:transparent;border-radius:50%}.loading.loading-lg{min-height:2rem}.loading.loading-lg::after{width:1.6rem;height:1.6rem;margin-top:-.8rem;margin-left:-.8rem}.clearfix::after{display:table;clear:both;content:''}.float-left{float:left!important}.float-right{float:right!important}.p-relative{position:relative!important}.p-absolute{position:absolute!important}.p-fixed{position:fixed!important}.p-sticky{position:-webkit-sticky!important;position:sticky!important}.p-centered{display:block;float:none;margin-right:auto;margin-left:auto}.flex-centered{display:flex;align-items:center;justify-content:center}.m-0{margin:0!important}.mb-0{margin-bottom:0!important}.ml-0{margin-left:0!important}.mr-0{margin-right:0!important}.mt-0{margin-top:0!important}.mx-0{margin-right:0!important;margin-left:0!important}.my-0{margin-top:0!important;margin-bottom:0!important}.m-1{margin:.2rem!important}.mb-1{margin-bottom:.2rem!important}.ml-1{margin-left:.2rem!important}.mr-1{margin-right:.2rem!important}.mt-1{margin-top:.2rem!important}.mx-1{margin-right:.2rem!important;margin-left:.2rem!important}.my-1{margin-top:.2rem!important;margin-bottom:.2rem!important}.m-2{margin:.4rem!important}.mb-2{margin-bottom:.4rem!important}.ml-2{margin-left:.4rem!important}.mr-2{margin-right:.4rem!important}.mt-2{margin-top:.4rem!important}.mx-2{margin-right:.4rem!important;margin-left:.4rem!important}.my-2{margin-top:.4rem!important;margin-bottom:.4rem!important}.p-0{padding:0!important}.pb-0{padding-bottom:0!important}.pl-0{padding-left:0!important}.pr-0{padding-right:0!important}.pt-0{padding-top:0!important}.px-0{padding-right:0!important;padding-left:0!important}.py-0{padding-top:0!important;padding-bottom:0!important}.p-1{padding:.2rem!important}.pb-1{padding-bottom:.2rem!important}.pl-1{padding-left:.2rem!important}.pr-1{padding-right:.2rem!important}.pt-1{padding-top:.2rem!important}.px-1{padding-right:.2rem!important;padding-left:.2rem!important}.py-1{padding-top:.2rem!important;padding-bottom:.2rem!important}.p-2{padding:.4rem!important}.pb-2{padding-bottom:.4rem!important}.pl-2{padding-left:.4rem!important}.pr-2{padding-right:.4rem!important}.pt-2{padding-top:.4rem!important}.px-2{padding-right:.4rem!important;padding-left:.4rem!important}.py-2{padding-top:.4rem!important;padding-bottom:.4rem!important}.s-rounded{border-radius:.1rem}.s-circle{border-radius:50%}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-normal{font-weight:400}.text-bold{font-weight:700}.text-italic{font-style:italic}.text-large{font-size:1.2em}.text-ellipsis{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.text-clip{overflow:hidden;white-space:nowrap;text-overflow:clip}.text-break{word-wrap:break-word;word-break:break-word;-webkit-hyphens:auto;hyphens:auto;-ms-hyphens:auto}/*! Spectre.css Icons v0.5.8 | MIT License | github.com/picturepan2/spectre */.icon{font-size:inherit;font-style:normal;position:relative;display:inline-block;box-sizing:border-box;width:1em;height:1em;vertical-align:middle;text-indent:-9999px}.icon::after,.icon::before{position:absolute;top:50%;left:50%;display:block;content:'';-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.icon.icon-2x{font-size:1.6rem}.icon.icon-3x{font-size:2.4rem}.icon.icon-4x{font-size:3.2rem}.accordion .icon,.btn .icon,.menu .icon,.toast .icon{vertical-align:-10%}.btn-lg .icon{vertical-align:-15%}.icon-arrow-down::before,.icon-arrow-left::before,.icon-arrow-right::before,.icon-arrow-up::before,.icon-back::before,.icon-downward::before,.icon-forward::before,.icon-upward::before{width:.65em;height:.65em;border:.1rem solid currentColor;border-right:0;border-bottom:0}.icon-arrow-down::before{-webkit-transform:translate(-50%,-75%) rotate(225deg);transform:translate(-50%,-75%) rotate(225deg)}.icon-arrow-left::before{-webkit-transform:translate(-25%,-50%) rotate(-45deg);transform:translate(-25%,-50%) rotate(-45deg)}.icon-arrow-right::before{-webkit-transform:translate(-75%,-50%) rotate(135deg);transform:translate(-75%,-50%) rotate(135deg)}.icon-arrow-up::before{-webkit-transform:translate(-50%,-25%) rotate(45deg);transform:translate(-50%,-25%) rotate(45deg)}.icon-back::after,.icon-forward::after{width:.8em;height:.1rem;background:currentColor}.icon-downward::after,.icon-upward::after{width:.1rem;height:.8em;background:currentColor}.icon-back::after{left:55%}.icon-back::before{-webkit-transform:translate(-50%,-50%) rotate(-45deg);transform:translate(-50%,-50%) rotate(-45deg)}.icon-downward::after{top:45%}.icon-downward::before{-webkit-transform:translate(-50%,-50%) rotate(-135deg);transform:translate(-50%,-50%) rotate(-135deg)}.icon-forward::after{left:45%}.icon-forward::before{-webkit-transform:translate(-50%,-50%) rotate(135deg);transform:translate(-50%,-50%) rotate(135deg)}.icon-upward::after{top:55%}.icon-upward::before{-webkit-transform:translate(-50%,-50%) rotate(45deg);transform:translate(-50%,-50%) rotate(45deg)}.icon-caret::before{width:0;height:0;-webkit-transform:translate(-50%,-25%);transform:translate(-50%,-25%);border-top:.3em solid currentColor;border-right:.3em solid transparent;border-left:.3em solid transparent}.icon-menu::before{width:100%;height:.1rem;background:currentColor;box-shadow:0 -.35em,0 .35em}.icon-apps::before{width:3px;height:3px;background:currentColor;box-shadow:-.35em -.35em,-.35em 0,-.35em .35em,0 -.35em,0 .35em,.35em -.35em,.35em 0,.35em .35em}.icon-resize-horiz::after,.icon-resize-horiz::before,.icon-resize-vert::after,.icon-resize-vert::before{width:.45em;height:.45em;border:.1rem solid currentColor;border-right:0;border-bottom:0}.icon-resize-horiz::before,.icon-resize-vert::before{-webkit-transform:translate(-50%,-90%) rotate(45deg);transform:translate(-50%,-90%) rotate(45deg)}.icon-resize-horiz::after,.icon-resize-vert::after{-webkit-transform:translate(-50%,-10%) rotate(225deg);transform:translate(-50%,-10%) rotate(225deg)}.icon-resize-horiz::before{-webkit-transform:translate(-90%,-50%) rotate(-45deg);transform:translate(-90%,-50%) rotate(-45deg)}.icon-resize-horiz::after{-webkit-transform:translate(-10%,-50%) rotate(135deg);transform:translate(-10%,-50%) rotate(135deg)}.icon-more-horiz::before,.icon-more-vert::before{width:3px;height:3px;border-radius:50%;background:currentColor;box-shadow:-.4em 0,.4em 0}.icon-more-vert::before{box-shadow:0 -.4em,0 .4em}.icon-cross::before,.icon-minus::before,.icon-plus::before{width:100%;height:.1rem;background:currentColor}.icon-cross::after,.icon-plus::after{width:.1rem;height:100%;background:currentColor}.icon-cross::before{width:100%}.icon-cross::after{height:100%}.icon-cross::after,.icon-cross::before{-webkit-transform:translate(-50%,-50%) rotate(45deg);transform:translate(-50%,-50%) rotate(45deg)}.icon-check::before{width:.9em;height:.5em;-webkit-transform:translate(-50%,-75%) rotate(-45deg);transform:translate(-50%,-75%) rotate(-45deg);border:.1rem solid currentColor;border-top:0;border-right:0}.icon-stop{border:.1rem solid currentColor;border-radius:50%}.icon-stop::before{width:1em;height:.1rem;-webkit-transform:translate(-50%,-50%) rotate(45deg);transform:translate(-50%,-50%) rotate(45deg);background:currentColor}.icon-shutdown{border:.1rem solid currentColor;border-top-color:transparent;border-radius:50%}.icon-shutdown::before{top:.1em;width:.1rem;height:.5em;content:'';background:currentColor}.icon-refresh::before{width:1em;height:1em;border:.1rem solid currentColor;border-right-color:transparent;border-radius:50%}.icon-refresh::after{top:20%;left:80%;width:0;height:0;border:.2em solid currentColor;border-top-color:transparent;border-left-color:transparent}.icon-search::before{top:5%;left:5%;width:.75em;height:.75em;-webkit-transform:translate(0,0) rotate(45deg);transform:translate(0,0) rotate(45deg);border:.1rem solid currentColor;border-radius:50%}.icon-search::after{top:80%;left:80%;width:.4em;height:.1rem;-webkit-transform:translate(-50%,-50%) rotate(45deg);transform:translate(-50%,-50%) rotate(45deg);background:currentColor}.icon-edit::before{width:.85em;height:.4em;-webkit-transform:translate(-40%,-60%) rotate(-45deg);transform:translate(-40%,-60%) rotate(-45deg);border:.1rem solid currentColor}.icon-edit::after{top:95%;left:5%;width:0;height:0;-webkit-transform:translate(0,-100%);transform:translate(0,-100%);border:.15em solid currentColor;border-top-color:transparent;border-right-color:transparent}.icon-delete::before{top:60%;width:.75em;height:.75em;border:.1rem solid currentColor;border-top:0;border-bottom-right-radius:.1rem;border-bottom-left-radius:.1rem}.icon-delete::after{top:.05rem;width:.5em;height:.1rem;background:currentColor;box-shadow:-.25em .2em,.25em .2em}.icon-share{border:.1rem solid currentColor;border-top:0;border-right:0;border-radius:.1rem}.icon-share::before{top:.25em;left:100%;width:.4em;height:.4em;-webkit-transform:translate(-125%,-50%) rotate(-45deg);transform:translate(-125%,-50%) rotate(-45deg);border:.1rem solid currentColor;border-top:0;border-left:0}.icon-share::after{width:.6em;height:.5em;border:.1rem solid currentColor;border-right:0;border-bottom:0;border-radius:75% 0}.icon-flag::before{left:15%;width:.1rem;height:1em;background:currentColor}.icon-flag::after{top:35%;left:60%;width:.8em;height:.65em;border:.1rem solid currentColor;border-left:0;border-top-right-radius:.1rem;border-bottom-right-radius:.1rem}.icon-bookmark::before{width:.8em;height:.9em;border:.1rem solid currentColor;border-bottom:0;border-top-left-radius:.1rem;border-top-right-radius:.1rem}.icon-bookmark::after{width:.5em;height:.5em;-webkit-transform:translate(-50%,35%) rotate(-45deg) skew(15deg,15deg);transform:translate(-50%,35%) rotate(-45deg) skew(15deg,15deg);border:.1rem solid currentColor;border-bottom:0;border-left:0;border-radius:.1rem}.icon-download,.icon-upload{border-bottom:.1rem solid currentColor}.icon-download::before,.icon-upload::before{width:.5em;height:.5em;-webkit-transform:translate(-50%,-60%) rotate(-135deg);transform:translate(-50%,-60%) rotate(-135deg);border:.1rem solid currentColor;border-right:0;border-bottom:0}.icon-download::after,.icon-upload::after{top:40%;width:.1rem;height:.6em;background:currentColor}.icon-upload::before{-webkit-transform:translate(-50%,-60%) rotate(45deg);transform:translate(-50%,-60%) rotate(45deg)}.icon-upload::after{top:50%}.icon-copy::before{top:35%;left:40%;width:.8em;height:.8em;border:.1rem solid currentColor;border-right:0;border-bottom:0;border-radius:.1rem}.icon-copy::after{top:60%;left:60%;width:.8em;height:.8em;border:.1rem solid currentColor;border-radius:.1rem}.icon-time{border:.1rem solid currentColor;border-radius:50%}.icon-time::before{width:.1rem;height:.4em;-webkit-transform:translate(-50%,-75%);transform:translate(-50%,-75%);background:currentColor}.icon-time::after{width:.1rem;height:.3em;-webkit-transform:translate(-50%,-75%) rotate(90deg);transform:translate(-50%,-75%) rotate(90deg);-webkit-transform-origin:50% 90%;transform-origin:50% 90%;background:currentColor}.icon-mail::before{width:1em;height:.8em;border:.1rem solid currentColor;border-radius:.1rem}.icon-mail::after{width:.5em;height:.5em;-webkit-transform:translate(-50%,-90%) rotate(-45deg) skew(10deg,10deg);transform:translate(-50%,-90%) rotate(-45deg) skew(10deg,10deg);border:.1rem solid currentColor;border-top:0;border-right:0}.icon-people::before{top:25%;width:.45em;height:.45em;border:.1rem solid currentColor;border-radius:50%}.icon-people::after{top:75%;width:.9em;height:.4em;border:.1rem solid currentColor;border-radius:50% 50% 0 0}.icon-message{border:.1rem solid currentColor;border-right:0;border-bottom:0;border-radius:.1rem}.icon-message::before{top:40%;left:65%;width:.7em;height:.8em;border:.1rem solid currentColor;border-top:0;border-left:0;border-bottom-right-radius:.1rem}.icon-message::after{top:100%;left:10%;width:.1rem;height:.3em;-webkit-transform:translate(0,-90%) rotate(45deg);transform:translate(0,-90%) rotate(45deg);border-radius:.1rem;background:currentColor}.icon-photo{border:.1rem solid currentColor;border-radius:.1rem}.icon-photo::before{top:35%;left:35%;width:.25em;height:.25em;border:.1rem solid currentColor;border-radius:50%}.icon-photo::after{left:60%;width:.5em;height:.5em;-webkit-transform:translate(-50%,25%) rotate(-45deg);transform:translate(-50%,25%) rotate(-45deg);border:.1rem solid currentColor;border-bottom:0;border-left:0}.icon-link::after,.icon-link::before{width:.75em;height:.5em;border:.1rem solid currentColor;border-right:0;border-radius:5em 0 0 5em}.icon-link::before{-webkit-transform:translate(-70%,-45%) rotate(-45deg);transform:translate(-70%,-45%) rotate(-45deg)}.icon-link::after{-webkit-transform:translate(-30%,-55%) rotate(135deg);transform:translate(-30%,-55%) rotate(135deg)}.icon-location::before{width:.8em;height:.8em;-webkit-transform:translate(-50%,-60%) rotate(-45deg);transform:translate(-50%,-60%) rotate(-45deg);border:.1rem solid currentColor;border-radius:50% 50% 50% 0}.icon-location::after{width:.2em;height:.2em;-webkit-transform:translate(-50%,-80%);transform:translate(-50%,-80%);border:.1rem solid currentColor;border-radius:50%}.icon-emoji{border:.1rem solid currentColor;border-radius:50%}.icon-emoji::before{width:.15em;height:.15em;border-radius:50%;box-shadow:-.17em -.1em,.17em -.1em}.icon-emoji::after{width:.5em;height:.5em;-webkit-transform:translate(-50%,-40%) rotate(-135deg);transform:translate(-50%,-40%) rotate(-135deg);border:.1rem solid currentColor;border-right-color:transparent;border-bottom-color:transparent;border-radius:50%}/*! Spectre.css Experimentals v0.5.8 | MIT License | github.com/picturepan2/spectre */.form-autocomplete{position:relative}.form-autocomplete .form-autocomplete-input{display:flex;height:auto;min-height:1.6rem;padding:.1rem;align-content:flex-start;flex-wrap:wrap}.form-autocomplete .form-autocomplete-input.is-focused{border-color:#5755d9;box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.form-autocomplete .form-autocomplete-input .form-input{line-height:.8rem;display:inline-block;width:auto;height:1.2rem;margin:.1rem;border-color:transparent;box-shadow:none;flex:1 0 auto}.form-autocomplete .menu{position:absolute;top:100%;left:0;width:100%}.form-autocomplete.autocomplete-oneline .form-autocomplete-input{overflow-x:auto;flex-wrap:nowrap}.form-autocomplete.autocomplete-oneline .chip{flex:1 0 auto}.calendar{display:block;min-width:280px;border:.05rem solid #dadee4;border-radius:.1rem}.calendar .calendar-nav{font-size:.9rem;display:flex;padding:.4rem;border-top-left-radius:.1rem;border-top-right-radius:.1rem;background:#f7f8f9;align-items:center}.calendar .calendar-body,.calendar .calendar-header{display:flex;padding:.4rem 0;flex-wrap:wrap;justify-content:center}.calendar .calendar-body .calendar-date,.calendar .calendar-header .calendar-date{max-width:14.28%;flex:0 0 14.28%}.calendar .calendar-header{font-size:.7rem;text-align:center;color:#bcc3ce;border-bottom:.05rem solid #dadee4;background:#f7f8f9}.calendar .calendar-body{color:#66758c}.calendar .calendar-date{padding:.2rem;border:0}.calendar .calendar-date .date-item{font-size:.7rem;line-height:1rem;position:relative;width:1.4rem;height:1.4rem;padding:.1rem;cursor:pointer;transition:background .2s,border .2s,box-shadow .2s,color .2s;text-align:center;vertical-align:middle;white-space:nowrap;text-decoration:none;color:#66758c;border:.05rem solid transparent;border-radius:50%;outline:0;background:0 0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.calendar .calendar-date .date-item.date-today{color:#5755d9;border-color:#e5e5f9}.calendar .calendar-date .date-item:focus{box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.calendar .calendar-date .date-item:focus,.calendar .calendar-date .date-item:hover{text-decoration:none;color:#5755d9;border-color:#e5e5f9;background:#fefeff}.calendar .calendar-date .date-item.active,.calendar .calendar-date .date-item:active{color:#fff;border-color:#3634d2;background:#4b48d6}.calendar .calendar-date .date-item.badge::after{position:absolute;top:3px;right:3px;-webkit-transform:translate(50%,-50%);transform:translate(50%,-50%)}.calendar .calendar-date .calendar-event.disabled,.calendar .calendar-date .calendar-event:disabled,.calendar .calendar-date .date-item.disabled,.calendar .calendar-date .date-item:disabled{cursor:default;pointer-events:none;opacity:.25}.calendar .calendar-date.next-month .calendar-event,.calendar .calendar-date.next-month .date-item,.calendar .calendar-date.prev-month .calendar-event,.calendar .calendar-date.prev-month .date-item{opacity:.25}.calendar .calendar-range{position:relative}.calendar .calendar-range::before{position:absolute;top:50%;right:0;left:0;height:1.4rem;content:'';-webkit-transform:translateY(-50%);transform:translateY(-50%);background:#f1f1fc}.calendar .calendar-range.range-start::before{left:50%}.calendar .calendar-range.range-end::before{right:50%}.calendar .calendar-range.range-end .date-item,.calendar .calendar-range.range-start .date-item{color:#fff;border-color:#3634d2;background:#4b48d6}.calendar .calendar-range .date-item{color:#5755d9}.calendar.calendar-lg .calendar-body{padding:0}.calendar.calendar-lg .calendar-body .calendar-date{display:flex;flex-direction:column;height:5.5rem;padding:0;border-right:.05rem solid #dadee4;border-bottom:.05rem solid #dadee4}.calendar.calendar-lg .calendar-body .calendar-date:nth-child(7n){border-right:0}.calendar.calendar-lg .calendar-body .calendar-date:nth-last-child(-n+7){border-bottom:0}.calendar.calendar-lg .date-item{height:1.4rem;margin-top:.2rem;margin-right:.2rem;align-self:flex-end}.calendar.calendar-lg .calendar-range::before{top:19px}.calendar.calendar-lg .calendar-range.range-start::before{left:auto;width:19px}.calendar.calendar-lg .calendar-range.range-end::before{right:19px}.calendar.calendar-lg .calendar-events{line-height:1;overflow-y:auto;padding:.2rem;flex-grow:1}.calendar.calendar-lg .calendar-event{font-size:.7rem;display:block;overflow:hidden;margin:.1rem auto;padding:3px 4px;white-space:nowrap;text-overflow:ellipsis;border-radius:.1rem}.carousel .carousel-locator:nth-of-type(1):checked~.carousel-container .carousel-item:nth-of-type(1),.carousel .carousel-locator:nth-of-type(2):checked~.carousel-container .carousel-item:nth-of-type(2),.carousel .carousel-locator:nth-of-type(3):checked~.carousel-container .carousel-item:nth-of-type(3),.carousel .carousel-locator:nth-of-type(4):checked~.carousel-container .carousel-item:nth-of-type(4),.carousel .carousel-locator:nth-of-type(5):checked~.carousel-container .carousel-item:nth-of-type(5),.carousel .carousel-locator:nth-of-type(6):checked~.carousel-container .carousel-item:nth-of-type(6),.carousel .carousel-locator:nth-of-type(7):checked~.carousel-container .carousel-item:nth-of-type(7),.carousel .carousel-locator:nth-of-type(8):checked~.carousel-container .carousel-item:nth-of-type(8){z-index:100;-webkit-animation:carousel-slidein .75s ease-in-out 1;animation:carousel-slidein .75s ease-in-out 1;opacity:1}.carousel .carousel-locator:nth-of-type(1):checked~.carousel-nav .nav-item:nth-of-type(1),.carousel .carousel-locator:nth-of-type(2):checked~.carousel-nav .nav-item:nth-of-type(2),.carousel .carousel-locator:nth-of-type(3):checked~.carousel-nav .nav-item:nth-of-type(3),.carousel .carousel-locator:nth-of-type(4):checked~.carousel-nav .nav-item:nth-of-type(4),.carousel .carousel-locator:nth-of-type(5):checked~.carousel-nav .nav-item:nth-of-type(5),.carousel .carousel-locator:nth-of-type(6):checked~.carousel-nav .nav-item:nth-of-type(6),.carousel .carousel-locator:nth-of-type(7):checked~.carousel-nav .nav-item:nth-of-type(7),.carousel .carousel-locator:nth-of-type(8):checked~.carousel-nav .nav-item:nth-of-type(8){color:#f7f8f9}.carousel{position:relative;z-index:1;display:block;overflow:hidden;width:100%;background:#f7f8f9;-webkit-overflow-scrolling:touch}.carousel .carousel-container{position:relative;left:0;height:100%}.carousel .carousel-container::before{display:block;padding-bottom:56.25%;content:''}.carousel .carousel-container .carousel-item{position:absolute;top:0;left:0;width:100%;height:100%;margin:0;-webkit-animation:carousel-slideout 1s ease-in-out 1;animation:carousel-slideout 1s ease-in-out 1;opacity:0}.carousel .carousel-container .carousel-item:hover .item-next,.carousel .carousel-container .carousel-item:hover .item-prev{opacity:1}.carousel .carousel-container .item-next,.carousel .carousel-container .item-prev{position:absolute;z-index:100;top:50%;transition:all .4s;-webkit-transform:translateY(-50%);transform:translateY(-50%);opacity:0;color:#f7f8f9;border-color:rgba(247,248,249,.5);background:rgba(247,248,249,.25)}.carousel .carousel-container .item-prev{left:1rem}.carousel .carousel-container .item-next{right:1rem}.carousel .carousel-nav{position:absolute;z-index:100;bottom:.4rem;left:50%;display:flex;width:10rem;-webkit-transform:translateX(-50%);transform:translateX(-50%);justify-content:center}.carousel .carousel-nav .nav-item{position:relative;display:block;max-width:2.5rem;height:1.6rem;margin:.2rem;color:rgba(247,248,249,.5);flex:1 0 auto}.carousel .carousel-nav .nav-item::before{position:absolute;top:.5rem;display:block;width:100%;height:.1rem;content:'';background:currentColor}@-webkit-keyframes carousel-slidein{0%{-webkit-transform:translateX(100%);transform:translateX(100%)}100%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes carousel-slidein{0%{-webkit-transform:translateX(100%);transform:translateX(100%)}100%{-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes carousel-slideout{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-50%);transform:translateX(-50%);opacity:1}}@keyframes carousel-slideout{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-50%);transform:translateX(-50%);opacity:1}}.comparison-slider{position:relative;overflow:hidden;width:100%;height:50vh;-webkit-overflow-scrolling:touch}.comparison-slider .comparison-after,.comparison-slider .comparison-before{position:absolute;top:0;left:0;overflow:hidden;height:100%;margin:0}.comparison-slider .comparison-after img,.comparison-slider .comparison-before img{position:absolute;width:100%;height:100%;-o-object-fit:cover;object-fit:cover;-o-object-position:left center;object-position:left center}.comparison-slider .comparison-before{z-index:1;width:100%}.comparison-slider .comparison-before .comparison-label{right:.8rem}.comparison-slider .comparison-after{z-index:2;min-width:0;max-width:100%}.comparison-slider .comparison-after::before{position:absolute;z-index:1;top:0;right:.8rem;left:0;height:100%;content:'';cursor:default;background:0 0}.comparison-slider .comparison-after::after{position:absolute;top:50%;right:.4rem;width:3px;height:3px;content:'';-webkit-transform:translate(50%,-50%);transform:translate(50%,-50%);color:#fff;border-radius:50%;background:currentColor;box-shadow:0 -5px,0 5px}.comparison-slider .comparison-after .comparison-label{left:.8rem}.comparison-slider .comparison-resizer{position:relative;top:50%;left:0;width:0;min-width:.8rem;max-width:100%;height:.8rem;resize:horizontal;cursor:ew-resize;-webkit-transform:translateY(-50%) scaleY(30);transform:translateY(-50%) scaleY(30);-webkit-animation:first-run 1.5s 1 ease-in-out;animation:first-run 1.5s 1 ease-in-out;opacity:0;outline:0}.comparison-slider .comparison-label{position:absolute;bottom:.8rem;padding:.2rem .4rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:#fff;background:rgba(48,55,66,.5)}@-webkit-keyframes first-run{0%{width:0}25%{width:2.4rem}50%{width:.8rem}75%{width:1.2rem}100%{width:0}}@keyframes first-run{0%{width:0}25%{width:2.4rem}50%{width:.8rem}75%{width:1.2rem}100%{width:0}}.filter .filter-tag#tag-0:checked~.filter-nav .chip[for=tag-0],.filter .filter-tag#tag-1:checked~.filter-nav .chip[for=tag-1],.filter .filter-tag#tag-2:checked~.filter-nav .chip[for=tag-2],.filter .filter-tag#tag-3:checked~.filter-nav .chip[for=tag-3],.filter .filter-tag#tag-4:checked~.filter-nav .chip[for=tag-4],.filter .filter-tag#tag-5:checked~.filter-nav .chip[for=tag-5],.filter .filter-tag#tag-6:checked~.filter-nav .chip[for=tag-6],.filter .filter-tag#tag-7:checked~.filter-nav .chip[for=tag-7],.filter .filter-tag#tag-8:checked~.filter-nav .chip[for=tag-8]{color:#fff;background:#5755d9}.filter .filter-tag#tag-1:checked~.filter-body .filter-item:not([data-tag~=tag-1]),.filter .filter-tag#tag-2:checked~.filter-body .filter-item:not([data-tag~=tag-2]),.filter .filter-tag#tag-3:checked~.filter-body .filter-item:not([data-tag~=tag-3]),.filter .filter-tag#tag-4:checked~.filter-body .filter-item:not([data-tag~=tag-4]),.filter .filter-tag#tag-5:checked~.filter-body .filter-item:not([data-tag~=tag-5]),.filter .filter-tag#tag-6:checked~.filter-body .filter-item:not([data-tag~=tag-6]),.filter .filter-tag#tag-7:checked~.filter-body .filter-item:not([data-tag~=tag-7]),.filter .filter-tag#tag-8:checked~.filter-body .filter-item:not([data-tag~=tag-8]){display:none}.filter .filter-nav{margin:.4rem 0}.filter .filter-body{display:flex;flex-wrap:wrap}.meter{display:block;width:100%;height:.8rem;border:0;border-radius:.1rem;background:#f7f8f9;-webkit-appearance:none;-moz-appearance:none;appearance:none}.meter::-webkit-meter-inner-element{display:block}.meter::-webkit-meter-bar,.meter::-webkit-meter-even-less-good-value,.meter::-webkit-meter-optimum-value,.meter::-webkit-meter-suboptimum-value{border-radius:.1rem}.meter::-webkit-meter-bar{background:#f7f8f9}.meter::-webkit-meter-optimum-value{background:#32b643}.meter::-webkit-meter-suboptimum-value{background:#ffb700}.meter::-webkit-meter-even-less-good-value{background:#e85600}.meter:-moz-meter-optimum,.meter:-moz-meter-sub-optimum,.meter:-moz-meter-sub-sub-optimum,.meter::-moz-meter-bar{border-radius:.1rem}.meter:-moz-meter-optimum::-moz-meter-bar{background:#32b643}.meter:-moz-meter-sub-optimum::-moz-meter-bar{background:#ffb700}.meter:-moz-meter-sub-sub-optimum::-moz-meter-bar{background:#e85600}.off-canvas{position:relative;display:flex;width:100%;height:100%;flex-flow:nowrap}.off-canvas .off-canvas-toggle{position:absolute;z-index:1;top:.4rem;left:.4rem;display:block;transition:none}.off-canvas .off-canvas-sidebar{position:fixed;z-index:200;top:0;bottom:0;left:0;overflow-y:auto;min-width:10rem;transition:-webkit-transform .25s;transition:transform .25s;transition:transform .25s,-webkit-transform .25s;-webkit-transform:translateX(-100%);transform:translateX(-100%);background:#f7f8f9}.off-canvas .off-canvas-content{height:100%;padding:.4rem .4rem .4rem 4rem;flex:1 1 auto}.off-canvas .off-canvas-overlay{position:fixed;top:0;right:0;bottom:0;left:0;display:none;width:100%;height:100%;border-color:transparent;border-radius:0;background:rgba(48,55,66,.1)}.off-canvas .off-canvas-sidebar.active,.off-canvas .off-canvas-sidebar:target{-webkit-transform:translateX(0);transform:translateX(0)}.off-canvas .off-canvas-sidebar.active~.off-canvas-overlay,.off-canvas .off-canvas-sidebar:target~.off-canvas-overlay{z-index:100;display:block}@media (min-width:960px){.off-canvas.off-canvas-sidebar-show .off-canvas-toggle{display:none}.off-canvas.off-canvas-sidebar-show .off-canvas-sidebar{position:relative;-webkit-transform:none;transform:none;flex:0 0 auto}.off-canvas.off-canvas-sidebar-show .off-canvas-overlay{display:none!important}}.parallax{position:relative;display:block;width:auto;height:auto}.parallax .parallax-content{width:100%;height:auto;transition:all .4s ease;-webkit-transform:perspective(1000px);transform:perspective(1000px);box-shadow:0 1rem 2.1rem rgba(48,55,66,.3);-webkit-transform-style:preserve-3d;transform-style:preserve-3d}.parallax .parallax-content::before{position:absolute;top:0;left:0;display:block;width:100%;height:100%;content:''}.parallax .parallax-front{position:absolute;z-index:1;top:0;left:0;display:flex;width:100%;height:100%;transition:-webkit-transform .4s;transition:transform .4s;transition:transform .4s,-webkit-transform .4s;-webkit-transform:translateZ(50px) scale(.95);transform:translateZ(50px) scale(.95);text-align:center;color:#fff;text-shadow:0 0 20px rgba(48,55,66,.75);align-items:center;justify-content:center}.parallax .parallax-top-left{position:absolute;z-index:100;top:0;left:0;width:50%;height:50%;outline:0}.parallax .parallax-top-left:focus~.parallax-content,.parallax .parallax-top-left:hover~.parallax-content{-webkit-transform:perspective(1000px) rotateX(3deg) rotateY(-3deg);transform:perspective(1000px) rotateX(3deg) rotateY(-3deg)}.parallax .parallax-top-left:focus~.parallax-content::before,.parallax .parallax-top-left:hover~.parallax-content::before{background:linear-gradient(135deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-top-left:focus~.parallax-content .parallax-front,.parallax .parallax-top-left:hover~.parallax-content .parallax-front{-webkit-transform:translate3d(4.5px,4.5px,50px) scale(.95);transform:translate3d(4.5px,4.5px,50px) scale(.95)}.parallax .parallax-top-right{position:absolute;z-index:100;top:0;right:0;width:50%;height:50%;outline:0}.parallax .parallax-top-right:focus~.parallax-content,.parallax .parallax-top-right:hover~.parallax-content{-webkit-transform:perspective(1000px) rotateX(3deg) rotateY(3deg);transform:perspective(1000px) rotateX(3deg) rotateY(3deg)}.parallax .parallax-top-right:focus~.parallax-content::before,.parallax .parallax-top-right:hover~.parallax-content::before{background:linear-gradient(-135deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-top-right:focus~.parallax-content .parallax-front,.parallax .parallax-top-right:hover~.parallax-content .parallax-front{-webkit-transform:translate3d(-4.5px,4.5px,50px) scale(.95);transform:translate3d(-4.5px,4.5px,50px) scale(.95)}.parallax .parallax-bottom-left{position:absolute;z-index:100;bottom:0;left:0;width:50%;height:50%;outline:0}.parallax .parallax-bottom-left:focus~.parallax-content,.parallax .parallax-bottom-left:hover~.parallax-content{-webkit-transform:perspective(1000px) rotateX(-3deg) rotateY(-3deg);transform:perspective(1000px) rotateX(-3deg) rotateY(-3deg)}.parallax .parallax-bottom-left:focus~.parallax-content::before,.parallax .parallax-bottom-left:hover~.parallax-content::before{background:linear-gradient(45deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-bottom-left:focus~.parallax-content .parallax-front,.parallax .parallax-bottom-left:hover~.parallax-content .parallax-front{-webkit-transform:translate3d(4.5px,-4.5px,50px) scale(.95);transform:translate3d(4.5px,-4.5px,50px) scale(.95)}.parallax .parallax-bottom-right{position:absolute;z-index:100;right:0;bottom:0;width:50%;height:50%;outline:0}.parallax .parallax-bottom-right:focus~.parallax-content,.parallax .parallax-bottom-right:hover~.parallax-content{-webkit-transform:perspective(1000px) rotateX(-3deg) rotateY(3deg);transform:perspective(1000px) rotateX(-3deg) rotateY(3deg)}.parallax .parallax-bottom-right:focus~.parallax-content::before,.parallax .parallax-bottom-right:hover~.parallax-content::before{background:linear-gradient(-45deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-bottom-right:focus~.parallax-content .parallax-front,.parallax .parallax-bottom-right:hover~.parallax-content .parallax-front{-webkit-transform:translate3d(-4.5px,-4.5px,50px) scale(.95);transform:translate3d(-4.5px,-4.5px,50px) scale(.95)}.progress{position:relative;width:100%;height:.2rem;color:#5755d9;border:0;border-radius:.1rem;background:#eef0f3;-webkit-appearance:none;-moz-appearance:none;appearance:none}.progress::-webkit-progress-bar{border-radius:.1rem;background:0 0}.progress::-webkit-progress-value{border-radius:.1rem;background:#5755d9}.progress::-moz-progress-bar{border-radius:.1rem;background:#5755d9}.progress:indeterminate{-webkit-animation:progress-indeterminate 1.5s linear infinite;animation:progress-indeterminate 1.5s linear infinite;background:#eef0f3 linear-gradient(to right,#5755d9 30%,#eef0f3 30%) top left/150% 150% no-repeat}.progress:indeterminate::-moz-progress-bar{background:0 0}@-webkit-keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}.slider{display:block;width:100%;height:1.2rem;background:0 0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.slider:focus{outline:0;box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.slider.tooltip:not([data-tooltip])::after{content:attr(value)}.slider::-webkit-slider-thumb{width:.6rem;height:.6rem;margin-top:-.25rem;transition:-webkit-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s;border:0;border-radius:50%;background:#5755d9;-webkit-appearance:none}.slider::-moz-range-thumb{width:.6rem;height:.6rem;transition:-webkit-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s;border:0;border-radius:50%;background:#5755d9}.slider::-ms-thumb{width:.6rem;height:.6rem;transition:-webkit-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s;border:0;border-radius:50%;background:#5755d9}.slider:active::-webkit-slider-thumb{-webkit-transform:scale(1.25);transform:scale(1.25)}.slider:active::-moz-range-thumb{transform:scale(1.25)}.slider:active::-ms-thumb{transform:scale(1.25)}.slider.disabled::-webkit-slider-thumb,.slider:disabled::-webkit-slider-thumb{-webkit-transform:scale(1);transform:scale(1);background:#f7f8f9}.slider.disabled::-moz-range-thumb,.slider:disabled::-moz-range-thumb{transform:scale(1);background:#f7f8f9}.slider.disabled::-ms-thumb,.slider:disabled::-ms-thumb{transform:scale(1);background:#f7f8f9}.slider::-webkit-slider-runnable-track{width:100%;height:.1rem;border-radius:.1rem;background:#eef0f3}.slider::-moz-range-track{width:100%;height:.1rem;border-radius:.1rem;background:#eef0f3}.slider::-ms-track{width:100%;height:.1rem;border-radius:.1rem;background:#eef0f3}.slider::-ms-fill-lower{background:#5755d9}.timeline .timeline-item{position:relative;display:flex;margin-bottom:1.2rem}.timeline .timeline-item::before{position:absolute;top:1.2rem;left:11px;width:2px;height:100%;content:'';background:#dadee4}.timeline .timeline-item .timeline-left{flex:0 0 auto}.timeline .timeline-item .timeline-content{padding:2px 0 2px .8rem;flex:1 1 auto}.timeline .timeline-item .timeline-icon{display:flex;width:1.2rem;height:1.2rem;text-align:center;color:#fff;border-radius:50%;align-items:center;justify-content:center}.timeline .timeline-item .timeline-icon::before{position:absolute;top:.4rem;left:.4rem;display:block;width:.4rem;height:.4rem;content:'';border:.1rem solid #5755d9;border-radius:50%}.timeline .timeline-item .timeline-icon.icon-lg{line-height:1.2rem;background:#5755d9}.timeline .timeline-item .timeline-icon.icon-lg::before{content:none}.viewer-360{display:flex;flex-direction:column;align-items:center}.viewer-360 .viewer-slider[max='36'][value='1']+.viewer-image{background-position-y:0}.viewer-360 .viewer-slider[max='36'][value='2']+.viewer-image{background-position-y:2.8571428571%}.viewer-360 .viewer-slider[max='36'][value='3']+.viewer-image{background-position-y:5.7142857143%}.viewer-360 .viewer-slider[max='36'][value='4']+.viewer-image{background-position-y:8.5714285714%}.viewer-360 .viewer-slider[max='36'][value='5']+.viewer-image{background-position-y:11.4285714286%}.viewer-360 .viewer-slider[max='36'][value='6']+.viewer-image{background-position-y:14.2857142857%}.viewer-360 .viewer-slider[max='36'][value='7']+.viewer-image{background-position-y:17.1428571429%}.viewer-360 .viewer-slider[max='36'][value='8']+.viewer-image{background-position-y:20%}.viewer-360 .viewer-slider[max='36'][value='9']+.viewer-image{background-position-y:22.8571428571%}.viewer-360 .viewer-slider[max='36'][value='10']+.viewer-image{background-position-y:25.7142857143%}.viewer-360 .viewer-slider[max='36'][value='11']+.viewer-image{background-position-y:28.5714285714%}.viewer-360 .viewer-slider[max='36'][value='12']+.viewer-image{background-position-y:31.4285714286%}.viewer-360 .viewer-slider[max='36'][value='13']+.viewer-image{background-position-y:34.2857142857%}.viewer-360 .viewer-slider[max='36'][value='14']+.viewer-image{background-position-y:37.1428571429%}.viewer-360 .viewer-slider[max='36'][value='15']+.viewer-image{background-position-y:40%}.viewer-360 .viewer-slider[max='36'][value='16']+.viewer-image{background-position-y:42.8571428571%}.viewer-360 .viewer-slider[max='36'][value='17']+.viewer-image{background-position-y:45.7142857143%}.viewer-360 .viewer-slider[max='36'][value='18']+.viewer-image{background-position-y:48.5714285714%}.viewer-360 .viewer-slider[max='36'][value='19']+.viewer-image{background-position-y:51.4285714286%}.viewer-360 .viewer-slider[max='36'][value='20']+.viewer-image{background-position-y:54.2857142857%}.viewer-360 .viewer-slider[max='36'][value='21']+.viewer-image{background-position-y:57.1428571429%}.viewer-360 .viewer-slider[max='36'][value='22']+.viewer-image{background-position-y:60%}.viewer-360 .viewer-slider[max='36'][value='23']+.viewer-image{background-position-y:62.8571428571%}.viewer-360 .viewer-slider[max='36'][value='24']+.viewer-image{background-position-y:65.7142857143%}.viewer-360 .viewer-slider[max='36'][value='25']+.viewer-image{background-position-y:68.5714285714%}.viewer-360 .viewer-slider[max='36'][value='26']+.viewer-image{background-position-y:71.4285714286%}.viewer-360 .viewer-slider[max='36'][value='27']+.viewer-image{background-position-y:74.2857142857%}.viewer-360 .viewer-slider[max='36'][value='28']+.viewer-image{background-position-y:77.1428571429%}.viewer-360 .viewer-slider[max='36'][value='29']+.viewer-image{background-position-y:80%}.viewer-360 .viewer-slider[max='36'][value='30']+.viewer-image{background-position-y:82.8571428571%}.viewer-360 .viewer-slider[max='36'][value='31']+.viewer-image{background-position-y:85.7142857143%}.viewer-360 .viewer-slider[max='36'][value='32']+.viewer-image{background-position-y:88.5714285714%}.viewer-360 .viewer-slider[max='36'][value='33']+.viewer-image{background-position-y:91.4285714286%}.viewer-360 .viewer-slider[max='36'][value='34']+.viewer-image{background-position-y:94.2857142857%}.viewer-360 .viewer-slider[max='36'][value='35']+.viewer-image{background-position-y:97.1428571429%}.viewer-360 .viewer-slider[max='36'][value='36']+.viewer-image{background-position-y:100%}.viewer-360 .viewer-slider{width:60%;margin:1rem;cursor:ew-resize;order:2}.viewer-360 .viewer-image{max-width:100%;background-repeat:no-repeat;background-position-y:0;background-size:100%;order:1}.btn-hero{margin-top:46px;border:1px double;border-radius:46px}.home-links{margin-top:66px;padding:12px;border-top:1px dotted bisque}.home-links a{margin:6px;color:#deb887}.home-header{padding:24px;text-align:right}.home-header a{margin:6px;color:#deb887}.error-block{display:flex;width:100%;height:99vh;justify-content:center;align-items:center}.error-block .icon{margin-bottom:6px}.form-header{position:absolute;padding:24px}.form-header a{margin:6px;color:#deb887}.form-block{display:flex;flex-direction:column;width:260px}.form-block .logo{height:50px}.form-block .logo-text{position:absolute;margin:6px;color:#426bff}.form-block .btn,.form-block input[type=password],.form-block input[type=text]{border-radius:26px}.form-block .btn{height:30px;margin-top:12px;padding:2px!important}.form-block h2{margin-bottom:26px}.form-block .links{display:block;width:100%;padding:6px;text-align:center}.form-block .links a{margin:6px;color:#deb887}.form-block .scaledown{width:120%;-webkit-transform:scale(.8) translateX(-30px);transform:scale(.8) translateX(-30px)}.center-block{display:flex;width:100%;height:100vh;justify-content:center;align-items:center}.center-block .form-input-hint{margin:0}.centered-block{width:100%;max-width:960px;margin:auto;padding:24px}.centered-block .header{display:flex;margin-bottom:56px;align-items:center}.centered-block .header .nav{margin-left:16px}.centered-block .header .btn{height:46px!important;margin:2px;padding:9px 9px!important;border-radius:46px}.centered-block .example{border-radius:16px;box-shadow:1px 1px 18px #ccc}a.column{text-decoration:none;color:#333}a.column:hover{text-decoration:none!important;color:#000550;-webkit-text-decoration-style:none!important;text-decoration-style:none!important}a.column .card{min-height:200px;transition:all .31s;border-width:0;border-radius:26px;box-shadow:1px 1px 16px #ddd}a.column .card:hover{border-radius:16px} -------------------------------------------------------------------------------- /assets/media/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 7 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /assets/worker.js: -------------------------------------------------------------------------------- 1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i]*)\ssrc\=/gi, ' { 24 | const {opts} = context.result 25 | 26 | if (typeof opts === 'object' && 27 | opts._t === '/m/') { 28 | const eventName = opts._id + ':update'; 29 | EventBus.dispatch(eventName, context, context.result.data) 30 | } 31 | 32 | return Promise.resolve(context) 33 | }], 34 | }, 35 | }; 36 | } 37 | 38 | if (request) { 39 | hooks = { 40 | before: { 41 | all: [(ctx) => { 42 | ctx.params.accessToken = request.cookies [COOKIE_NAME] 43 | }] 44 | } 45 | } 46 | } 47 | 48 | const endpoint = (isClient ? location.origin : process.env.ORIGIN) || 'http://localhost:6767' 49 | 50 | 51 | const rest = feathers.rest(endpoint) 52 | const app = feathers() 53 | 54 | app.configure(rest.axios(axios)) 55 | app.configure(auth(authOptions)) 56 | app.hooks(hooks) 57 | 58 | 59 | if (isClient && WEBSOCKETS) { 60 | const wsc = feathers() 61 | const ws = io(endpoint) 62 | wsc.configure(auth(authOptions)) 63 | wsc.configure(socketio(ws, { 64 | timeout: 2000, 65 | })) 66 | wsc.hooks(hooks) 67 | app.io = wsc 68 | app.ws = ws 69 | } 70 | 71 | return app 72 | 73 | } 74 | 75 | module.exports = factory() 76 | module.exports.factory = factory -------------------------------------------------------------------------------- /components/devtools.js: -------------------------------------------------------------------------------- 1 | // require devtools here 2 | // this script is only for NODE_ENV === development 3 | 4 | require('@frontless/devtool/client') -------------------------------------------------------------------------------- /components/examples/caesar.riot: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 7 | 12 |
13 |
14 | 17 | 20 |
21 |
22 | 60 |
-------------------------------------------------------------------------------- /components/examples/chat.riot: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
Chat
5 |
6 |
7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |

{m.from}

19 |

20 | {m.message} 21 |

22 |
23 |
24 | 25 |
26 | 32 |
33 | 107 | 138 |
-------------------------------------------------------------------------------- /components/examples/example.riot: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 10 | 11 | 16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 | 30 | 31 | 37 | -------------------------------------------------------------------------------- /components/examples/random.riot: -------------------------------------------------------------------------------- 1 | 2 |

Random Number

3 |
4 |

{state.randomNumber}

5 | 6 | Server error. 7 | 8 |
9 |
10 |
11 | 14 | 17 |
18 | 19 | 64 | 82 |
-------------------------------------------------------------------------------- /components/examples/routes/routes.riot: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
Positional Arguments
5 |
6 | Save form state to the URL query then reload the page. 7 |
8 |
9 |
10 |
11 | First name (positional argument) 12 | 13 |
14 |
15 | Last name (positional argument) 16 | 17 |
18 |
19 | Age (url query) 20 | 21 |
22 |
23 | Profession (url query) 24 | 25 |
26 |
27 | 32 |
33 |
34 | 37 |   38 | 41 |
42 | 83 |
-------------------------------------------------------------------------------- /components/examples/todo/todo-list.riot: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Todo App Example

5 |
Enter any unique todo list name. The name should be hard to guess if you want to keep it private :)
6 | 7 |
8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 | 19 | 23 | 27 |
28 |
    29 |
  • 30 |
    31 | markDone(ev, item) }/> 32 | 33 | enter(ev, item) } 36 | value={ item.text } 37 | class={ classes.edit + ' item' } 38 | autofocus /> 39 | 40 |
    41 |
  • 42 |
43 |
    state.pagination.limit }> 44 |
  • 45 | << Prev 46 |
  • 47 |
  • 48 | Next >> 49 |
  • 50 |
51 |
52 | Page { state.pagination.page + 1 } of { totalPages } 53 |
54 |
55 | 56 |
57 | 58 | 255 | -------------------------------------------------------------------------------- /components/examples/todo/todo-list.style.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | promptIcon: { 4 | position: 'absolute !important', 5 | fontSize: '35px !important', 6 | color: 'darkgrey', 7 | top: 10, 8 | left: 12, 9 | }, 10 | filters: { 11 | display: 'flex', 12 | flexWrap: 'wrap', 13 | '& *:last-child:not(:first-child)': { 14 | borderBottomRightRadius: 0, 15 | borderTopRightRadius: 0, 16 | }, 17 | '& *:first-child:not(:last-child)': { 18 | borderBottomLeftRadius: 0, 19 | borderTopLeftRadius: 0, 20 | } 21 | }, 22 | filterButton: { 23 | cursor: 'pointer', 24 | display: 'inline-block', 25 | outline: 0, 26 | fontSize: '.7rem', 27 | height: '1.4rem', 28 | padding: '.05rem .3rem', 29 | flex: '1 0 0', 30 | background: '#fff', 31 | borderColor: '#ccc', 32 | border: '1px solid', 33 | color: '#333', 34 | }, 35 | filterActive: { 36 | background: 'burlywood', 37 | }, 38 | pagination: { 39 | display: 'flex', 40 | listStyle: 'none', 41 | margin: '.2rem 0', 42 | padding: '.2rem 0', 43 | '& li': { 44 | flex: '1 0 50%', 45 | fontSize: '13px', 46 | padding: '10px', 47 | color: 'burlywood', 48 | textDecoration: 'none', 49 | cursor: 'pointer', 50 | }, 51 | '& li:last-child':{ 52 | textAlign: 'right' 53 | } 54 | }, 55 | pageCounter: { 56 | textAlign: 'center', 57 | pointerEvents: 'none', 58 | position: 'absolute', 59 | bottom: '12px', 60 | left: '46%', 61 | fontSize: '13px', 62 | color: 'burlywood', 63 | }, 64 | hidden: { 65 | display: 'none', 66 | }, 67 | 68 | main : { 69 | position: 'relative', 70 | zIndex: 2, 71 | borderTop: '1px solid #e6e6e6', 72 | '&::after': { 73 | color: 'burlywood !important' 74 | }, 75 | }, 76 | 77 | todoname : { 78 | padding: 26 79 | }, 80 | 81 | edit: { 82 | position: 'relative', 83 | margin: 0, 84 | width: '100%', 85 | fontSize: 24, 86 | fontFamily: 'inherit', 87 | fontWeight: 'inherit', 88 | lineHeight: '1.4em', 89 | color: 'inherit', 90 | padding: '16px', 91 | border: '1px solid #999', 92 | boxShadow: 'inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2)', 93 | boxSizing: 'border-box', 94 | '-webkit-font-smoothing': 'antialiased', 95 | '-moz-osx-font-smoothing': 'grayscale', 96 | '&:focus' : { 97 | outline: 'none' 98 | } 99 | }, 100 | 101 | newTodo: { 102 | padding: '16px 16px 16px 60px', 103 | border: 'none', 104 | background: 'rgba(0, 0, 0, 0.003)', 105 | boxShadow: 'inset 0 -2px 1px rgba(0,0,0,0.03)', 106 | width: '100%', 107 | '&:focus': { 108 | outline: 'none', 109 | }, 110 | '&::placeholder': { 111 | fontStyle: 'italic', 112 | fontWeight: 300, 113 | color: '#aaa' 114 | }, 115 | }, 116 | 117 | todoapp: { 118 | 119 | background: '#fff', 120 | // margin: '130px 0 40px 0', 121 | position: 'relative', 122 | // boxShadow: '0 2px 4px 0 rgba(0, 0, 0, 0.2),0 25px 50px 0 rgba(0, 0, 0, 0.1)', 123 | borderRadius: 16, 124 | '& h1': { 125 | position: 'absolute', 126 | top: '-155px', 127 | width: '100%', 128 | fontSize: '100px', 129 | fontWeight: '100', 130 | textAlign: 'center', 131 | color: 'rgba(175, 47, 47, 0.15)', 132 | textRendering: 'optimizeLegibility', 133 | '-webkit-text-rendering': 'optimizeLegibility', 134 | '-moz-text-rendering': 'optimizeLegibility', 135 | }, 136 | 137 | '& li $toggle': { 138 | textAlign: 'center', 139 | width: 40, 140 | height: 'auto', 141 | position: 'absolute', 142 | top: 0, 143 | bottom: 0, 144 | margin: 'auto 0', 145 | border: 'none', 146 | appearance: 'none', 147 | '-webkit-appearance': 'none', 148 | } 149 | 150 | }, 151 | 152 | todoList: { 153 | margin: 0, 154 | padding: 0, 155 | listStyle: 'none', 156 | paddingBottom: 40, 157 | 158 | '& li': { 159 | position: 'relative', 160 | fontSize: 24, 161 | borderBottom: '1px solid #ededed', 162 | 163 | '&:last-child': { 164 | borderBottom: 'none' 165 | }, 166 | 167 | '& label': { 168 | wordBreak: 'break-all', 169 | padding: '15px 15px 15px 60px', 170 | display: 'block', 171 | lineHeight: '1.2', 172 | transition: 'color 0.4s' 173 | }, 174 | '&.completed label': { 175 | color: '#d9d9d9', 176 | textDecoration: 'line-through' 177 | }, 178 | '&:hover .destroy': { 179 | display: 'block' 180 | }, 181 | '& .destroy': { 182 | display: 'none', 183 | position: 'absolute', 184 | top: 0, 185 | right: 10, 186 | bottom: 0, 187 | width: 40, 188 | height: 40, 189 | margin: 'auto 0', 190 | background: 'transparent', 191 | borderRadius: 180, 192 | border: '1px solid #ccc', 193 | fontSize: 30, 194 | color: '#cc9a9a', 195 | marginBottom: 11, 196 | transition: 'color 0.2s ease-out', 197 | '&:hover': { 198 | color: '#af5b5e' 199 | }, 200 | '&:after': { 201 | content: '"×"', 202 | position: 'relative', 203 | top: -5 204 | }, 205 | '&:focus': { 206 | outline: 'none', 207 | } 208 | } 209 | }, 210 | }, 211 | 212 | editing: { 213 | borderBottom: 'none', 214 | padding: 0, 215 | '& .view': { 216 | display: 'none', 217 | } 218 | }, 219 | 220 | editItem: { 221 | display: 'block', 222 | width: 'calc(100% - 43px)', 223 | padding: '12px 16px', 224 | margin: '0 0 0 43px' 225 | }, 226 | 227 | toggle: { 228 | textAlign: 'center', 229 | width: 40, 230 | height: 'auto', 231 | position: 'absolute', 232 | top: 0, 233 | bottom: 0, 234 | margin: 'auto 0', 235 | border: 'none', 236 | appearance: 'none', 237 | '-webkit-appearance': 'none', 238 | opacity: 0, 239 | 240 | '& + label': { 241 | backgroundImage: `url(data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E)`, 242 | backgroundRepeat: 'no-repeat', 243 | backgroundPosition: 'center left', 244 | }, 245 | '&:checked + label':{ 246 | backgroundImage: `url(data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E)`, 247 | } 248 | } 249 | 250 | 251 | 252 | 253 | } 254 | -------------------------------------------------------------------------------- /components/gist.riot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 |
9 | 10 |
11 | 12 | 50 | 65 |
-------------------------------------------------------------------------------- /components/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | ███████╗██████╗ ██████╗ ███╗ ██╗████████╗██╗ ███████╗███████╗███████╗ 3 | ██╔════╝██╔══██╗██╔═══██╗████╗ ██║╚══██╔══╝██║ ██╔════╝██╔════╝██╔════╝ 4 | █████╗ ██████╔╝██║ ██║██╔██╗ ██║ ██║ ██║ █████╗ ███████╗███████╗ 5 | ██╔══╝ ██╔══██╗██║ ██║██║╚██╗██║ ██║ ██║ ██╔══╝ ╚════██║╚════██║ 6 | ██║ ██║ ██║╚██████╔╝██║ ╚████║ ██║ ███████╗███████╗███████║███████║ 7 | ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚══════╝╚══════╝╚══════╝ 8 | <<<<<<<<<<<< FeathersJS - RiotJS - Turbolinks - Express >>>>>>>>>>>>>>> 9 | ---------------------------------------------------------------------------- 10 | @GitHub: https://github.com/nesterow/frontless 11 | @License: MIT 12 | @Author: Anton Nesterov 13 | */ 14 | 15 | 16 | import serverConfig from 'config/server' 17 | import browserConfig from 'config/browser' 18 | 19 | const {corsResolver} = serverConfig 20 | 21 | const { 22 | CACHE_PAGES, 23 | COOKIE_NAME, 24 | IS_PWA 25 | } = browserConfig; 26 | global.CACHE_PAGES = CACHE_PAGES 27 | global.IS_PWA = IS_PWA 28 | 29 | const { 30 | HTTP_SESSION_SECRET, 31 | HTTP_SESSION_SECURE, 32 | REST_AUTH_SECRET, 33 | REST_AUTH_SERVICE, 34 | ORIGIN, 35 | MONGODB_URI 36 | } = process.env; 37 | 38 | const xss = require("xss") 39 | const xssOptions = {} 40 | global.XSS = new xss.FilterXSS(xssOptions) 41 | 42 | 43 | import cors from 'cors' 44 | import cookieParser from 'cookie-parser' 45 | import session from 'express-session' 46 | 47 | import express from '@feathersjs/express' 48 | import feathers from '@feathersjs/feathers' 49 | import socketio from '@feathersjs/socketio' 50 | import authentication from '@feathersjs/authentication' 51 | import local from '@feathersjs/authentication-local' 52 | import Verifier from 'components/verifier' 53 | 54 | 55 | import 'plugins' 56 | import {Frontless} from '@frontless/core/server' 57 | import {MongoClient} from 'mongodb' 58 | import services from 'services' 59 | 60 | 61 | const sessionMiddleware = session({ 62 | secret: HTTP_SESSION_SECRET || 'secret', 63 | resave: false, 64 | saveUninitialized: true, 65 | cookie: {secure: HTTP_SESSION_SECURE === 'yes'}, 66 | }); 67 | 68 | const corsMiddleware = cors({ 69 | origin: corsResolver, 70 | }); 71 | 72 | const api = feathers() 73 | const app = express(api) 74 | 75 | app.emit('setup', app) 76 | 77 | app.use(cookieParser()) 78 | app.use(corsMiddleware) 79 | app.use(sessionMiddleware) 80 | app.use(express.json()) 81 | app.use(express.urlencoded({extended: true})) 82 | app.configure(express.rest()) 83 | app.use('/assets', express.static('assets')) 84 | app.use('/worker.js', express.static('assets/worker.js')) 85 | app.use('/boot.js', express.static('assets/boot.js')) 86 | 87 | app.use((req, res, next) => { 88 | const token = req.cookies [COOKIE_NAME] 89 | app.passport.verifyJWT(token, { 90 | secret: REST_AUTH_SECRET || 'secret', 91 | }). 92 | 93 | then((user) => { 94 | req.session.authenticated = true 95 | req.session.user = user 96 | req.session.save() 97 | next() 98 | }). 99 | 100 | catch((err)=> { 101 | req.session.authenticated = false 102 | req.session.user = { userId: null } 103 | req.session.save() 104 | next() 105 | }) 106 | 107 | }) 108 | 109 | app.configure(socketio({}, function(io) { 110 | 111 | io.origins(corsResolver) 112 | 113 | io.use(function(socket, next) { 114 | sessionMiddleware(socket.request, socket.request.res, next) 115 | }) 116 | 117 | io.use(function(socket, next) { 118 | socket.feathers.request = socket.request 119 | next() 120 | }) 121 | 122 | })) 123 | 124 | app.configure(authentication({ 125 | session: true, 126 | secret: REST_AUTH_SECRET || 'secret', 127 | service: REST_AUTH_SERVICE || 'users', 128 | cookie: { 129 | enabled: true, 130 | name: COOKIE_NAME, 131 | httpOnly: false, 132 | secure: false 133 | }, 134 | jwt: { 135 | header: { typ: 'access' }, 136 | audience: ORIGIN, 137 | subject: 'authentication', 138 | issuer: 'frontless', 139 | algorithm: 'HS256', 140 | expiresIn: '10d' // the access token expiry 141 | }, 142 | })) 143 | 144 | app.configure(local({ 145 | session: true, 146 | usernameField: 'username', 147 | passwordField: 'password', 148 | entityUsernameField: 'username', 149 | entityPasswordField: 'password', 150 | Verifier, 151 | })) 152 | 153 | const dir = __dirname + '/..' 154 | app.emit('setup:ssr', app) 155 | app.use('/*@:args', Frontless(dir, ['styles'])) 156 | app.use('/*', Frontless(dir, ['styles'])) 157 | 158 | app.use((err, req, res, next) => { 159 | const {type, code} = err; 160 | if (type === 'FeathersError') { 161 | res.status(code).json(err.toJSON()) 162 | } 163 | }) 164 | 165 | app.setState = (id, data) => { 166 | return { 167 | opts: { 168 | _t: '/m/', 169 | _id: id, 170 | }, 171 | data, 172 | } 173 | } 174 | 175 | 176 | let Resolve = () => 0 177 | let Reject = () => 0 178 | const ReadyPromise = new Promise((resolve, reject) => { 179 | Resolve = resolve; 180 | Reject = reject; 181 | }) 182 | 183 | const start = (mongo) => { 184 | const {PORT = 6767} = process.env; 185 | app.emit('connected', app, mongo) 186 | services(app, mongo) 187 | app.mongo = mongo; 188 | 189 | app.listen(PORT, (err) => { 190 | console.log(`👍 app is listening on http://localhost:${PORT} \r\n`) 191 | Resolve({app, mongo}) 192 | }). 193 | 194 | on('error', (error) => { 195 | console.log(`❌ ${error} \r\n`) 196 | Reject(error) 197 | }) 198 | 199 | } 200 | 201 | if (MONGODB_URI) 202 | { 203 | 204 | MongoClient.connect(MONGODB_URI, { useNewUrlParser: true }) 205 | .then((mongo) => { 206 | console.error(`✔️ MongoDB connection is active`) 207 | start(mongo) 208 | }) 209 | .catch(() => { 210 | console.error(`❌ MongoDB connection error`) 211 | console.log('↪️ Trying to continue without MongoDB') 212 | start(null) 213 | }) 214 | } 215 | 216 | else 217 | { 218 | start() 219 | } 220 | 221 | export default ReadyPromise -------------------------------------------------------------------------------- /components/store.js: -------------------------------------------------------------------------------- 1 | import {isBrowser} from '@frontless/core' 2 | import store from '@frontless/redux' 3 | 4 | const state = isBrowser && document.__GLOBAL_SHARED_STATE || { 5 | title: '', 6 | } 7 | 8 | const actions = { 9 | 10 | TITLE: function(state, {title}) { 11 | state.title = title 12 | }, 13 | 14 | } 15 | 16 | 17 | 18 | export default store({ 19 | state, 20 | actions, 21 | }, 22 | function() { 23 | return isBrowser ? document : require('@frontless/core/src/mutex').release(); 24 | }) -------------------------------------------------------------------------------- /components/validators/login.validate.js: -------------------------------------------------------------------------------- 1 | 2 | import validate from 'validate.js' 3 | 4 | const Model = { 5 | username: { 6 | presence: { 7 | message: '^Login is required', 8 | }, 9 | length: { 10 | maximum: 12, 11 | minimum: 4, 12 | message: '^Username must be from 6 to 12 symbols', 13 | }, 14 | }, 15 | password: { 16 | presence: { 17 | message: '^Password is required', 18 | }, 19 | length: { 20 | minimum: 6, 21 | message: '^Minimum 6 symbols', 22 | }, 23 | }, 24 | } 25 | 26 | export const LoginModel = Model; 27 | export default (data) => validate(data, Model); 28 | -------------------------------------------------------------------------------- /components/validators/register.validate.js: -------------------------------------------------------------------------------- 1 | import validate from 'validate.js' 2 | import {UserModel} from './login.validate' 3 | 4 | const Model = { 5 | ...UserModel, 6 | agree: { 7 | presence: { 8 | message: '^Before signing up you have to agree with our terms an conditions', 9 | }, 10 | }, 11 | } 12 | 13 | export default (data) => validate(data, Model); -------------------------------------------------------------------------------- /components/verifier.js: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | import bcrypt from 'bcryptjs' 3 | import { get, omit } from 'lodash' 4 | import {FeathersError} from '@feathersjs/errors' 5 | 6 | const debug = Debug('@feathersjs/authentication-local:verify'); 7 | const {MONGO_DATABASE} = process.env 8 | 9 | class Unauthorized extends FeathersError { 10 | constructor(message, data) { 11 | super(message, 'unathorized', 401, 'Unathorized', data) 12 | } 13 | } 14 | 15 | class LocalVerifier { 16 | constructor (app, options = {}) { 17 | this.app = app; 18 | this.options = options; 19 | this._comparePassword = this._comparePassword.bind(this); 20 | this._normalizeResult = this._normalizeResult.bind(this); 21 | this.verify = this.verify.bind(this); 22 | } 23 | 24 | get users(){ 25 | return this.app.mongo.db(MONGO_DATABASE).collection('users') 26 | } 27 | 28 | _comparePassword (entity, password) { 29 | // select entity password field - take entityPasswordField over passwordField 30 | const passwordField = this.options.entityPasswordField || this.options.passwordField; 31 | 32 | // find password in entity, this allows for dot notation 33 | const hash = get(entity, passwordField); 34 | 35 | if (!hash) { 36 | return Promise.reject(new Error(`'${this.options.entity}' record in the database is missing a '${passwordField}'`)); 37 | } 38 | 39 | debug('Verifying password'); 40 | 41 | return new Promise((resolve, reject) => { 42 | bcrypt.compare(password, hash, function (error, result) { 43 | // Handle 500 server error. 44 | if (error) { 45 | return reject(error); 46 | } 47 | 48 | if (!result) { 49 | debug('Password incorrect'); 50 | return reject(false); // eslint-disable-line 51 | } 52 | 53 | debug('Password correct'); 54 | return resolve(entity); 55 | }); 56 | }); 57 | } 58 | 59 | _normalizeResult (results) { 60 | // Paginated services return the array of results in the data attribute. 61 | let entities = results.data ? results.data : results; 62 | let entity = entities[0]; 63 | 64 | // Handle bad username. 65 | if (!entity) { 66 | return Promise.reject(false); // eslint-disable-line 67 | } 68 | 69 | debug(`${this.options.entity} found`); 70 | return Promise.resolve(entity); 71 | } 72 | 73 | async verify (req, username, password, done) { 74 | debug('Checking credentials', username, password) 75 | 76 | const error = new Unauthorized('Invalid credentials', { message: 'Invalid credentials' }) 77 | 78 | if (!this.users) 79 | return done(error, {}, { message: 'MongoDB connection error' }) 80 | 81 | const user = await this.users.findOne({username,}) 82 | if (!user) 83 | return done(error, {}, { message: 'Invalid login' }) 84 | 85 | try { 86 | return await this._comparePassword(user, password).then(()=> { 87 | return done(null, true, {userId : user._id, username: user.username, sub: 'users'}) 88 | }). 89 | catch((err) => { 90 | return done(error, {} , { message: 'Invalid credentials' }) 91 | }) 92 | } catch (e) { 93 | return done(error, {} , { message: 'Invalid credentials' }) 94 | } 95 | 96 | 97 | } 98 | } 99 | 100 | export default LocalVerifier; 101 | -------------------------------------------------------------------------------- /components/webworker/boot.js: -------------------------------------------------------------------------------- 1 | 2 | navigator.serviceWorker.register('/worker.js') 3 | .then(reg => { 4 | 5 | }) 6 | .catch(err => console.log('Boo!', err)); 7 | 8 | navigator.serviceWorker.addEventListener('message', (event) => { 9 | switch(event.data.type) { 10 | case 'before:revisit': 11 | console.log('before:revisit') 12 | break; 13 | case 'revisit': 14 | Turbolinks.visit(event.data.url) 15 | break; 16 | default: 17 | break; 18 | } 19 | }) -------------------------------------------------------------------------------- /components/webworker/index.js: -------------------------------------------------------------------------------- 1 | 2 | import {CACHE_ASSETS, CACHE_PAGES} from 'config/browser' 3 | 4 | const __ALL__ = [...CACHE_ASSETS, ...CACHE_PAGES] 5 | 6 | const message = (msg) => { 7 | clients.matchAll().then(clients => { 8 | clients.forEach(client => { 9 | client.postMessage(msg) 10 | }) 11 | }) 12 | }; 13 | 14 | const isPage = (string) => { 15 | return CACHE_PAGES.filter(e => (string || '').endsWith(e)).length 16 | } 17 | 18 | self.addEventListener('install', (event) => { 19 | self.skipWaiting() 20 | event.waitUntil(caches.open('appdata').then( 21 | (cache) => { 22 | return cache.addAll(__ALL__) 23 | }) 24 | ) 25 | }) 26 | 27 | self.addEventListener('fetch', event => { 28 | event.respondWith( 29 | caches.open('appdata') 30 | .then (cache => { 31 | return cache.match(event.request) 32 | .then(function(response) { 33 | // return immediately and delete from cache 34 | // then ask turbolinks to revisit location 35 | if (response && isPage(event.request.url)) { 36 | message({ type: 'before:revisit', url: event.request.url }) 37 | setTimeout(() => { 38 | cache.delete(event.request).then((success)=>{ 39 | if (success) { 40 | message({ type: 'revisit', url: event.request.url }) 41 | } 42 | }) 43 | },100); 44 | 45 | return response.text().then((text) => { 46 | 47 | try { 48 | 49 | let data = text.replace(']*)\ssrc\=/gi, ' { 119 | await gulp.task('scss')() 120 | await gulp.task('worker')() 121 | await gulp.task('boot')() 122 | server.emit('restart', 'bundle') 123 | devtool.buildStatus('frontend') 124 | return b.bundle() 125 | .pipe(source('application.js')) 126 | .pipe(gulp.dest('assets/')) 127 | } 128 | bundle() 129 | b.on('update', bundle) 130 | b.on('bundle', () => { 131 | devtool.buildFinshed() 132 | }) 133 | 134 | const server = nodemon({ 135 | script: 'index.js' 136 | , tasks: ['worker', 'boot', 'scss'] 137 | , args: ['./config/environ.env'] 138 | , ignore: ['node_modules/', 'assets/'] 139 | , ext: 'js ejs json jss scss env' 140 | , env: { 'NODE_ENV': process.env.NODE_ENV || 'development' } 141 | , done: done 142 | }). 143 | 144 | on('restart', (files) => { 145 | if (files === 'bundle') { 146 | devtool.buildStarted() 147 | return; 148 | } 149 | setTimeout( () => exec('touch pages/index.riot'), 500 ) 150 | }). 151 | 152 | on('start', (files) => { 153 | devtool.buildStatus('started') 154 | }) 155 | 156 | return server 157 | 158 | 159 | }) 160 | 161 | 162 | 163 | gulp.task('install', ()=>{ 164 | const [a,b,c, repo] = process.argv; 165 | if (!repo) { 166 | return console.log(` 167 | Command syntax: 168 | gulp install @username/repository 169 | `) 170 | } else { 171 | const target = repo.replace('@', ''); 172 | const path = `components/${target}` 173 | const repoURL = `git@github.com:${target}.git` 174 | return runCommand('git', ['clone', repoURL, path]) 175 | .then(() => { 176 | console.log(target, 'installed!') 177 | return runCommand('rm', ['-rf', `${path}/.git`]); 178 | }) 179 | } 180 | }) 181 | 182 | 183 | 184 | gulp.task('scss', function() { 185 | return gulp.src('./styles.scss') 186 | .pipe(sass({outputStyle: 'compact', precision: 10}) 187 | .on('error', sass.logError) 188 | ) 189 | .pipe(autoprefixer()) 190 | .pipe(csscomb()) 191 | .pipe(gulp.dest('./assets/css')) 192 | .pipe(cleancss()) 193 | .pipe(rename({ 194 | suffix: '.min' 195 | })) 196 | .pipe(gulp.dest('./assets/css')); 197 | }); 198 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | ███████╗██████╗ ██████╗ ███╗ ██╗████████╗██╗ ███████╗███████╗███████╗ 3 | ██╔════╝██╔══██╗██╔═══██╗████╗ ██║╚══██╔══╝██║ ██╔════╝██╔════╝██╔════╝ 4 | █████╗ ██████╔╝██║ ██║██╔██╗ ██║ ██║ ██║ █████╗ ███████╗███████╗ 5 | ██╔══╝ ██╔══██╗██║ ██║██║╚██╗██║ ██║ ██║ ██╔══╝ ╚════██║╚════██║ 6 | ██║ ██║ ██║╚██████╔╝██║ ╚████║ ██║ ███████╗███████╗███████║███████║ 7 | ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚══════╝╚══════╝╚══════╝ 8 | <<<<<<<<<<<< FeathersJS - RiotJS - Turbolinks - Express >>>>>>>>>>>>>>> 9 | ---------------------------------------------------------------------------- 10 | @GitHub: https://github.com/nesterow/frontless 11 | @License: MIT 12 | @Author: Anton Nesterov 13 | */ 14 | 15 | const {NODE_ENV} = process.env; 16 | 17 | if (NODE_ENV !== 'test') 18 | { 19 | const dotenv = require('dotenv') 20 | dotenv.config({path: process.argv[process.argv.length - 1]}) 21 | } 22 | 23 | const {babel} = require('@frontless/core/server') 24 | babel() 25 | 26 | module.exports = require('components/server') -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontless", 3 | "version": "1.0.7", 4 | "main": "index.js", 5 | "repository": "git@github.com:nesterow/frontless.git", 6 | "author": "Anton Nesterov ", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "cross-env NODE_ENV=development gulp", 10 | "build": "cross-env NODE_ENV=production gulp build", 11 | "test": "cross-env NODE_PATH=\".:./components:./node_modules\" NODE_ENV=test mocha -r esm specs/*.js -not -path tests/e2e/*", 12 | "test:e2e": "cross-env NODE_PATH=\".:./components:./node_modules\" NODE_ENV=test mocha -r esm specs/e2e/*.js" 13 | }, 14 | "dependencies": { 15 | "@babel/preset-env": "^7.5.5", 16 | "@feathersjs/authentication": "^2.1.16", 17 | "@feathersjs/authentication-client": "^1.0.11", 18 | "@feathersjs/authentication-local": "^1.2.9", 19 | "@feathersjs/client": "^3.7.8", 20 | "@feathersjs/express": "^1.3.1", 21 | "@feathersjs/feathers": "^3.3.1", 22 | "@feathersjs/socketio": "^3.2.9", 23 | "@feathersjs/socketio-client": "^1.2.1", 24 | "@frontless/core": "^1.1.1", 25 | "@frontless/redux": "1.0.0", 26 | "@frontless/riot": "^4.4.0", 27 | "@riotjs/compiler": "^4.3.10", 28 | "@riotjs/hydrate": "^4.0.0", 29 | "@riotjs/ssr": "^4.1.0", 30 | "axios": "^0.19.0", 31 | "cookie-parser": "^1.4.4", 32 | "cookie-storage": "^5.0.3", 33 | "cors": "^2.8.5", 34 | "dotenv": "^8.1.0", 35 | "eventbusjs": "^0.2.0", 36 | "express": "^4.17.1", 37 | "express-session": "^1.16.2", 38 | "feathers-mongodb": "^5.0.0", 39 | "gulp": "^4.0.2", 40 | "jsdom": "15.1.1", 41 | "lodash": "^4.17.15", 42 | "mongodb": "^3.3.0", 43 | "query-string": "^6.8.2", 44 | "require-globify": "^1.4.1", 45 | "riot": "^4.4.0", 46 | "riot-jss": "^1.0.9", 47 | "socket.io-client": "^2.2.0", 48 | "spectre.css": "^0.5.8", 49 | "turbolinks": "^5.2.0", 50 | "validate.js": "^0.13.1" 51 | }, 52 | "devDependencies": { 53 | "@frontless/devtool": "^1.0.0", 54 | "babelify": "^10.0.0", 55 | "browserify": "^16.5.0", 56 | "chai": "^4.2.0", 57 | "cross-env": "^5.2.0", 58 | "eslint": "^6.2.1", 59 | "eslint-config-riot": "^2.0.0", 60 | "esm": "^3.2.25", 61 | "gulp-autoprefixer": "^6.1.0", 62 | "gulp-clean-css": "^4.2.0", 63 | "gulp-csscomb": "^3.0.8", 64 | "gulp-nodemon": "^2.4.2", 65 | "gulp-rename": "^1.4.0", 66 | "gulp-sass": "^4.0.2", 67 | "loadtest": "^3.0.7", 68 | "minify-stream": "^1.2.0", 69 | "minimatch": "^3.0.4", 70 | "mocha": "^6.2.0", 71 | "node-uuid": "^1.4.8", 72 | "riotify": "^4.0.0", 73 | "sinon": "^7.4.1", 74 | "sinon-chai": "^3.3.0", 75 | "tinyify": "^2.5.1", 76 | "uglifyify": "^5.0.2", 77 | "vinyl-source-stream": "^2.0.0", 78 | "watchify": "^3.11.1" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pages/errors/400.riot: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | 5 | 400 6 |
7 | 8 | Error 9 | 10 |

11 | 12 |
13 |
14 |     { unescape(state.stack) }
15 |   
16 | 30 |
-------------------------------------------------------------------------------- /pages/errors/404.riot: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

5 | 6 | 404 7 |
8 | 9 | Page not found 10 | 11 |

12 | 13 |
14 | 15 |
-------------------------------------------------------------------------------- /pages/examples/chat.riot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 18 | -------------------------------------------------------------------------------- /pages/examples/forms.riot: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Form Validation

5 |
6 | 7 |

{error}

8 |
9 |
10 | 11 |

{error}

12 |
13 |
14 | 15 |

{error}

16 |
17 |
18 | 19 |

{error}

20 |
21 |
22 | 27 |

{error}

28 |
29 |
30 | 31 |
32 |
33 |

Success!

34 |
35 |
36 |
37 | 38 | 66 | 75 |
-------------------------------------------------------------------------------- /pages/examples/index.riot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 54 | -------------------------------------------------------------------------------- /pages/examples/random.riot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /pages/examples/todo.riot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 18 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | // 1. import global libraries like JQuery after this line 2 | 3 | import start from '@frontless/core/start' 4 | 5 | const PAGES = require('./**/*.riot', {mode: 'list'}); 6 | const MODULES = require('../components/**/*.riot', {mode: 'list'}); 7 | const components = PAGES.concat(MODULES) 8 | 9 | start({ 10 | components: components, 11 | 12 | before() { 13 | // 2. Do somethig before app is hydrated 14 | 15 | require('plugins') 16 | return Promise.resolve() 17 | }, 18 | 19 | after() { 20 | 21 | // 3. Perform actions after application is hydrated 22 | // require('components/vendor') 23 | 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /pages/index.riot: -------------------------------------------------------------------------------- 1 | 2 | 11 |
12 | Welcome, 13 | 14 | {state.username}! 15 | 16 |
17 |
18 | 19 | 20 | 21 | 26 | 27 | 36 | 37 |
38 | 39 | 64 |
-------------------------------------------------------------------------------- /pages/layout/base.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <% if (IS_PWA && CACHE_PAGES.filter((e) => e.endsWith(req.params [0] || '')).length) { %> 9 | 10 | <%}%> 11 | <% if (IS_PWA) {%> 12 | 13 | <% } %> 14 | 15 | <%- head %> 16 | <%- page.title %> 17 | 18 | 19 | 20 | <%- output %> 21 | 22 | 23 | 24 | 25 | <% if (process.env.NODE_ENV === 'development') { %> 26 | 27 | <%}%> 28 | 29 | -------------------------------------------------------------------------------- /pages/login.riot: -------------------------------------------------------------------------------- 1 | 2 | 7 |
8 |
9 | 10 |

11 | 12 |

13 |
14 | 15 |

{error}

16 |
17 |
18 | 19 |

{error}

20 |
21 |
22 | 23 | 31 |
32 |
33 |
34 | 35 | 95 | 96 |
-------------------------------------------------------------------------------- /pages/profile.riot: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Welcome, {state.username}!

5 | Home 6 | 7 | Logout 8 | 9 |
10 | 11 |
12 | 24 | -------------------------------------------------------------------------------- /pages/register.riot: -------------------------------------------------------------------------------- 1 | 2 | 7 |
8 |
9 | 10 |

11 | 12 | Register 13 |

14 |
15 |
16 | 17 |

{error}

18 |
19 |
20 | 21 |

{error}

22 |
23 |
24 | 29 |

{error}

30 |
31 |
32 | 33 | 41 |
42 |
43 |
44 | 45 | 113 | 114 |
-------------------------------------------------------------------------------- /plugins.js: -------------------------------------------------------------------------------- 1 | //** RiotJS plugins that supposed to work the same way on client and server */ 2 | import client from 'client' 3 | import {extend} from 'lodash' 4 | import {COOKIE_NAME} from 'config/browser' 5 | import {withRouter} from '@frontless/core/browser' 6 | import Store from 'components/store' 7 | 8 | const isBrowser = typeof window !== 'undefined' 9 | const riot = isBrowser ? require('riot') : require('@frontless/riot') 10 | 11 | 12 | // First register components 13 | if (!isBrowser) { 14 | const glob = require('glob') 15 | const path = require('path') 16 | const register = (file) => { 17 | const tag = require(path.resolve(file)) 18 | const component = tag.default; 19 | riot.register(component.name, component) 20 | }; 21 | const test = (file) => { 22 | return !file.startsWith('./specs/') && !file.startsWith('./node_modules/') 23 | } 24 | glob.sync( './**/*.riot' ).forEach( ( file ) => test(file) && register(file)) 25 | } 26 | 27 | const Global = (instance) => { 28 | 29 | instance.setGlobal = function(data) { 30 | if (!isBrowser) { 31 | const globals = JSON.parse(this.req.session.globals || '{}'); 32 | this.req.session.globals = JSON.stringify(extend(globals, data)) 33 | } 34 | }.bind(instance) 35 | 36 | Object.defineProperty(instance, 'globals', { 37 | get: function() { 38 | if (isBrowser) { 39 | const el = document.getElementById('globals') 40 | const data = el ? el.innerText : '{}' 41 | return JSON.parse(data || '{}') 42 | } else { 43 | return JSON.parse(this.req.session.globals || '{}') 44 | } 45 | }.bind(instance) 46 | }) 47 | 48 | if (isBrowser) { 49 | const mounted = instance.onMounted; 50 | instance.onMounted = function(props, state) { 51 | mounted.bind(this)(props, state); 52 | if (instance.onBrowser) { 53 | instance.onBrowser.bind(instance)() 54 | } 55 | }.bind(instance) 56 | } 57 | 58 | } 59 | 60 | const ClientPlugin = (instance) => { 61 | Object.defineProperty(instance, 'client', { 62 | get: function() { 63 | return client.factory(this.req) 64 | }.bind(instance) 65 | }) 66 | instance.service = function(name) { 67 | return this.client.service(name) 68 | }.bind(instance); 69 | }; 70 | 71 | const AuthPlugin = (instance) => { 72 | 73 | const beforeRequest = instance.beforeRequest || (() => Promise.resolve()); 74 | instance.beforeRequest = function(props) { 75 | if (!isBrowser) { 76 | const {authenticated, user} = props.req.session; 77 | const {loggedIn, group} = (instance.access || {}); 78 | if (loggedIn) { 79 | if (!authenticated) { 80 | return this.redirect('/login') 81 | } 82 | if (group && group !== user.group) { 83 | return this.redirect('/login') 84 | } 85 | } 86 | this.setGlobal({ 87 | authenticated: authenticated, 88 | ...user, 89 | }) 90 | } 91 | return Promise.resolve(beforeRequest.bind(instance)(props)) 92 | }.bind(instance) 93 | 94 | 95 | instance.logout = function() { 96 | if (isBrowser) { 97 | const { CookieStorage } = require('cookie-storage') 98 | new CookieStorage().removeItem(COOKIE_NAME) 99 | this.redirect('/login') 100 | } 101 | }.bind(instance) 102 | 103 | } 104 | 105 | if (isBrowser) { 106 | document.__GLOBAL = {} 107 | } 108 | 109 | riot.install(withRouter) 110 | riot.install(Global) 111 | riot.install(Store) 112 | riot.install(ClientPlugin) 113 | riot.install(AuthPlugin) -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Build Status](https://travis-ci.org/nesterow/frontless.svg?branch=master)](https://travis-ci.org/nesterow/frontless) 4 | [![StackShare](http://img.shields.io/badge/tech-stack-0690fa.svg?style=flat)](https://stackshare.io/nesterow/frontless) 5 | ![version](https://img.shields.io/badge/Version-1.0.0@alpha-yellow.svg) 6 | ![MIT license](https://img.shields.io/badge/License-MIT-blue.svg) 7 | 8 |

Feathers + Riot + Turbolinks + Express

9 | 10 | View demo on CodeSandbox.io (refresh if you see 503 error) 11 | 12 | 13 | 14 | ## About 15 | 16 | Frontless is a node.js stack for building universal (isomorphic) javascript applications. At the core, Frontless is just a small Express server that provides a developer with powerful tools for building SSR web applications. 17 | 18 | Frontless is built around the best javascript technologies: Feathers.JS , Riot.JS, Turbolinks, and Express. 19 | 20 | #### Motivation 21 | 22 | In practice, the serverless approach significantly complicates work with data and causes front-end developer to write the code, which would be better performed by the server rather than a browser application. The server has to be responsible for things like routing, db requests, user state (sessions), and in some cases - component's view-model. It would make a front-end developer better concentrate on UI rather than repeating the functionality which is done by back-end in more reliable way. 23 | 24 | 25 | #### The Stack 26 | 27 | Before you start, it is highly recommended to have essential understanding of following technologies: 28 |
29 | [FeathersJS](https://github.com/feathersjs/feathers) | 30 | [RiotJS](https://github.com/riot/riot) | 31 | [Turbolinks](https://github.com/turbolinks/turbolinks) | 32 | [ExpressJS](https://github.com/expressjs/express) 33 |
Stack summary 34 | 35 | 36 | | SERVER | CLIENT | 37 | | :------------- |:-------------| 38 | | Routing - *express.js* | Navigation - *turbolinks* | 39 | | View Model - *feathers* | Data Representation - *riot.js* | 40 | | Layout Rendering - *riot/ssr* | User input - *riot.js* | 41 | | Sessions - *express.js* | *JWT, Cookies* | 42 | | Realtime - *feathers, socket.io]* | *@feathers/client* | 43 | | DB Interface - *@feathers/client* | Rest/IO - *@feathers/client* | 44 | 45 | 46 |
47 | 48 | ## Getting Started 49 | 50 | 1. Clone [this repo](https://github.com/nesterow/frontless) or use NPX 51 | 52 | ``` 53 | npx create-frontless 54 | ``` 55 | 2. Setup a MongoDB Server (optional). Frontless reads `MONGODB_URI` environment variable. 56 | ``` 57 | # config.env 58 | MONGODB_URI=mongodb://localhost:27017/frontless 59 | ``` 60 | 3. Install dependencies and start dev. server 61 | ``` 62 | npm run install 63 | npm start 64 | ``` 65 | Оpen [http://localhost:6767](http://localhost:6767) in your browser. Navigate to the playground for examples 66 | 67 | ## Features 68 | 69 | **Simple routing scheme** 70 | 71 | Routing in-web applications should be as simple as it is in static sites. With that in mind, any Riot.JS component placed in the pages directory is accessible by browser: [`index.riot -> GET /`, `page.riot -> GET /page`]. 72 | 73 | Also, a page can accept positional arguments and it also has access to the Express request context: 74 | ```javascript 75 | // GET https://example.com/foo@bar;baz 76 | export default { 77 | async fetch(){ 78 | const {args} = this.req.params; 79 | const [arg1, arg2] = args; 80 | console.log(arg1 === 'bar') // true 81 | // arg2 = baz 82 | } 83 | } 84 | ``` 85 | 86 | **Synchronous rendering** 87 | 88 | Frontless can render pages after all asynchronous calls are complete. Including children riot components nested inside the page markup. 89 | 90 | **Server-sent state** 91 | 92 | Some API requests can return a ready view-model for a specific component. After it happens, the target component will update its state from received response. This is convenient whenever you want to update the view after a request is done. Given that, the server should return a ready view-model which eliminates extra steps you would do to handle response. 93 | 94 | **State initialization** 95 | 96 | All Riot components rendered on the server side initialize in browser with last state they were on the server side. 97 | 98 | 99 | **RestAPI/Socket.IO** 100 | 101 | Stay close to the database with power of FeathersJS services. 102 | 103 | **It is just Express.JS** 104 | 105 | Everything you can do with an express application. 106 | 107 | ## Documentation 108 | [Frontless Docs](https://frontless.js.org) | [Feathers Docs](https://docs.feathersjs.com/) | [Riot Docs](https://riot.js.org/) 109 | 110 | ## ❤️ Contribute 111 | 112 | If you found a problem and know the solution: 113 | - Fork repository 114 | - Fix the problem 115 | - Push your fix to a separate branch 116 | - Make pull request to the `development` branch 117 | 118 | If you need help, just [open an issue](https://github.com/nesterow/frontless/issues) 119 | 120 | If you understand how it works under the hood, or feel like you can make this project better don't hesitate to message me directly. 121 | 122 | ## License 123 | 124 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/nesterow/frontless/blob/master/LICENSE) file for details 125 | 126 | ## Changelog v1.0.x 127 | [changelog.md](https://github.com/nesterow/frontless/blob/master/changelog.md) 128 | 129 | ## Roadmap v2.0 130 | * [ ] Static site builder [10%] 131 | * [ ] Global state syncronization 132 | * [ ] Push Notifications 133 | 134 | ## Authors 135 | 136 | * **Anton Nesterov** - [@nesterow](https://github.com/nesterow) 137 | 138 | ## Credits 139 | * **Gianluca Guarini** - [@GianlucaGuarini](https://github.com/GianlucaGuarini) - *[riot/hydrate](https://github.com/riot/hydrate)*, *[Riot.js](https://github.com/riot/riot)* 140 | 141 | ## Readme 142 | [Why B2B startups shouldn't use React](https://dev.to/snird/why-b2b-startups-shouldn-t-use-react-or-any-js-framework-3j74) 143 | 144 | -------------------------------------------------------------------------------- /services/examples/caesar.js: -------------------------------------------------------------------------------- 1 | module.exports = (app) => { 2 | 3 | // Caesar Shift 4 | const caesar = (text, shift) => { 5 | return String.fromCharCode( 6 | ...text.split('').map(char => ((char.charCodeAt() - 97 + shift) % 26) + 97), 7 | ); 8 | } 9 | 10 | app.use('caesar', { 11 | 12 | async get(string, params) { 13 | const {query = {}} = params; 14 | const shift = query.decrypt ? -1 : 1; 15 | return app.setState('caesar-shift', { 16 | result: caesar(string, shift) 17 | }) 18 | } 19 | }) 20 | } -------------------------------------------------------------------------------- /services/examples/chat.js: -------------------------------------------------------------------------------- 1 | 2 | const messages = []; 3 | /** 4 | * @param {{from: String, message: String}} message - add message to stack 5 | * */ 6 | function addMessage(message) { 7 | messages.push(message); 8 | if (messages.length > 25) { 9 | messages.unshift(); 10 | } 11 | } 12 | 13 | module.exports = (app) => { 14 | app.use('chat/example', { 15 | 16 | // get messages 17 | async find() { 18 | return messages; 19 | }, 20 | 21 | // enter chat with nickname 22 | async get(nickname, params) { 23 | const username = nickname + Math.round(Math.random() * 250); 24 | params.request.session['nickname'] = username; 25 | params.request.session.save(); 26 | return Promise.resolve({nickname: username}); 27 | }, 28 | 29 | // send a status message 30 | async patch(status, data, params) { 31 | return { 32 | nickname: params.request.session['nickname'], 33 | status, 34 | }; 35 | }, 36 | 37 | // send a message to chat 38 | async create(data, params) { 39 | const msg = { 40 | from: params.request.session['nickname'], 41 | message: data.message, 42 | }; 43 | addMessage(msg); 44 | return msg; 45 | }, 46 | 47 | }); 48 | 49 | app.on('connection', (connection) => { 50 | app.channel('chat/example').join(connection); 51 | }); 52 | 53 | app.service('chat/example').publish('created', (data, context) => { 54 | return [app.channel('chat/example')]; 55 | }); 56 | app.service('chat/example').publish('patched', (data, context) => { 57 | return [app.channel('chat/example')]; 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /services/examples/form.js: -------------------------------------------------------------------------------- 1 | // const validator = require('validator') 2 | const validate = require('validate.js') 3 | 4 | const formModel = { 5 | 6 | username: { 7 | presence: { 8 | message: '^Login is required', 9 | }, 10 | length: { 11 | maximum: 12, 12 | minimum: 4, 13 | message: '^Username must be from 6 to 12 symbols', 14 | }, 15 | }, 16 | 17 | email: { 18 | email: { 19 | message: '^Must be an email', 20 | }, 21 | }, 22 | 23 | password1: { 24 | presence: { 25 | message: '^Password is required', 26 | }, 27 | length: { 28 | minimum: 6, 29 | message: '^Minimum 6 symbols', 30 | }, 31 | }, 32 | 33 | password2: { 34 | equality: { 35 | attribute: "password1", 36 | message: "Passwords do not match", 37 | }, 38 | presence: { 39 | message: '^Password is required', 40 | }, 41 | length: { 42 | minimum: 6, 43 | message: '^Minimum 6 symbols', 44 | }, 45 | }, 46 | 47 | agree: { 48 | presence: { 49 | message: '^Before signing up you have to agree with our terms an conditions', 50 | }, 51 | }, 52 | 53 | } 54 | 55 | 56 | module.exports = (app) => { 57 | app.use('form-validation', { 58 | 59 | async create(data) { 60 | const errors = validate(data, formModel) 61 | if (errors) { 62 | return app.setState('form-validation', { 63 | errors, 64 | success: false, 65 | }) 66 | } 67 | 68 | return app.setState('form-validation', { 69 | errors: {}, 70 | success: true, 71 | }) 72 | 73 | }, 74 | 75 | 76 | }) 77 | } -------------------------------------------------------------------------------- /services/examples/random.js: -------------------------------------------------------------------------------- 1 | module.exports = (app) => { 2 | 3 | const emulateDelay = () => new Promise((resolve) => { 4 | setTimeout(resolve, 100); 5 | }) 6 | 7 | app.use('random-number', { 8 | 9 | async create() { 10 | await emulateDelay(); 11 | return app.setState('random-number-id', { 12 | randomNumber: parseInt(Math.random() * 100000) 13 | }) 14 | }, 15 | 16 | 17 | }) 18 | } -------------------------------------------------------------------------------- /services/examples/todos.js: -------------------------------------------------------------------------------- 1 | const service = require('feathers-mongodb'); 2 | 3 | module.exports = (app, mongo) => { 4 | 5 | const Model = mongo.db('test').collection('todos') 6 | 7 | app.use('/todos', service({ 8 | Model, 9 | paginate: { 10 | default: 5, 11 | max: 10 12 | } 13 | })) 14 | 15 | app.use('todo/count', { 16 | async find(ctx) { 17 | const {identity} = ctx.query; 18 | const all = await Model.countDocuments ({identity,}) 19 | const active = await Model.countDocuments ({ status: 'active', identity }) 20 | const done = await Model.countDocuments ({ status: 'done', identity }) 21 | return { 22 | all, 23 | active, 24 | done, 25 | } 26 | } 27 | }); 28 | 29 | 30 | 31 | } -------------------------------------------------------------------------------- /services/index.js: -------------------------------------------------------------------------------- 1 | 2 | import users from './users' 3 | 4 | export default function (app, mongo) { 5 | 6 | users(app, mongo) 7 | 8 | //Playground services 9 | require('./examples/random')(app) 10 | require('./examples/form')(app) 11 | require('./examples/chat')(app) 12 | require('./examples/todos')(app, mongo) 13 | 14 | } -------------------------------------------------------------------------------- /services/users.js: -------------------------------------------------------------------------------- 1 | import local from '@feathersjs/authentication-local' 2 | import auth from '@feathersjs/authentication' 3 | import validate from 'validators/register.validate' 4 | const {MONGO_DATABASE} = process.env 5 | 6 | 7 | export default (app, mongo) => { 8 | 9 | const Model = mongo.db(MONGO_DATABASE).collection('users') 10 | 11 | app.use('users', { 12 | 13 | async find() { 14 | return [] 15 | }, 16 | 17 | // check if user exists 18 | async get(username) { 19 | const user = await Model.findOne({username,}) 20 | return { 21 | exists: !!user 22 | } 23 | }, 24 | 25 | async create(form) { 26 | 27 | const errors = validate(form) 28 | if (errors) { 29 | return app.setState('signup-form', { 30 | errors, 31 | }) 32 | } 33 | 34 | const {exists} = await this.get(form.username) 35 | if (!exists) { 36 | return Model.insert(form) 37 | } else { 38 | return app.setState('signup-form', { 39 | errors: { 40 | username: ["User already exists"] 41 | } 42 | }); 43 | } 44 | }, 45 | 46 | async update() { 47 | return [] 48 | }, 49 | 50 | async remove() { 51 | return [] 52 | }, 53 | 54 | }) 55 | 56 | app.service('users').hooks({ 57 | before: { 58 | create: [ 59 | local.hooks.hashPassword(), 60 | ] 61 | }, 62 | after: local.hooks.protect('password') 63 | }) 64 | 65 | app.service('authentication').hooks({ 66 | before: { 67 | create: [ 68 | // You can chain multiple strategies 69 | auth.hooks.authenticate(['local']), 70 | ], 71 | remove: [ 72 | auth.hooks.authenticate('jwt') 73 | ] 74 | } 75 | }); 76 | 77 | } 78 | -------------------------------------------------------------------------------- /specs/e2e/loadtest.js: -------------------------------------------------------------------------------- 1 | 2 | const loadtest = require('loadtest'); 3 | const app = require('index').default 4 | 5 | let LAST_LATENCY; 6 | function statusCallback(error, result, latency) { 7 | if (error) { 8 | return console.error(error) 9 | } 10 | LAST_LATENCY = latency 11 | } 12 | 13 | const baseOptions = { 14 | maxRequests: 100, 15 | requestsPerSecond: 10, 16 | statusCallback: statusCallback, 17 | timeout: 5000, 18 | concurrency: 12 19 | }; 20 | 21 | 22 | describe("Test request performance", function() { 23 | this.timeout(10000000) 24 | before(async () => { 25 | return app 26 | }) 27 | 28 | after(async () => { 29 | app.then(({server, mongo}) => { 30 | server.close() 31 | mongo.close() 32 | }) 33 | }) 34 | 35 | 36 | it('perform a load test on `/playground/routes`', async () => { 37 | return new Promise((resolve, reject) => { 38 | loadtest.loadTest({url: 'http://localhost:6767/playground/routes', ...baseOptions}, function(error) { 39 | if (error) { 40 | reject(error) 41 | } 42 | console.log(` 43 | ------------------------------------ 44 | Total requests: ${LAST_LATENCY.totalRequests} 45 | RPS: ${LAST_LATENCY.rps} 46 | ==================================== 47 | Total errors: ${LAST_LATENCY.totalErrors} 48 | Mean Latency: ${LAST_LATENCY.meanLatencyMs}ms 49 | Max Latency: ${LAST_LATENCY.maxLatencyMs}ms 50 | Min Latency: ${LAST_LATENCY.minLatencyMs}ms 51 | ==================================== 52 | `) 53 | resolve(true) 54 | setTimeout(()=>{ 55 | process.exit() 56 | },100) 57 | }); 58 | }) 59 | }) 60 | 61 | }) 62 | 63 | -------------------------------------------------------------------------------- /specs/render.js: -------------------------------------------------------------------------------- 1 | import JSDOMGlobal from 'jsdom-global' 2 | import ssr from '@riotjs/ssr/register' 3 | import {mount, register, install, component} from '@frontless/riot' 4 | import {render} from '@frontless/core/server' 5 | import {expect, use} from 'chai' 6 | import sinonChai from 'sinon-chai' 7 | 8 | describe('render function', function() { 9 | 10 | this.timeout(5000) 11 | 12 | before(() => { 13 | use(sinonChai) 14 | ssr() 15 | }) 16 | 17 | it('it can render pages', async () => { 18 | 19 | 20 | 21 | const PAGE_NAME = 'index-page' 22 | const Page = require('./render/page.riot').default 23 | register('index-page', Page) 24 | register('test', require('./render/test.riot').default) 25 | register('with-jss', require('./render/with-jss.riot').default) 26 | 27 | const {output, state, shared, layout, head, stylesheet } = await render(PAGE_NAME, Page, {}, ['styles']) 28 | 29 | expect(typeof output).to.be.equal('string') 30 | expect(output).to.match(/ { 4 | setTimeout(resolve, timeout) 5 | }) 6 | } -------------------------------------------------------------------------------- /specs/render/page.riot: -------------------------------------------------------------------------------- 1 | 2 |

Test Page

3 | 4 | 5 | 6 | 29 |
-------------------------------------------------------------------------------- /specs/render/test.riot: -------------------------------------------------------------------------------- 1 | 2 |

Test Component

3 | {state.value} 4 | 21 | 26 |
-------------------------------------------------------------------------------- /specs/render/with-jss.riot: -------------------------------------------------------------------------------- 1 | 2 |

With JSS

3 | {state.value} 4 | 30 |
-------------------------------------------------------------------------------- /styles.scss: -------------------------------------------------------------------------------- 1 | // Global styles 2 | @import 'node_modules/spectre.css/src/spectre.scss'; 3 | @import 'node_modules/spectre.css/src/spectre-icons.scss'; 4 | @import 'node_modules/spectre.css/src/spectre-exp.scss'; 5 | 6 | .btn-hero { 7 | border: 1px double; 8 | border-radius: 46px; 9 | margin-top: 46px; 10 | } 11 | 12 | .home-links { 13 | margin-top: 66px; 14 | border-top: 1px dotted bisque; 15 | padding: 12px; 16 | a { 17 | margin: 6px; 18 | color: burlywood; 19 | } 20 | } 21 | 22 | .home-header { 23 | text-align: right; 24 | padding: 24px; 25 | a { 26 | margin: 6px; 27 | color: burlywood; 28 | } 29 | } 30 | 31 | .error-block { 32 | display: flex; 33 | width: 100%; 34 | height: 99vh; 35 | justify-content: center; 36 | align-items: center; 37 | .icon { 38 | margin-bottom: 6px; 39 | } 40 | } 41 | 42 | .form-header { 43 | padding: 24px; 44 | position: absolute; 45 | a { 46 | margin: 6px; 47 | color: burlywood; 48 | } 49 | } 50 | .form-block { 51 | width: 260px; 52 | display: flex; 53 | flex-direction: column; 54 | 55 | .logo { height: 50px; } 56 | .logo-text { 57 | position: absolute; 58 | margin: 6px; 59 | color: #426bff; 60 | } 61 | input[type='text'], input[type='password'], .btn { 62 | border-radius: 26px; 63 | } 64 | .btn{ 65 | height: 30px; 66 | padding: 2px !important; 67 | margin-top: 12px; 68 | } 69 | h2 { 70 | margin-bottom: 26px; 71 | } 72 | .links { 73 | text-align: center; 74 | display: block; 75 | width: 100%; 76 | padding: 6px; 77 | a { 78 | margin: 6px; 79 | color: burlywood; 80 | } 81 | } 82 | .scaledown { 83 | transform: scale(.8) translateX(-30px); 84 | width: 120%; 85 | } 86 | } 87 | .center-block { 88 | width: 100%; 89 | height: 100vh; 90 | display: flex; 91 | justify-content: center; 92 | align-items: center; 93 | 94 | .form-input-hint { 95 | margin: 0px; 96 | } 97 | } 98 | 99 | .centered-block { 100 | width: 100%; 101 | max-width: 960px; 102 | margin: auto; 103 | padding: 24px; 104 | 105 | .header { 106 | margin-bottom: 56px; 107 | display: flex; 108 | align-items: center; 109 | .nav { 110 | margin-left: 16px; 111 | } 112 | .btn { 113 | border-radius: 46px; 114 | height: 46px !important; 115 | margin: 2px; 116 | padding: 9px 9px !important; 117 | } 118 | } 119 | 120 | .example { 121 | box-shadow: 1px 1px 18px #ccc; 122 | border-radius: 16px; 123 | } 124 | } 125 | 126 | a.column { 127 | color: #333; 128 | text-decoration: none; 129 | &:hover { 130 | color: rgb(0, 5, 80); 131 | text-decoration: none !important; 132 | text-decoration-style: none !important; 133 | } 134 | .card { 135 | border-radius: 26px; 136 | -moz-transition: all .31s; 137 | -o-transition: all .31s; 138 | -webkit-transition: all .31s; 139 | transition: all .31s; 140 | min-height: 200px; 141 | box-shadow: 1px 1px 16px #ddd; 142 | border-width: 0px; 143 | } 144 | .card:hover { 145 | border-radius: 16px; 146 | } 147 | } --------------------------------------------------------------------------------