wip new UI with spectre

master
Ondřej Hruška 4 years ago
parent 3997381748
commit 4a51f7d3c2
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 1
      Cargo.toml
  2. 19
      yopa-test/Cargo.toml
  3. 120
      yopa-test/src/main.rs
  4. 18
      yopa-web/resources/src/components/EditObjectForm.vue
  5. 58
      yopa-web/resources/src/components/EditPropertyField.vue
  6. 45
      yopa-web/resources/src/components/EditRelationForm.vue
  7. 4
      yopa-web/resources/src/components/PropertyField.vue
  8. 5
      yopa-web/resources/src/components/value/BooleanValue.vue
  9. 2
      yopa-web/resources/src/components/value/DecimalValue.vue
  10. 2
      yopa-web/resources/src/components/value/IntegerValue.vue
  11. 2
      yopa-web/resources/src/components/value/StringValue.vue
  12. 8
      yopa-web/resources/src/main.js
  13. 21
      yopa-web/resources/src/style/_icons-extra.scss
  14. 27
      yopa-web/resources/src/style/_spectre-patches.scss
  15. 15
      yopa-web/resources/src/style/app.scss
  16. 21
      yopa-web/resources/src/style/spectre/LICENSE
  17. 38
      yopa-web/resources/src/style/spectre/src/_accordions.scss
  18. 20
      yopa-web/resources/src/style/spectre/src/_animations.scss
  19. 43
      yopa-web/resources/src/style/spectre/src/_asian.scss
  20. 47
      yopa-web/resources/src/style/spectre/src/_autocomplete.scss
  21. 77
      yopa-web/resources/src/style/spectre/src/_avatars.scss
  22. 60
      yopa-web/resources/src/style/spectre/src/_badges.scss
  23. 71
      yopa-web/resources/src/style/spectre/src/_bars.scss
  24. 44
      yopa-web/resources/src/style/spectre/src/_base.scss
  25. 29
      yopa-web/resources/src/style/spectre/src/_breadcrumbs.scss
  26. 193
      yopa-web/resources/src/style/spectre/src/_buttons.scss
  27. 222
      yopa-web/resources/src/style/spectre/src/_calendars.scss
  28. 43
      yopa-web/resources/src/style/spectre/src/_cards.scss
  29. 136
      yopa-web/resources/src/style/spectre/src/_carousels.scss
  30. 33
      yopa-web/resources/src/style/spectre/src/_chips.scss
  31. 31
      yopa-web/resources/src/style/spectre/src/_codes.scss
  32. 116
      yopa-web/resources/src/style/spectre/src/_comparison-sliders.scss
  33. 36
      yopa-web/resources/src/style/spectre/src/_dropdowns.scss
  34. 21
      yopa-web/resources/src/style/spectre/src/_empty.scss
  35. 37
      yopa-web/resources/src/style/spectre/src/_filters.scss
  36. 555
      yopa-web/resources/src/style/spectre/src/_forms.scss
  37. 22
      yopa-web/resources/src/style/spectre/src/_hero.scss
  38. 5
      yopa-web/resources/src/style/spectre/src/_icons.scss
  39. 34
      yopa-web/resources/src/style/spectre/src/_labels.scss
  40. 446
      yopa-web/resources/src/style/spectre/src/_layout.scss
  41. 75
      yopa-web/resources/src/style/spectre/src/_media.scss
  42. 66
      yopa-web/resources/src/style/spectre/src/_menus.scss
  43. 57
      yopa-web/resources/src/style/spectre/src/_meters.scss
  44. 10
      yopa-web/resources/src/style/spectre/src/_mixins.scss
  45. 87
      yopa-web/resources/src/style/spectre/src/_modals.scss
  46. 28
      yopa-web/resources/src/style/spectre/src/_navbar.scss
  47. 34
      yopa-web/resources/src/style/spectre/src/_navs.scss
  48. 446
      yopa-web/resources/src/style/spectre/src/_normalize.scss
  49. 95
      yopa-web/resources/src/style/spectre/src/_off-canvas.scss
  50. 60
      yopa-web/resources/src/style/spectre/src/_pagination.scss
  51. 23
      yopa-web/resources/src/style/spectre/src/_panels.scss
  52. 135
      yopa-web/resources/src/style/spectre/src/_parallax.scss
  53. 65
      yopa-web/resources/src/style/spectre/src/_popovers.scss
  54. 45
      yopa-web/resources/src/style/spectre/src/_progress.scss
  55. 99
      yopa-web/resources/src/style/spectre/src/_sliders.scss
  56. 71
      yopa-web/resources/src/style/spectre/src/_steps.scss
  57. 57
      yopa-web/resources/src/style/spectre/src/_tables.scss
  58. 66
      yopa-web/resources/src/style/spectre/src/_tabs.scss
  59. 38
      yopa-web/resources/src/style/spectre/src/_tiles.scss
  60. 56
      yopa-web/resources/src/style/spectre/src/_timelines.scss
  61. 48
      yopa-web/resources/src/style/spectre/src/_toasts.scss
  62. 79
      yopa-web/resources/src/style/spectre/src/_tooltips.scss
  63. 129
      yopa-web/resources/src/style/spectre/src/_typography.scss
  64. 8
      yopa-web/resources/src/style/spectre/src/_utilities.scss
  65. 117
      yopa-web/resources/src/style/spectre/src/_variables.scss
  66. 34
      yopa-web/resources/src/style/spectre/src/_viewer-360.scss
  67. 315
      yopa-web/resources/src/style/spectre/src/icons/_icons-action.scss
  68. 54
      yopa-web/resources/src/style/spectre/src/icons/_icons-core.scss
  69. 128
      yopa-web/resources/src/style/spectre/src/icons/_icons-navigation.scss
  70. 161
      yopa-web/resources/src/style/spectre/src/icons/_icons-object.scss
  71. 6
      yopa-web/resources/src/style/spectre/src/mixins/_avatar.scss
  72. 54
      yopa-web/resources/src/style/spectre/src/mixins/_button.scss
  73. 8
      yopa-web/resources/src/style/spectre/src/mixins/_clearfix.scss
  74. 27
      yopa-web/resources/src/style/spectre/src/mixins/_color.scss
  75. 11
      yopa-web/resources/src/style/spectre/src/mixins/_label.scss
  76. 65
      yopa-web/resources/src/style/spectre/src/mixins/_position.scss
  77. 9
      yopa-web/resources/src/style/spectre/src/mixins/_shadow.scss
  78. 6
      yopa-web/resources/src/style/spectre/src/mixins/_text.scss
  79. 5
      yopa-web/resources/src/style/spectre/src/mixins/_toast.scss
  80. 18
      yopa-web/resources/src/style/spectre/src/spectre-exp.scss
  81. 10
      yopa-web/resources/src/style/spectre/src/spectre-icons.scss
  82. 49
      yopa-web/resources/src/style/spectre/src/spectre.scss
  83. 31
      yopa-web/resources/src/style/spectre/src/utilities/_colors.scss
  84. 24
      yopa-web/resources/src/style/spectre/src/utilities/_cursors.scss
  85. 44
      yopa-web/resources/src/style/spectre/src/utilities/_display.scss
  86. 50
      yopa-web/resources/src/style/spectre/src/utilities/_divider.scss
  87. 37
      yopa-web/resources/src/style/spectre/src/utilities/_loading.scss
  88. 54
      yopa-web/resources/src/style/spectre/src/utilities/_position.scss
  89. 8
      yopa-web/resources/src/style/spectre/src/utilities/_shapes.scss
  90. 76
      yopa-web/resources/src/style/spectre/src/utilities/_text.scss
  91. 10412
      yopa-web/resources/static/bundle.js
  92. 2
      yopa-web/resources/static/bundle.js.map
  93. 4241
      yopa-web/resources/static/style.css
  94. 270
      yopa-web/resources/static/test_form.html
  95. 286
      yopa-web/resources/test/edit_object.html

