diff --git a/src/components/v-avatar/index.ts b/src/components/v-avatar/index.ts
new file mode 100644
index 0000000000..573eafcb91
--- /dev/null
+++ b/src/components/v-avatar/index.ts
@@ -0,0 +1,4 @@
+import VAvatar from './v-avatar.vue';
+
+export { VAvatar };
+export default VAvatar;
diff --git a/src/components/v-avatar/v-avatar.readme.md b/src/components/v-avatar/v-avatar.readme.md
new file mode 100644
index 0000000000..4d35ba1c81
--- /dev/null
+++ b/src/components/v-avatar/v-avatar.readme.md
@@ -0,0 +1,26 @@
+# Avatar
+
+```html
+RVZ
+
+
+
+
+
+
+
+
+```
+
+## Sizes / Colors
+
+The avatar component supports multiple sizes and colors. The color prop accepts any valid CSS color. CSS variable names can be passed as well.
+
+| Prop Name | Size in PX |
+|----------------|------------|
+| `x-small` | 32 |
+| `small` | 40 |
+| None (default) | 48 |
+| `large` | 56 |
+| `x-large` | 64 |
+
diff --git a/src/components/v-avatar/v-avatar.story.ts b/src/components/v-avatar/v-avatar.story.ts
new file mode 100644
index 0000000000..9453e0adb8
--- /dev/null
+++ b/src/components/v-avatar/v-avatar.story.ts
@@ -0,0 +1,174 @@
+import {
+ withKnobs,
+ text,
+ boolean,
+ number,
+ color,
+ optionsKnob as options
+} from '@storybook/addon-knobs';
+import { action } from '@storybook/addon-actions';
+import Vue from 'vue';
+import VAvatar from './v-avatar.vue';
+import VIcon from '../v-icon/';
+import markdown from './v-avatar.readme.md';
+
+Vue.component('v-avatar', VAvatar);
+Vue.component('v-icon', VIcon);
+
+export default {
+ title: 'Components / Avatar',
+ component: VAvatar,
+ decorators: [withKnobs],
+ parameters: {
+ notes: markdown
+ }
+};
+
+export const withText = () => ({
+ props: {
+ text: {
+ default: text('Text', 'RVZ')
+ },
+ tile: {
+ default: boolean('Tile', false)
+ },
+ color: {
+ default: color('Color', '#009688')
+ },
+ size: {
+ default: options(
+ 'Size',
+ {
+ 'Extra Small': 'xSmall',
+ Small: 'small',
+ '(default)': 'default',
+ Large: 'large',
+ 'Extra Large': 'xLarge'
+ },
+ 'default',
+ {
+ display: 'select'
+ }
+ )
+ },
+ customSize: {
+ default: number('Size (in px)', 0)
+ }
+ },
+ template: `
+ {{ text }}`
+});
+
+export const withImage = () => ({
+ props: {
+ tile: {
+ default: boolean('Tile', false)
+ },
+ color: {
+ default: color('Color', '#009688')
+ },
+ size: {
+ default: options(
+ 'Size',
+ {
+ 'Extra Small': 'xSmall',
+ Small: 'small',
+ '(default)': 'default',
+ Large: 'large',
+ 'Extra Large': 'xLarge'
+ },
+ 'default',
+ {
+ display: 'select'
+ }
+ )
+ },
+ customSize: {
+ default: number('Size (in px)', 0)
+ }
+ },
+ template: `
+
+
+ `
+});
+
+export const withIcon = () => ({
+ props: {
+ tile: {
+ default: boolean('Tile', false)
+ },
+ color: {
+ default: color('Color', '#009688')
+ },
+ size: {
+ default: options(
+ 'Size',
+ {
+ 'Extra Small': 'xSmall',
+ Small: 'small',
+ '(default)': 'default',
+ Large: 'large',
+ 'Extra Large': 'xLarge'
+ },
+ 'default',
+ {
+ display: 'select'
+ }
+ )
+ },
+ customSize: {
+ default: number('Size (in px)', 0)
+ }
+ },
+ template: `
+
+
+ `
+});
+
+export const sizes = () => ({
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+});
diff --git a/src/components/v-avatar/v-avatar.test.ts b/src/components/v-avatar/v-avatar.test.ts
new file mode 100644
index 0000000000..b8b81a8692
--- /dev/null
+++ b/src/components/v-avatar/v-avatar.test.ts
@@ -0,0 +1,78 @@
+import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
+import VueCompositionAPI from '@vue/composition-api';
+
+const localVue = createLocalVue();
+localVue.use(VueCompositionAPI);
+
+import VAvatar from './v-avatar.vue';
+
+describe('Avatar', () => {
+ let component: Wrapper;
+
+ beforeEach(() => (component = mount(VAvatar, { localVue })));
+
+ it('Sets the tile class if tile prop is passed', async () => {
+ component.setProps({ tile: true });
+ await component.vm.$nextTick();
+ expect(component.classes()).toContain('tile');
+ });
+
+ it('Sets the correct custom color', async () => {
+ component.setProps({ color: '--red' });
+ await component.vm.$nextTick();
+ expect((component.vm as any).styles['--_v-avatar-color']).toEqual('var(--red)');
+ });
+
+ describe('Sizes', () => {
+ test('Extra Small', () => {
+ component.setProps({
+ xSmall: true,
+ small: false,
+ large: false,
+ xLarge: false
+ });
+ component.vm.$nextTick(() => expect(component.classes()).toContain('x-small'));
+ });
+
+ test('Small', () => {
+ component.setProps({
+ xSmall: false,
+ small: true,
+ large: false,
+ xLarge: false
+ });
+ component.vm.$nextTick(() => expect(component.classes()).toContain('small'));
+ });
+
+ test('Large', () => {
+ component.setProps({
+ xSmall: false,
+ small: false,
+ large: true,
+ xLarge: false
+ });
+ component.vm.$nextTick(() => expect(component.classes()).toContain('large'));
+ });
+
+ test('Extra Large', () => {
+ component.setProps({
+ xSmall: false,
+ small: false,
+ large: false,
+ xLarge: true
+ });
+ component.vm.$nextTick(() => expect(component.classes()).toContain('x-large'));
+ });
+
+ it('Sets the correct custom size', () => {
+ const component = mount(VAvatar, {
+ localVue,
+ propsData: {
+ size: 128
+ }
+ });
+
+ expect((component.vm as any).styles['--_v-avatar-size']).toBe('128px');
+ });
+ });
+});
diff --git a/src/components/v-avatar/v-avatar.vue b/src/components/v-avatar/v-avatar.vue
new file mode 100644
index 0000000000..b6209857e3
--- /dev/null
+++ b/src/components/v-avatar/v-avatar.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/v-button/index.ts b/src/components/v-button/index.ts
new file mode 100644
index 0000000000..c649b63005
--- /dev/null
+++ b/src/components/v-button/index.ts
@@ -0,0 +1,4 @@
+import VButton from './v-button.vue';
+
+export { VButton };
+export default VButton;
diff --git a/src/components/v-button/v-button.readme.md b/src/components/v-button/v-button.readme.md
new file mode 100644
index 0000000000..5b8f005bce
--- /dev/null
+++ b/src/components/v-button/v-button.readme.md
@@ -0,0 +1,86 @@
+# Button
+
+```html
+Click me!
+```
+
+## Sizes
+
+The button component supports the following sizes through the use of props:
+
+* x-small
+* small
+* (default)
+* large
+* x-large
+
+Alternatively, you can force the font-size through the `size` prop:
+
+```html
+Click me!
+```
+
+## Colors
+
+You can set the color, background color, hover color, and background hover color with the `color`, `background-color`, `hover-color`, and `hover-background-color` props respectively:
+
+```html
+
+ Click me
+
+```
+
+## Events
+
+The only event that can be added to the button is the `click` event:
+
+```html
+Hello!
+```
+
+## Loading
+
+The button has a loading state that can be enabled with the `loading` prop. By default, the button will render a `v-spinner`. You can override what's being shown during the loading state by using the `#loading` slot:
+
+```html
+
+
+ ... Almost done ...
+
+
+```
+
+The loading slot is rendered _on top_ of the content that was there before. Make sure that your loading content doesn't exceed the size of the default state content. This restriction is put in place to prevent jumps when going from and to the loading state.
+
+## Props
+
+| Prop | Description | Default |
+|--------------------------|---------------------------------------------------------------------------|-------------------------------------------|
+| `block` | Enable ull width (display block) | `false` |
+| `icon` | Remove padding / min-width. Meant to be used with just an icon as content | `false` |
+| `outlined` | No background | `false` |
+| `rounded` | Enable rounded corners | `false` |
+| `color` | Text / icon color | `--button-primary-text-color` |
+| `hover-color` | Text / icon color on hover | `--button-primary-text-color` |
+| `background-color` | Button color | `--button-primary-background-color` |
+| `hover-background-color` | Button color on hover | `--button-primary-background-color-hover` |
+| `type` | HTML `type` attribute | `button` |
+| `disabled` | Disabled state | `false` |
+| `loading` | Loading state | `false` |
+| `width` | Width in px | -- |
+| `size` | Size of the text in the button in px | -- |
+| `x-small` | Render extra small | `false` |
+| `small` | Render small | `false` |
+| `large` | Render large | `false` |
+| `x-large` | Render extra large | `false` |
+
+## Slots
+
+| Slot | Description |
+|-----------|----------------------------------------------|
+| `loading` | Content that's rendered during loading state |
diff --git a/src/components/v-button/v-button.story.ts b/src/components/v-button/v-button.story.ts
new file mode 100644
index 0000000000..b7877af8a3
--- /dev/null
+++ b/src/components/v-button/v-button.story.ts
@@ -0,0 +1,279 @@
+import { withKnobs, text, boolean, number, optionsKnob as options } from '@storybook/addon-knobs';
+import { action } from '@storybook/addon-actions';
+import Vue from 'vue';
+import VButton from './v-button.vue';
+import VIcon from '../v-icon/';
+import markdown from './v-button.readme.md';
+
+Vue.component('v-button', VButton);
+Vue.component('v-icon', VIcon);
+
+export default {
+ title: 'Components / Button',
+ component: VButton,
+ decorators: [withKnobs],
+ parameters: {
+ notes: markdown
+ }
+};
+
+export const withText = () => ({
+ methods: { onClick: action('click') },
+ props: {
+ text: {
+ default: text('Text in button', 'Click me')
+ },
+ block: {
+ default: boolean('Block', false, 'Button')
+ },
+ rounded: {
+ default: boolean('Rounded', false, 'Button')
+ },
+ icon: {
+ default: boolean('Icon mode', false, 'Button')
+ },
+ outlined: {
+ default: boolean('Outlined', false, 'Button')
+ },
+ type: {
+ default: text('Type attribute', 'button', 'Button')
+ },
+ loading: {
+ default: boolean('Loading', false, 'Button')
+ },
+ width: {
+ default: number('Width', 0, {}, 'Button')
+ },
+ size: {
+ default: options(
+ 'Size',
+ {
+ 'Extra Small': 'xSmall',
+ Small: 'small',
+ '(default)': 'default',
+ Large: 'large',
+ 'Extra Large': 'xLarge'
+ },
+ 'default',
+ {
+ display: 'select'
+ },
+ 'Button'
+ )
+ },
+ disabled: {
+ default: boolean('Disabled', false, 'Button')
+ },
+ color: {
+ default: text('Color', '--button-primary-text-color', 'Colors')
+ },
+ backgroundColor: {
+ default: text('Background Color', '--button-primary-background-color', 'Colors')
+ },
+ hoverColor: {
+ default: text('Color (hover)', '--white', 'Colors')
+ },
+ hoverBackgroundColor: {
+ default: text('Background Color (hover)', '--black', 'Colors')
+ }
+ },
+ template: `
+
+ {{ text }}
+
+ `
+});
+
+export const withIcon = () => ({
+ methods: { onClick: action('click') },
+ props: {
+ iconName: {
+ default: text('Material Icon', 'add')
+ },
+ block: {
+ default: boolean('Block', false, 'Button')
+ },
+ rounded: {
+ default: boolean('Rounded', true, 'Button')
+ },
+ icon: {
+ default: boolean('Icon mode', true, 'Button')
+ },
+ outlined: {
+ default: boolean('Outlined', false, 'Button')
+ },
+ type: {
+ default: text('Type attribute', 'button', 'Button')
+ },
+ loading: {
+ default: boolean('Loading', false, 'Button')
+ },
+ width: {
+ default: number('Width', 0, {}, 'Button')
+ },
+ size: {
+ default: options(
+ 'Size',
+ {
+ 'Extra Small': 'xSmall',
+ Small: 'small',
+ '(default)': 'default',
+ Large: 'large',
+ 'Extra Large': 'xLarge'
+ },
+ 'default',
+ {
+ display: 'select'
+ },
+ 'Button'
+ )
+ },
+ iconSize: {
+ default: options(
+ 'Size (Icon)',
+ {
+ 'Extra Small': 'xSmall',
+ Small: 'small',
+ '(default)': 'default',
+ Large: 'large',
+ 'Extra Large': 'xLarge'
+ },
+ 'default',
+ {
+ display: 'select'
+ },
+ 'Button'
+ )
+ },
+ disabled: {
+ default: boolean('Disabled', false, 'Button')
+ },
+ color: {
+ default: text('Color', '--button-primary-text-color', 'Colors')
+ },
+ backgroundColor: {
+ default: text('Background Color', '--button-primary-background-color', 'Colors')
+ },
+ hoverColor: {
+ default: text('Color (hover)', '--white', 'Colors')
+ },
+ hoverBackgroundColor: {
+ default: text('Background Color (hover)', '--black', 'Colors')
+ }
+ },
+ template: `
+
+
+
+ `
+});
+
+export const sizes = () => `
+
+ Extra small
+ Small
+ Default
+ Large
+ Extra Large
+
+`;
+
+export const colors = () => `
+
+
+ Delete
+
+
+ Save
+
+
+ Warn
+
+
+ Hover
+
+
+ Transparent
+
+
+`;
+
+export const customLoading = () => ({
+ props: {
+ loading: {
+ default: boolean('Loading', true)
+ }
+ },
+ template: `
+
+ Hello, World!
+
+ ..Loading..
+
+ `
+});
diff --git a/src/components/v-button/v-button.test.ts b/src/components/v-button/v-button.test.ts
new file mode 100644
index 0000000000..e15f9a6d4f
--- /dev/null
+++ b/src/components/v-button/v-button.test.ts
@@ -0,0 +1,157 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import VueCompositionAPI from '@vue/composition-api';
+
+const localVue = createLocalVue();
+localVue.use(VueCompositionAPI);
+localVue.component('v-spinner', VSpinner);
+
+import VButton from './v-button.vue';
+import VSpinner from '../v-spinner/';
+
+describe('Button', () => {
+ it('Renders the provided markup in the default slow', () => {
+ const component = mount(VButton, {
+ localVue,
+ slots: {
+ default: 'Click me'
+ }
+ });
+
+ expect(component.text()).toContain('Click me');
+ });
+
+ it('Adds the outline class for outline buttons', () => {
+ const component = mount(VButton, {
+ localVue,
+ propsData: {
+ outlined: true
+ }
+ });
+
+ expect(component.classes()).toContain('outlined');
+ });
+
+ it('Adds the block class for block buttons', () => {
+ const component = mount(VButton, {
+ localVue,
+ propsData: {
+ block: true
+ }
+ });
+
+ expect(component.classes()).toContain('block');
+ });
+
+ it('Adds the rounded class for rounded buttons', () => {
+ const component = mount(VButton, {
+ localVue,
+ propsData: {
+ rounded: true
+ }
+ });
+
+ expect(component.classes()).toContain('rounded');
+ });
+
+ it('Adds the icon class for icon buttons', () => {
+ const component = mount(VButton, {
+ localVue,
+ propsData: {
+ icon: true
+ }
+ });
+
+ expect(component.classes()).toContain('icon');
+ });
+
+ it('Adds the loading class for loading buttons', () => {
+ const component = mount(VButton, {
+ localVue,
+ propsData: {
+ loading: true
+ }
+ });
+
+ expect(component.classes()).toContain('loading');
+ });
+
+ it('Sets the correct CSS variables for custom colors', () => {
+ const component = mount(VButton, {
+ localVue,
+ propsData: {
+ color: '--red',
+ hoverColor: '--blue',
+ backgroundColor: '--green',
+ hoverBackgroundColor: '--yellow'
+ }
+ });
+
+ expect((component.vm as any).styles['--_v-button-color']).toBe('var(--red)');
+ expect((component.vm as any).styles['--_v-button-hover-color']).toBe('var(--blue)');
+ expect((component.vm as any).styles['--_v-button-background-color']).toBe('var(--green)');
+ expect((component.vm as any).styles['--_v-button-hover-background-color']).toBe(
+ 'var(--yellow)'
+ );
+ });
+
+ describe('Sizes', () => {
+ const component = mount(VButton, {
+ localVue,
+ propsData: {
+ color: '--blue-grey',
+ name: 'person'
+ }
+ });
+
+ test('Extra Small', () => {
+ component.setProps({
+ xSmall: true,
+ small: false,
+ large: false,
+ xLarge: false
+ });
+ component.vm.$nextTick(() => expect(component.classes()).toContain('x-small'));
+ });
+
+ test('Small', () => {
+ component.setProps({
+ xSmall: false,
+ small: true,
+ large: false,
+ xLarge: false
+ });
+ component.vm.$nextTick(() => expect(component.classes()).toContain('small'));
+ });
+
+ test('Large', () => {
+ component.setProps({
+ xSmall: false,
+ small: false,
+ large: true,
+ xLarge: false
+ });
+ component.vm.$nextTick(() => expect(component.classes()).toContain('large'));
+ });
+
+ test('Extra Large', () => {
+ component.setProps({
+ xSmall: false,
+ small: false,
+ large: false,
+ xLarge: true
+ });
+ component.vm.$nextTick(() => expect(component.classes()).toContain('x-large'));
+ });
+
+ it('Sets the correct custom width', () => {
+ const component = mount(VButton, {
+ localVue,
+ propsData: {
+ width: 56
+ }
+ });
+
+ expect((component.vm as any).styles.width).toBe('56px');
+ });
+ });
+});
diff --git a/src/components/v-button/v-button.vue b/src/components/v-button/v-button.vue
new file mode 100644
index 0000000000..f5100cbfc8
--- /dev/null
+++ b/src/components/v-button/v-button.vue
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+
diff --git a/src/components/v-checkbox/index.ts b/src/components/v-checkbox/index.ts
new file mode 100644
index 0000000000..b6c9fbb95c
--- /dev/null
+++ b/src/components/v-checkbox/index.ts
@@ -0,0 +1,4 @@
+import VCheckbox from './v-checkbox.vue';
+
+export { VCheckbox };
+export default VCheckbox;
diff --git a/src/components/v-checkbox/v-checkbox.readme.md b/src/components/v-checkbox/v-checkbox.readme.md
new file mode 100644
index 0000000000..af66fab6d2
--- /dev/null
+++ b/src/components/v-checkbox/v-checkbox.readme.md
@@ -0,0 +1,92 @@
+# Checkbox
+
+## Basic usage
+
+```html
+
+```
+
+## Colors
+
+The checkbox component accepts any CSS color value, or variable name:
+
+```html
+
+
+
+
+```
+
+## Boolean vs arrays
+
+Just as with checkboxes, you can use `v-model` with both an array and a boolean:
+
+
+```html
+
+
+
+
+
+
+
+
+
+```
+
+Keep in mind to pass the `value` prop with a unique value when using arrays in `v-model`.
+
+## Indeterminate
+
+The indeterminate state can be set with the `indeterminate` prop. We recommend using the `.sync` modifier with the indeterminate prop, so the checkbox can set change it too:
+
+```html
+
+
+
+```
+
+If you can't, you should listen to the `update:indeterminate` event and respond to that:
+
+```html
+
+```
+
+## Events
+
+| Event | Description | Data |
+|------------------------|----------------------------|----------------------------|
+| `change` | New state for the checkbox | Boolean or array of values |
+| `update:indeterminate` | New state for the checkbox | Boolean or array of values |
+
+## Props
+
+| Prop | Description | Default |
+|-----------------|--------------------------------------------------------------------------------------------------------|-----------------------------------|
+| `value` | Value for checkbox. Similar to value attr on checkbox type input in HTML | `--` |
+| `inputValue` | Value that's used with `v-model`. Either boolean or array of values | `false` |
+| `label` | Label for the checkbox | `--` |
+| `color` | Color for the checked state of the checkbox. Either CSS var name (fe `--red`) or other valid CSS color | `--input-background-color-active` |
+| `disabled` | Disable the checkbox | `false` |
+| `indeterminate` | Show the indeterminate state | `false` |
+
+## Slots
+
+| Slot | Description |
+|---------|------------------------------------------------------------------------------------------------|
+| `label` | Allows custom markup and HTML to be rendered inside the label. Will override the `label` prop. |
diff --git a/src/components/v-checkbox/v-checkbox.story.ts b/src/components/v-checkbox/v-checkbox.story.ts
new file mode 100644
index 0000000000..e784b1854a
--- /dev/null
+++ b/src/components/v-checkbox/v-checkbox.story.ts
@@ -0,0 +1,120 @@
+import {
+ withKnobs,
+ text,
+ boolean,
+ number,
+ optionsKnob as options,
+ color
+} from '@storybook/addon-knobs';
+import { action } from '@storybook/addon-actions';
+import Vue from 'vue';
+import VCheckbox from '../v-checkbox';
+import markdown from './v-checkbox.readme.md';
+
+Vue.component('v-checkbox', VCheckbox);
+
+export default {
+ title: 'Components / Checkbox',
+ component: VCheckbox,
+ decorators: [withKnobs],
+ parameters: {
+ notes: markdown
+ }
+};
+
+export const booleanState = () => ({
+ methods: {
+ onChange: action('change')
+ },
+ data() {
+ return {
+ checked: false
+ };
+ },
+ template: `
+
+ `
+});
+
+export const arrayState = () => ({
+ methods: {
+ onChange: action('change')
+ },
+ data() {
+ return {
+ options: ['html', 'css']
+ };
+ },
+ template: `
+
+ `
+});
+
+export const disabled = () =>
+ `
`;
+
+export const indeterminate = () => ({
+ data() {
+ return {
+ indeterminate: true,
+ value: null
+ };
+ },
+ template: `
+
+
+indeterminate: {{indeterminate}}
+value: {{value}}
+
+
`
+});
+
+export const colors = () => ({
+ methods: {
+ onChange: action('change')
+ },
+ data() {
+ return {
+ options: ['red', 'yellow', 'custom']
+ };
+ },
+ props: {
+ customColor: {
+ default: color('Custom color', '#4CAF50')
+ }
+ },
+ template: `
+
+
+
+
+
+
+ `
+});
+
+export const htmlLabel = () => ({
+ methods: {
+ onChange: action('change')
+ },
+ data() {
+ return {
+ checked: true
+ };
+ },
+ template: `
+
+
+ Any custom markup in here
+
+
+ `
+});
diff --git a/src/components/v-checkbox/v-checkbox.test.ts b/src/components/v-checkbox/v-checkbox.test.ts
new file mode 100644
index 0000000000..8358040a0e
--- /dev/null
+++ b/src/components/v-checkbox/v-checkbox.test.ts
@@ -0,0 +1,149 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import VueCompositionAPI from '@vue/composition-api';
+import VIcon from '../v-icon/';
+
+const localVue = createLocalVue();
+localVue.use(VueCompositionAPI);
+localVue.component('v-icon', VIcon);
+
+import VCheckbox from './v-checkbox.vue';
+
+describe('Checkbox', () => {
+ it('Renders passed label', () => {
+ const component = mount(VCheckbox, {
+ localVue,
+ propsData: {
+ label: 'Turn me on'
+ }
+ });
+
+ expect(component.find('span[class="label"]').text()).toContain('Turn me on');
+ });
+
+ it('Renders as checked when inputValue `true` is given', () => {
+ const component = mount(VCheckbox, {
+ localVue,
+ propsData: {
+ inputValue: true
+ }
+ });
+
+ expect((component.vm as any).isChecked).toBe(true);
+ });
+
+ it('Calculates check for array inputValue', () => {
+ const component = mount(VCheckbox, {
+ localVue,
+ propsData: {
+ value: 'red',
+ inputValue: ['red']
+ }
+ });
+
+ expect((component.vm as any).isChecked).toBe(true);
+ });
+
+ it('Emits true when state is false', () => {
+ const component = mount(VCheckbox, {
+ localVue,
+ propsData: {
+ inputValue: false
+ }
+ });
+
+ const button = component.find('button');
+ button.trigger('click');
+
+ expect(component.emitted().change[0][0]).toBe(true);
+ });
+
+ it('Disables the button when disabled prop is set', () => {
+ const component = mount(VCheckbox, {
+ localVue,
+ propsData: {
+ disabled: true
+ }
+ });
+
+ const button = component.find('button');
+ expect(Object.keys(button.attributes())).toContain('disabled');
+ });
+
+ it('Appends value to array', () => {
+ const component = mount(VCheckbox, {
+ localVue,
+ propsData: {
+ value: 'red',
+ inputValue: ['blue', 'green']
+ }
+ });
+
+ const button = component.find('button');
+ button.trigger('click');
+
+ expect(component.emitted().change[0][0]).toEqual(['blue', 'green', 'red']);
+ });
+
+ it('Removes value from array', () => {
+ const component = mount(VCheckbox, {
+ localVue,
+ propsData: {
+ value: 'red',
+ inputValue: ['blue', 'green', 'red']
+ }
+ });
+
+ const button = component.find('button');
+ button.trigger('click');
+
+ expect(component.emitted().change[0][0]).toEqual(['blue', 'green']);
+ });
+
+ it('Renders the correct icon for state', () => {
+ const component = mount(VCheckbox, {
+ localVue,
+ propsData: {
+ inputValue: false
+ }
+ });
+
+ expect((component.vm as any).icon).toBe('check_box_outline_blank');
+
+ component.setProps({ inputValue: true });
+
+ expect((component.vm as any).icon).toBe('check_box');
+
+ component.setProps({ indeterminate: true });
+
+ expect((component.vm as any).icon).toBe('indeterminate_check_box');
+ });
+
+ it('Renders the correct iconColor for state', () => {
+ const component = mount(VCheckbox, {
+ localVue,
+ propsData: {
+ inputValue: false,
+ color: '--red'
+ }
+ });
+
+ expect((component.vm as any).iconColor).toBe('--input-border-color');
+
+ component.setProps({ inputValue: true });
+
+ expect((component.vm as any).iconColor).toBe('--red');
+ });
+
+ it('Emits the update:indeterminate event when the checkbox is toggled when indeterminate', () => {
+ const component = mount(VCheckbox, {
+ localVue,
+ propsData: {
+ indeterminate: true
+ }
+ });
+
+ component.find('button').trigger('click');
+
+ expect(component.emitted('update:indeterminate')[0]).toEqual([false]);
+ });
+});
diff --git a/src/components/v-checkbox/v-checkbox.vue b/src/components/v-checkbox/v-checkbox.vue
new file mode 100644
index 0000000000..9d70c9483e
--- /dev/null
+++ b/src/components/v-checkbox/v-checkbox.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
diff --git a/src/components/v-hover/index.ts b/src/components/v-hover/index.ts
new file mode 100644
index 0000000000..aca02b1e16
--- /dev/null
+++ b/src/components/v-hover/index.ts
@@ -0,0 +1,4 @@
+import VHover from './v-hover.vue';
+
+export { VHover };
+export default VHover;
diff --git a/src/components/v-hover/v-hover.readme.md b/src/components/v-hover/v-hover.readme.md
new file mode 100644
index 0000000000..f2d66fe43f
--- /dev/null
+++ b/src/components/v-hover/v-hover.readme.md
@@ -0,0 +1,33 @@
+# Hover (util)
+
+```html
+
+
+
+```
+
+## Delays
+
+You can control how long the hover state persists after the user leaves the element with the `close-delay` prop. Likewise, you can set how long it will take before the hover state is set with the `open-delay` prop:
+
+```html
+
+
+
+```
+
+## Props
+
+| Prop | Description | Default |
+|---------------|----------------------------------------------|---------|
+| `close-delay` | Delay (ms) before the hover state is removed | `0` |
+| `open-delay` | Delay (ms) before the hover state is applied | `0` |
+| `disabled` | Disables the hover state from happening | `false` |
+| `tag` | What HTML tag to use for the wrapper | `div` |
+
+## Events
+n/a
+
+## Slots
+
+Only the default slot is available. The hover state is passed as scoped slot data
diff --git a/src/components/v-hover/v-hover.story.ts b/src/components/v-hover/v-hover.story.ts
new file mode 100644
index 0000000000..8c4553d858
--- /dev/null
+++ b/src/components/v-hover/v-hover.story.ts
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import markdown from './v-hover.readme.md';
+import VIcon from '../v-icon/';
+import VHover from './v-hover.vue';
+
+Vue.component('v-hover', VHover);
+Vue.component('v-icon', VIcon);
+
+export default {
+ title: 'Components / Hover',
+ component: VHover,
+ parameters: {
+ notes: markdown
+ }
+};
+
+export const basic = () => `
+
+
+
+`;
+
+export const customMarkup = () => `
+
+
+
+ Hovering! 🎉🥳
+
+
+ Hover me.
+
+
+`;
diff --git a/src/components/v-hover/v-hover.test.ts b/src/components/v-hover/v-hover.test.ts
new file mode 100644
index 0000000000..eec55c2b7f
--- /dev/null
+++ b/src/components/v-hover/v-hover.test.ts
@@ -0,0 +1,84 @@
+import VueCompositionAPI from '@vue/composition-api';
+import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
+import VHover from './v-hover.vue';
+
+const localVue = createLocalVue();
+localVue.use(VueCompositionAPI);
+
+function sleep(ms: number): Promise {
+ return new Promise(resolve => {
+ setTimeout(resolve, ms);
+ });
+}
+
+describe('Hover', () => {
+ let component: Wrapper;
+
+ beforeEach(() => {
+ component = mount(VHover, { localVue });
+ jest.useFakeTimers();
+ });
+
+ it('Renders the correct element based on tag prop', async () => {
+ component.setProps({ tag: 'span' });
+ await component.vm.$nextTick();
+ expect(component.find('span').exists()).toBe(true);
+ component.setProps({ tag: 'section' });
+ await component.vm.$nextTick();
+ expect(component.find('section').exists()).toBe(true);
+ });
+
+ it('Keeps track of the hover state', async () => {
+ component.find('div').trigger('mouseenter');
+ jest.runAllTimers();
+ expect((component.vm as any).hover).toBe(true);
+
+ component.find('div').trigger('mouseleave');
+ jest.runAllTimers();
+ expect((component.vm as any).hover).toBe(false);
+ });
+
+ it('Adds delays to enter/leave based on props', async () => {
+ component.setProps({
+ openDelay: 300,
+ closeDelay: 600
+ });
+
+ component.find('div').trigger('mouseenter');
+ expect((component.vm as any).hover).toBe(false);
+
+ jest.advanceTimersByTime(150);
+
+ expect((component.vm as any).hover).toBe(false);
+
+ jest.advanceTimersByTime(200);
+
+ expect((component.vm as any).hover).toBe(true);
+
+ component.find('div').trigger('mouseleave');
+
+ jest.advanceTimersByTime(300);
+
+ expect((component.vm as any).hover).toBe(true);
+
+ jest.advanceTimersByTime(300);
+
+ expect((component.vm as any).hover).toBe(false);
+ });
+
+ it("Doesn't do anything if disabled prop is set", async () => {
+ component.setProps({ disabled: true });
+
+ expect((component.vm as any).hover).toBe(false);
+
+ component.find('div').trigger('mouseenter');
+ jest.runAllTimers();
+
+ expect((component.vm as any).hover).toBe(false);
+
+ component.find('div').trigger('mouseleave');
+ jest.runAllTimers();
+
+ expect((component.vm as any).hover).toBe(false);
+ });
+});
diff --git a/src/components/v-hover/v-hover.vue b/src/components/v-hover/v-hover.vue
new file mode 100644
index 0000000000..63da1afb2f
--- /dev/null
+++ b/src/components/v-hover/v-hover.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
diff --git a/src/components/v-icon/custom-icons/box.vue b/src/components/v-icon/custom-icons/box.vue
new file mode 100644
index 0000000000..245663d254
--- /dev/null
+++ b/src/components/v-icon/custom-icons/box.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/src/components/v-icon/index.ts b/src/components/v-icon/index.ts
new file mode 100644
index 0000000000..8fbaaead0e
--- /dev/null
+++ b/src/components/v-icon/index.ts
@@ -0,0 +1,4 @@
+import VIcon from './v-icon.vue';
+
+export { VIcon };
+export default VIcon;
diff --git a/src/components/v-icon/v-icon.readme.md b/src/components/v-icon/v-icon.readme.md
new file mode 100644
index 0000000000..1fb00fb6db
--- /dev/null
+++ b/src/components/v-icon/v-icon.readme.md
@@ -0,0 +1,46 @@
+# Icon
+
+The icon component allows you to render any [Material Design Icons](https://material.io/icons). It also supports rendering of custom SVG based icons.
+
+## Sizes / Colors
+
+The icon component supports multiple sizes and colors. The color prop accepts any valid CSS color. CSS variable names can be passed as well.
+
+| Prop Name | Size in PX |
+|----------------|------------|
+| `sup` | 8 |
+| `x-small` | 12 |
+| `small` | 18 |
+| None (default) | 24 |
+| `large` | 36 |
+| `x-large` | 48 |
+
+The `sup` size is meant to be used as superscript. For example the required state flag.
+
+## Custom Size
+If the default sizes don't give you the exact size you require, you can add the `size` prop with any
+custom pixel value. Note: we recommend using one of the pre-defined sizes to ensure a consistent look
+across the platform.
+
+## Outline
+You can render the outline variant of the Material Icon by setting the `outline` property.
+
+## Click events
+When you add a click event to the icon, the icon will automatically add a pointer cursor.
+
+## Props
+| Name | Description | Default |
+|-----------|-------------------------------------------------------------------|----------------|
+| `name`* | Name of the icon | -- |
+| `color` | CSS color variable name (fe `--blue-grey`) or CSS color value | `currentColor` |
+| `outline` | Use outline Material Icons. Note: only works for non-custom icons | `false` |
+| `size` | Custom pixel size | `false` |
+| `x-small` | Render the icon extra small | `false` |
+| `small` | Render the icon small | `false` |
+| `large` | Render the icon large | `false` |
+| `x-large` | Render the icon extra large | `false` |
+
+## Events
+| Event | Description | Data |
+|---------|----------------------|--------------|
+| `click` | Standard click event | `MouseEvent` |
diff --git a/src/components/v-icon/v-icon.story.ts b/src/components/v-icon/v-icon.story.ts
new file mode 100644
index 0000000000..acfa04a119
--- /dev/null
+++ b/src/components/v-icon/v-icon.story.ts
@@ -0,0 +1,87 @@
+import { withKnobs, text, boolean, number, optionsKnob as options } from '@storybook/addon-knobs';
+import { action } from '@storybook/addon-actions';
+import Vue from 'vue';
+import VIcon from '../v-icon/';
+import markdown from './v-icon.readme.md';
+
+Vue.component('v-icon', VIcon);
+
+export default {
+ title: 'Components / Icon',
+ component: VIcon,
+ decorators: [withKnobs],
+ parameters: {
+ notes: markdown
+ }
+};
+
+export const interactive = () => ({
+ methods: { onClick: action('click') },
+ props: {
+ name: {
+ default: text('Icon Name', 'person')
+ },
+ color: {
+ default: text('Color', '--blue-grey-800')
+ },
+ outline: {
+ default: boolean('Outline', false)
+ },
+ sup: {
+ default: boolean('Superscript', false)
+ },
+ size: {
+ default: options(
+ 'Size',
+ {
+ 'Extra Small': 'xSmall',
+ Small: 'small',
+ '(default)': 'default',
+ Large: 'large',
+ 'Extra Large': 'xLarge'
+ },
+ 'default',
+ {
+ display: 'select'
+ }
+ )
+ },
+ customSize: {
+ default: number('Size (in px)', 0)
+ }
+ },
+ template: `
+
+ `
+});
+
+export const superscript = () => `Title`;
+
+export const sizesAndColors = () => `
+
+
+
+
+
+
+
+
+`;
+
+export const withClickEvent = () => ({
+ methods: {
+ click: action('click')
+ },
+ template: `
+
+`
+});
diff --git a/src/components/v-icon/v-icon.test.ts b/src/components/v-icon/v-icon.test.ts
new file mode 100644
index 0000000000..b52923d6ef
--- /dev/null
+++ b/src/components/v-icon/v-icon.test.ts
@@ -0,0 +1,151 @@
+import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
+import VueCompositionAPI from '@vue/composition-api';
+
+const localVue = createLocalVue();
+localVue.use(VueCompositionAPI);
+
+import VIcon from './v-icon.vue';
+
+describe('Icon', () => {
+ let component: Wrapper;
+
+ beforeEach(() => {
+ component = mount(VIcon, { localVue, propsData: { name: 'person' } });
+ });
+
+ it('Renders the correct markup for a Material Icon', async () => {
+ component.setProps({
+ color: '--blue-grey',
+ name: 'person'
+ });
+
+ await component.vm.$nextTick();
+
+ expect(component.html()).toContain(
+ 'person'
+ );
+ });
+
+ it('Renders custom icons as inline