@ -3,5 +3,4 @@
members = [
"yopa",
"yopa-web",
#"yopa-test",
]

@ -1,19 +0,0 @@
[package]
name = "yopa-test"
version = "0.1.0"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.13"
simple-logging = "2.0.2"
yopa = { path = "../yopa", features = [ "uuid-ids" ] }
serde_json = "1.0.61"
serde = { version = "1.0.120", features = ["derive"] }
anyhow = "1.0.38"
thiserror = "1.0.23"

@ -1,120 +0,0 @@
#[macro_use] extern crate log;
use log::LevelFilter;
use yopa::{model, InMemoryStorage};
use yopa::model::DataType;
use yopa::insert::{InsertObj, InsertValue, InsertRel};
use yopa::data::TypedValue;
fn main() {
simple_logging::log_to_stderr(LevelFilter::Debug);
main_test_recipes().unwrap();
}
#[allow(non_snake_case)]
fn main_test_recipes() -> anyhow::Result<()> {
simple_logging::log_to_stderr(LevelFilter::Debug);
let mut store = yopa::InMemoryStorage::new();
let Recipe = store.define_object(model::ObjectModel {
id: Default::default(),
name: "recipe".to_string(),
parent: None
})?;
let RecipeTitle = store.define_property(model::PropertyModel {
id: Default::default(),
object: Recipe,
name: "title".to_string(),
optional: false,
multiple: false,
data_type: DataType::String,
default: None
})?;
let _PrepHours = store.define_property(model::PropertyModel {
id: Default::default(),
object: Recipe,
name: "prep_hours".to_string(),
optional: true,
multiple: false,
data_type: DataType::Decimal,
default: None
})?;
let Book = store.define_object(model::ObjectModel {
id: Default::default(),
name: "book".to_string(),
parent: None
})?;
let BookName = store.define_property(model::PropertyModel {
id: Default::default(),
object: Book,
name: "name".to_string(),
optional: false,
multiple: false,
data_type: DataType::String,
default: None
})?;
let BookToRecipe = store.define_relation(model::RelationModel {
id: Default::default(),
object: Recipe,
name: "book reference".to_string(),
optional: true,
multiple: true,
related: Book
})?;
let BookToRecipePage = store.define_property(model::PropertyModel {
id: Default::default(),
object: BookToRecipe,
name: "page".to_string(),
optional: false,
multiple: false,
data_type: DataType::Integer,
default: None
})?;
debug!("{:#?}", store);
let MyBook1 = store.insert_object(InsertObj {
model_id: Book,
values: vec![
InsertValue::new(BookName, TypedValue::String("Recipe Book 1".into())),
],
relations: vec![],
})?;
store.insert_object(InsertObj {
model_id: Recipe,
values: vec![
InsertValue::new(RecipeTitle, TypedValue::String("Pancakes".into())),
],
relations: vec![
InsertRel {
model_id: BookToRecipe,
related_id: MyBook1,
values: vec![
InsertValue::new(BookToRecipePage, TypedValue::Integer(123))
]
}
],
})?;
debug!("{:#?}", store);
let as_s = serde_json::to_string_pretty(&store).unwrap();
println!("{}", as_s);
let _back : InMemoryStorage = serde_json::from_str(&as_s)?;
debug!("After unpack: {:#?}", store);
Ok(())
}

@ -134,13 +134,21 @@ export default {
</script>
<template>
<h2>Edit {{ model.name }}</h2>
<p><input type="button" value="Save" @click="trySave"></p>
<div class="cols">
<div class="col-9">
<h2>Edit {{ model.name }}</h2>
</div>
<div class="col-3 text-right">
<button @click="trySave" class="btn btn-primary">
<i class="icon icon-check"></i>
Save
</button>
</div>
</div>
<table>
<div class="form-horizontal">
<edit-property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></edit-property>
</table>
</div>
<div v-if="haveRelations">
<h3>Relations</h3>

@ -19,7 +19,7 @@ export default {
});
Vue.nextTick(() => {
this.fieldRefs[this.values.length-1].focus();
this.fieldRefs[this.values.length - 1].focus();
})
},
removeValue(vi) {
@ -38,25 +38,39 @@ export default {
</script>
<template>
<tr v-if="values.length===0">
<th @click="addValue">{{model.name}}</th>
<td>
<a href="#" @click="addValue">Add</a>
</td>
</tr>
<tr v-else v-for="(instance, vi) in values" :key="vi">
<th :rowspan="values.length + model.multiple" v-if="vi == 0"><label :for="widget_id">{{model.name}}</label></th>
<td>
<string-value :ref="setFieldRef" :value="instance.value" :id="vi===0?widget_id:null" v-if="model.data_type==='String'"></string-value>
<integer-value :ref="setFieldRef" :value="instance.value" :id="vi===0?widget_id:null" v-if="model.data_type==='Integer'"></integer-value>
<decimal-value :ref="setFieldRef" :value="instance.value" :id="vi===0?widget_id:null" v-if="model.data_type==='Decimal'"></decimal-value>
<boolean-value :ref="setFieldRef" :value="instance.value" :id="vi===0?widget_id:null" v-if="model.data_type==='Boolean'"></boolean-value>
<a href="#" @click="removeValue(vi)" v-if="vi > 0 || model.optional" style="margin-left:5px">X</a>
</td>
</tr>
<tr v-if="values.length > 0 && model.multiple">
<td>
<a href="#" @click="addValue">Add</a>
</td>
</tr>
<div v-if="values.length===0" class="form-group">
<div class="col-3">
<label class="form-label" :for="widget_id">{{ model.name }}</label>
</div>
<div class="col-9">
<a class="btn" @click="addValue">
<i class="icon icon-plus"></i>
Add
</a>
</div>
</div>
<div v-if="values.length>0" class="form-group" v-for="(instance, vi) in values" :key="vi">
<div class="col-3">
<label class="form-label" :for="widget_id" v-if="vi===0">{{ model.name }}</label>
</div>
<div class="col-9">
<string-value :ref="setFieldRef" :value="instance.value" :id="vi === 0 ? widget_id : null" v-if="model.data_type==='String'"></string-value>
<integer-value :ref="setFieldRef" :value="instance.value" :id="vi === 0 ? widget_id : null" v-if="model.data_type==='Integer'"></integer-value>
<decimal-value :ref="setFieldRef" :value="instance.value" :id="vi === 0 ? widget_id : null" v-if="model.data_type==='Decimal'"></decimal-value>
<boolean-value :ref="setFieldRef" :value="instance.value" :id="vi === 0 ? widget_id : null" v-if="model.data_type==='Boolean'"></boolean-value>
<a class="btn btn-delete ml-1" @click="removeValue(vi)" v-if="values.length > 1 || model.optional">
<i class="icon icon-cross"></i>
Delete
</a>
</div>
</div>
<div v-if="values.length>0 && model.multiple" class="form-group">
<div class="col-3"></div>
<div class="col-9">
<a class="btn" @click="addValue">
<i class="icon icon-plus"></i>
Add
</a>
</div>
</div>
</template>

@ -104,22 +104,35 @@ export default {
</style>
<template>
<div class="new-relation" v-for="(instance, ri) in instances" :key="ri">
<b>{{ model.name }} -&gt; {{ related_model.name }}
<select v-model="instance.related">
<option v-for="(name, id) in object_names" :value="id">{{name}}</option>
</select>
</b>
<a href="#" v-if="model.multiple || model.optional && instances.length > 0"
style="margin-left: 5px"
@click="removeInstance(ri)">X</a>
<table v-if="properties">
<edit-property v-for="(property, id) in properties" :model="property" :values="instance.values[id]" :key="id"></edit-property>
</table>
<div class="form-horizontal panel mt-1 mb-1 p-2" v-for="(instance, ri) in instances" :key="ri">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ model.name }} -&gt; {{ related_model.name }}</label>
</div>
<div class="col-7">
<select class="form-select input-inline" v-model="instance.related">
<option v-for="(name, id) in object_names" :value="id">{{name}}</option>
</select>
</div>
<div class="col-2 text-right">
<a class="btn btn-delete" v-if="model.multiple || model.optional && instances.length > 0"
@click="removeInstance(ri)">
<i class="icon-cross icon"></i>
Delete
</a>
</div>
</div>
<edit-property v-for="(property, id) in properties"
:model="property"
:values="instance.values[id]" :key="id"></edit-property>
</div>
<a href="#" v-if="model.multiple || model.optional && instances.length==0"
@click="addInstance">Add {{ model.name }} -&gt; {{ related_model.name }}</a><br>
<div class="mt-2 mb-2">
<a class="btn" v-if="model.multiple || model.optional && instances.length==0"
@click="addInstance">
<i class="icon icon-plus"></i>
Add {{ model.name }} -&gt; {{ related_model.name }}
</a>
</div>
</template>

@ -48,12 +48,12 @@ export default {
<integer-value :ref="setFieldRef" :value="value" :id="vi===0?widget_id:null" v-if="model.data_type==='Integer'"></integer-value>
<decimal-value :ref="setFieldRef" :value="value" :id="vi===0?widget_id:null" v-if="model.data_type==='Decimal'"></decimal-value>
<boolean-value :ref="setFieldRef" :value="value" :id="vi===0?widget_id:null" v-if="model.data_type==='Boolean'"></boolean-value>
<a href="#" @click="removeValue(vi)" v-if="vi > 0 || model.optional" style="margin-left:5px">X</a>
<a class="button ml-2" @click="removeValue(vi)" v-if="vi > 0 || model.optional" style="margin-left:5px">Remove</a>
</td>
</tr>
<tr v-if="values.length > 0 && model.multiple">
<td>
<a href="#" @click="addValue">Add</a>
<a class="button" @click="addValue">Add</a>
</td>
</tr>
</template>

@ -27,5 +27,8 @@ export default {
</script>
<template>
<input ref="input" type="checkbox" :id="id" value="true" v-model="inputValue">
<label class="form-switch input-inline">
<input ref="input" type="checkbox" :id="id" value="true" v-model="inputValue">
<i class="form-icon"></i>
</label>
</template>

@ -27,5 +27,5 @@ export default {
</script>
<template>
<input ref="input" type="number" :id="id" step="any" v-model="inputValue">
<input ref="input" class="form-input input-inline" type="number" :id="id" step="any" v-model="inputValue">
</template>

@ -27,5 +27,5 @@ export default {
</script>
<template>
<input ref="input" type="number" :id="id" step="1" v-model="inputValue">
<input ref="input" class="form-input input-inline" type="number" :id="id" step="1" v-model="inputValue">
</template>

@ -27,5 +27,5 @@ export default {
</script>
<template>
<input ref="input" type="text" :id="id" v-model="inputValue">
<input ref="input" class="form-input input-inline" type="text" :id="id" v-model="inputValue">
</template>

@ -2,10 +2,10 @@ import * as Vue from "vue";
import './style/app.scss';
import StringValue from "./components/StringValue.vue";
import DecimalValue from "./components/DecimalValue.vue";
import BooleanValue from "./components/BooleanValue.vue";
import IntegerValue from "./components/IntegerValue.vue";
import StringValue from "./components/value/StringValue.vue";
import DecimalValue from "./components/value/DecimalValue.vue";
import BooleanValue from "./components/value/BooleanValue.vue";
import IntegerValue from "./components/value/IntegerValue.vue";
import PropertyField from "./components/PropertyField.vue";
import NewObjectForm from "./components/NewObjectForm.vue";

@ -0,0 +1,21 @@
@import "spectre/src/variables";
$icon-border-width: $border-width-lg;
.icon-home {
&::before {
border: $icon-border-width solid currentColor;
border-bottom: 0;
border-right: 0;
transform: translate(-50%, -40%) rotate(45deg);
width: .8em;
height: .8em;
}
&::after {
transform: translate(-50%, -50%);
border-bottom: $icon-border-width solid currentColor;
height: 1em;
width: .8em;
}
}

@ -0,0 +1,27 @@
@import "spectre/src/variables";
.form-select.input-inline {
display: inline-block;
vertical-align: middle;
width: auto;
}
$error-color-faint: lighten($error-color, 50%) !default;
$error-color-dark: darken($error-color, 3%) !default;
.btn.btn-delete {
color: $error-color;
border-color: $error-color;
&:focus,
&:hover {
background: $error-color-faint;
border-color: $error-color-dark;
}
&:active,
&.active {
color: $light-color;
background: $error-color;
border-color: darken($error-color-dark, 5%);
}
}

@ -1,5 +1,12 @@
@import "common";
//@import "common";
//@import "bulma/bulma";
@import "spectre/src/spectre";
@import "spectre/src/spectre-exp";
@import "spectre/src/spectre-icons";
@import "icons-extra";
@import "spectre-patches";
/*
.EditForm th {
text-align: left;
vertical-align: top;
@ -8,3 +15,9 @@
label {
cursor: pointer;
}
*/
//.navbar-brand {
// font-size: 120%;
// font-weight: bold;
//}

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 - 2020 Yan Zhu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,38 @@
// Accordions
.accordion {
input:checked ~,
&[open] {
& .accordion-header > {
.icon:first-child {
transform: rotate(90deg);
}
}
& .accordion-body {
max-height: 50rem;
}
}
.accordion-header {
display: block;
padding: $unit-1 $unit-2;
.icon {
transition: transform .25s;
}
}
.accordion-body {
margin-bottom: $layout-spacing;
max-height: 0;
overflow: hidden;
transition: max-height .25s;
}
}
// Remove default details marker in Webkit
summary.accordion-header {
&::-webkit-details-marker {
display: none;
}
}

@ -0,0 +1,20 @@
// Animations
@keyframes loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes slide-down {
0% {
opacity: 0;
transform: translateY(-$unit-8);
}
100% {
opacity: 1;
transform: translateY(0);
}
}

@ -0,0 +1,43 @@
// Optimized for East Asian CJK
html:lang(zh),
html:lang(zh-Hans),
.lang-zh,
.lang-zh-hans {
font-family: $cjk-zh-hans-font-family;
}
html:lang(zh-Hant),
.lang-zh-hant {
font-family: $cjk-zh-hant-font-family;
}
html:lang(ja),
.lang-ja {
font-family: $cjk-jp-font-family;
}
html:lang(ko),
.lang-ko {
font-family: $cjk-ko-font-family;
}
:lang(zh),
:lang(ja),
.lang-cjk {
ins,
u {
border-bottom: $border-width solid;
text-decoration: none;
}
del + del,
del + s,
ins + ins,
ins + u,
s + del,
s + s,
u + ins,
u + u {
margin-left: .125em;
}
}

@ -0,0 +1,47 @@
// Autocomplete
.form-autocomplete {
position: relative;
.form-autocomplete-input {
align-content: flex-start;
display: flex;
flex-wrap: wrap;
height: auto;
min-height: $unit-8;
padding: $unit-h;
&.is-focused {
@include control-shadow();
border-color: $primary-color;
}
.form-input {
border-color: transparent;
box-shadow: none;
display: inline-block;
flex: 1 0 auto;
height: $unit-6;
line-height: $unit-4;
margin: $unit-h;
width: auto;
}
}
.menu {
left: 0;
position: absolute;
top: 100%;
width: 100%;
}
&.autocomplete-oneline {
.form-autocomplete-input {
flex-wrap: nowrap;
overflow-x: auto;
}
.chip {
flex: 1 0 auto;
}
}
}

@ -0,0 +1,77 @@
// Avatars
.avatar {
@include avatar-base();
background: $primary-color;
border-radius: 50%;
color: rgba($light-color, .85);
display: inline-block;
font-weight: 300;
line-height: 1.25;
margin: 0;
position: relative;
vertical-align: middle;
&.avatar-xs {
@include avatar-base($unit-4);
}
&.avatar-sm {
@include avatar-base($unit-6);
}
&.avatar-lg {
@include avatar-base($unit-12);
}
&.avatar-xl {
@include avatar-base($unit-16);
}
img {
border-radius: 50%;
height: 100%;
position: relative;
width: 100%;
z-index: $zindex-0;
}
.avatar-icon,
.avatar-presence {
background: $bg-color-light;
bottom: 14.64%;
height: 50%;
padding: $border-width-lg;
position: absolute;
right: 14.64%;
transform: translate(50%, 50%);
width: 50%;
z-index: $zindex-0 + 1;
}
.avatar-presence {
background: $gray-color;
box-shadow: 0 0 0 $border-width-lg $light-color;
border-radius: 50%;
height: .5em;
width: .5em;
&.online {
background: $success-color;
}
&.busy {
background: $error-color;
}
&.away {
background: $warning-color;
}
}
&[data-initial]::before {
color: currentColor;
content: attr(data-initial);
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
z-index: $zindex-0;
}
}

@ -0,0 +1,60 @@
// Badges
.badge {
position: relative;
white-space: nowrap;
&[data-badge],
&:not([data-badge]) {
&::after {
background: $primary-color;
background-clip: padding-box;
border-radius: .5rem;
box-shadow: 0 0 0 .1rem $bg-color-light;
color: $light-color;
content: attr(data-badge);
display: inline-block;
transform: translate(-.05rem, -.5rem);
}
}
&[data-badge] {
&::after {
font-size: $font-size-sm;
height: .9rem;
line-height: 1;
min-width: .9rem;
padding: .1rem .2rem;
text-align: center;
white-space: nowrap;
}
}
&:not([data-badge]),
&[data-badge=""] {
&::after {
height: 6px;
min-width: 6px;
padding: 0;
width: 6px;
}
}
// Badges for Buttons
&.btn {
&::after {
position: absolute;
top: 0;
right: 0;
transform: translate(50%, -50%);
}
}
// Badges for Avatars
&.avatar {
&::after {
position: absolute;
top: 14.64%;
right: 14.64%;
transform: translate(50%, -50%);
z-index: $zindex-1;
}
}
}

@ -0,0 +1,71 @@
// Bars
.bar {
background: $bg-color-dark;
border-radius: $border-radius;
display: flex;
flex-wrap: nowrap;
height: $unit-4;
width: 100%;
&.bar-sm {
height: $unit-1;
}
// TODO: attr() support
.bar-item {
background: $primary-color;
color: $light-color;
display: block;
font-size: $font-size-sm;
flex-shrink: 0;
line-height: $unit-4;
height: 100%;
position: relative;
text-align: center;
width: 0;
&:first-child {
border-bottom-left-radius: $border-radius;
border-top-left-radius: $border-radius;
}
&:last-child {
border-bottom-right-radius: $border-radius;
border-top-right-radius: $border-radius;
flex-shrink: 1;
}
}
}
// Slider bar
.bar-slider {
height: $border-width-lg;
margin: $layout-spacing 0;
position: relative;
.bar-item {
left: 0;
padding: 0;
position: absolute;
&:not(:last-child):first-child {
background: $bg-color-dark;
z-index: $zindex-0;
}
}
.bar-slider-btn {
background: $primary-color;
border: 0;
border-radius: 50%;
height: $unit-3;
padding: 0;
position: absolute;
right: 0;
top: 50%;
transform: translate(50%, -50%);
width: $unit-3;
&:active {
box-shadow: 0 0 0 .1rem $primary-color;
}
}
}

@ -0,0 +1,44 @@
// Base
*,
*::before,
*::after {
box-sizing: inherit;
}
html {
box-sizing: border-box;
font-size: $html-font-size;
line-height: $html-line-height;
-webkit-tap-highlight-color: transparent;
}
body {
background: $body-bg;
color: $body-font-color;
font-family: $body-font-family;
font-size: $font-size;
overflow-x: hidden;
text-rendering: optimizeLegibility;
}
a {
color: $link-color;
outline: none;
text-decoration: none;
&:focus {
@include control-shadow();
}
&:focus,
&:hover,
&:active,
&.active {
color: $link-color-dark;
text-decoration: underline;
}
&:visited {
color: $link-color-light;
}
}

@ -0,0 +1,29 @@
// Breadcrumbs
.breadcrumb {
list-style: none;
margin: $unit-1 0;
padding: $unit-1 0;
.breadcrumb-item {
color: $gray-color-dark;
display: inline-block;
margin: 0;
padding: $unit-1 0;
&:not(:last-child) {
margin-right: $unit-1;
a {
color: $gray-color-dark;
}
}
&:not(:first-child) {
&::before {
color: $gray-color-dark;
content: "/";
padding-right: $unit-2;
}
}
}
}

@ -0,0 +1,193 @@
// Buttons
.btn {
appearance: none;
background: $bg-color-light;
border: $border-width solid $primary-color;
border-radius: $border-radius;
color: $primary-color;
cursor: pointer;
display: inline-block;
font-size: $font-size;
height: $control-size;
line-height: $line-height;
outline: none;
padding: $control-padding-y $control-padding-x;
text-align: center;
text-decoration: none;
transition: background .2s, border .2s, box-shadow .2s, color .2s;
user-select: none;
vertical-align: middle;
white-space: nowrap;
&:focus {
@include control-shadow();
}
&:focus,
&:hover {
background: $secondary-color;
border-color: $primary-color-dark;
text-decoration: none;
}
&:active,
&.active {
background: $primary-color-dark;
border-color: darken($primary-color-dark, 5%);
color: $light-color;
text-decoration: none;
&.loading {
&::after {
border-bottom-color: $light-color;
border-left-color: $light-color;
}
}
}
&[disabled],
&:disabled,
&.disabled {
cursor: default;
opacity: .5;
pointer-events: none;
}
// Button Primary
&.btn-primary {
background: $primary-color;
border-color: $primary-color-dark;
color: $light-color;
&:focus,
&:hover {
background: darken($primary-color-dark, 2%);
border-color: darken($primary-color-dark, 5%);
color: $light-color;
}
&:active,
&.active {
background: darken($primary-color-dark, 4%);
border-color: darken($primary-color-dark, 7%);
color: $light-color;
}
&.loading {
&::after {
border-bottom-color: $light-color;
border-left-color: $light-color;
}
}
}
// Button Colors
&.btn-success {
@include button-variant($success-color);
}
&.btn-error {
@include button-variant($error-color);
}
// Button Link
&.btn-link {
background: transparent;
border-color: transparent;
color: $link-color;
&:focus,
&:hover,
&:active,
&.active {
color: $link-color-dark;
}
}
// Button Sizes
&.btn-sm {
font-size: $font-size-sm;
height: $control-size-sm;
padding: $control-padding-y-sm $control-padding-x-sm;
}
&.btn-lg {
font-size: $font-size-lg;
height: $control-size-lg;
padding: $control-padding-y-lg $control-padding-x-lg;
}
// Button Block
&.btn-block {
display: block;
width: 100%;
}
// Button Action
&.btn-action {
width: $control-size;
padding-left: 0;
padding-right: 0;
&.btn-sm {
width: $control-size-sm;
}
&.btn-lg {
width: $control-size-lg;
}
}
// Button Clear
&.btn-clear {
background: transparent;
border: 0;
color: currentColor;
height: $unit-5;
line-height: $unit-4;
margin-left: $unit-1;
margin-right: -2px;
opacity: 1;
padding: $unit-h;
text-decoration: none;
width: $unit-5;
&:focus,
&:hover {
background: rgba($bg-color, .5);
opacity: .95;
}
&::before {
content: "\2715";
}
}
}
// Button groups
.btn-group {
display: inline-flex;
flex-wrap: wrap;
.btn {
flex: 1 0 auto;
&:first-child:not(:last-child) {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
&:not(:first-child):not(:last-child) {
border-radius: 0;
margin-left: -$border-width;
}
&:last-child:not(:first-child) {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
margin-left: -$border-width;
}
&:focus,
&:hover,
&:active,
&.active {
z-index: $zindex-0;
}
}
&.btn-group-block {
display: flex;
.btn {
flex: 1 0 0;
}
}
}

@ -0,0 +1,222 @@
// Calendars
.calendar {
border: $border-width solid $border-color;
border-radius: $border-radius;
display: block;
min-width: 280px;
.calendar-nav {
align-items: center;
background: $bg-color;
border-top-left-radius: $border-radius;
border-top-right-radius: $border-radius;
display: flex;
font-size: $font-size-lg;
padding: $layout-spacing;
}
.calendar-header,
.calendar-body {
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: $layout-spacing 0;
.calendar-date {
flex: 0 0 14.28%; // 7 calendar-items each row
max-width: 14.28%;
}
}
.calendar-header {
background: $bg-color;
border-bottom: $border-width solid $border-color;
color: $gray-color;
font-size: $font-size-sm;
text-align: center;
}
.calendar-body {
color: $gray-color-dark;
}
.calendar-date {
border: 0;
padding: $unit-1;
.date-item {
appearance: none;
background: transparent;
border: $border-width solid transparent;
border-radius: 50%;
color: $gray-color-dark;
cursor: pointer;
font-size: $font-size-sm;
height: $unit-7;
line-height: $unit-5;
outline: none;
padding: $unit-h;
position: relative;
text-align: center;
text-decoration: none;
transition: background .2s, border .2s, box-shadow .2s, color .2s;
vertical-align: middle;
white-space: nowrap;
width: $unit-7;
&.date-today {
border-color: $secondary-color-dark;
color: $primary-color;
}
&:focus {
@include control-shadow();
}
&:focus,
&:hover {
background: $secondary-color-light;
border-color: $secondary-color-dark;
color: $primary-color;
text-decoration: none;
}
&:active,
&.active {
background: $primary-color-dark;
border-color: darken($primary-color-dark, 5%);
color: $light-color;
}
// Calendar badge support
&.badge {
&::after {
position: absolute;
top: 3px;
right: 3px;
transform: translate(50%, -50%);
}
}
}
.date-item,
.calendar-event {
&:disabled,
&.disabled {
cursor: default;
opacity: .25;
pointer-events: none;
}
}
&.prev-month,
&.next-month {
.date-item,
.calendar-event {
opacity: .25;
}
}
}
.calendar-range {
position: relative;
&::before {
background: $secondary-color;
content: "";
height: $unit-7;
left: 0;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
&.range-start {
&::before {
left: 50%;
}
}
&.range-end {
&::before {
right: 50%;
}
}
&.range-start,
&.range-end {
.date-item {
background: $primary-color-dark;
border-color: darken($primary-color-dark, 5%);
color: $light-color;
}
}
.date-item {
color: $primary-color;
}
}
// Calendars size
&.calendar-lg {
.calendar-body {
padding: 0;
.calendar-date {
border-bottom: $border-width solid $border-color;
border-right: $border-width solid $border-color;
display: flex;
flex-direction: column;
height: 5.5rem;
padding: 0;
&:nth-child(7n) {
border-right: 0;
}
&:nth-last-child(-n+7) {
border-bottom: 0;
}
}
}
.date-item {
align-self: flex-end;
height: $unit-7;
margin-right: $layout-spacing-sm;
margin-top: $layout-spacing-sm;
}
.calendar-range {
&::before {
top: 19px;
}
&.range-start {
&::before {
left: auto;
width: 19px;
}
}
&.range-end {
&::before {
right: 19px;
}
}
}
.calendar-events {
flex-grow: 1;
line-height: 1;
overflow-y: auto;
padding: $layout-spacing-sm;
}
.calendar-event {
border-radius: $border-radius;
font-size: $font-size-sm;
display: block;
margin: $unit-h auto;
overflow: hidden;
padding: 3px 4px;
text-overflow: ellipsis;