diff --git a/src/components/v-card/index.ts b/src/components/v-card/index.ts
new file mode 100644
index 0000000000..ee384d2c5c
--- /dev/null
+++ b/src/components/v-card/index.ts
@@ -0,0 +1,4 @@
+import VCard from './v-card.vue';
+
+export { VCard };
+export default VCard;
diff --git a/src/components/v-card/readme.md b/src/components/v-card/readme.md
new file mode 100644
index 0000000000..bd7601a9c5
--- /dev/null
+++ b/src/components/v-card/readme.md
@@ -0,0 +1,167 @@
+# Card
+
+Renders a card. A card is nothing but a v-sheet with predefined building blocks to enforce consistency.
+
+## Usage
+
+```html
+
+ Hello, world!
+ This is a card
+ Consectetur enim ullamco sint sit deserunt proident consectetur.
+
+ Save
+
+
+```
+
+Cards can be used to consistently style dialogs:
+
+```html
+
+
+ Show dialog
+
+
+
+ Are you sure you want to delete 1 item?
+
+ Cancel
+ Yes
+
+
+
+```
+
+## Props
+| Prop | Description | Default |
+|------------|---------------------------------------------------|---------|
+| `disabled` | Disable the card, prevents all cursor interaction | `false` |
+| `tile` | Render without rounded corners | `false` |
+
+## Events
+n/a
+
+## Slots
+| Slot | Description | Data |
+|-----------|-------------|------|
+| _default_ | | |
+
+## CSS Variables
+| Variable | Default |
+|-----------------------------|--------------------|
+| `--v-card-min-width` | `none` |
+| `--v-card-max-width` | `400px` |
+| `--v-card-min-height` | `none` |
+| `--v-card-max-height` | `none` |
+| `--v-card-padding` | `16px` |
+| `--v-card-background-color` | `var(--highlight)` |
+
+---
+
+# Card Title
+
+Functional component that enforces consistent styling.
+
+## Usage
+
+```html
+Hello, world!
+```
+
+## Props
+n/a
+
+## Events
+n/a
+
+## Slots
+| Slot | Description | Data |
+|-----------|-------------|------|
+| _default_ | | |
+
+## CSS Variables
+n/a
+
+---
+
+# Card Subtitle
+
+Functional component that enforces consistent styling.
+
+## Usage
+
+```html
+Hello from the subtitle
+```
+
+## Props
+n/a
+
+## Events
+n/a
+
+## Slots
+| Slot | Description | Data |
+|-----------|-------------|------|
+| _default_ | | |
+
+## CSS Variables
+n/a
+
+---
+
+# Card Text
+
+Functional component that enforces consistent styling.
+
+## Usage
+
+```html
+Nisi anim deserunt Lorem reprehenderit laborum.
+```
+
+## Props
+n/a
+
+## Events
+n/a
+
+## Slots
+| Slot | Description | Data |
+|-----------|-------------|------|
+| _default_ | | |
+
+## CSS Variables
+n/a
+
+---
+
+# Card Actions
+
+Functional component that enforces consistent styling.
+
+## Usage
+
+```html
+
+
+
+
+```
+
+## Props
+n/a
+
+## Events
+n/a
+
+## Slots
+| Slot | Description | Data |
+|-----------|-------------|------|
+| _default_ | | |
+
+## CSS Variables
+n/a
+
+---
diff --git a/src/components/v-card/v-card-actions.vue b/src/components/v-card/v-card-actions.vue
new file mode 100644
index 0000000000..5dfb61bcc6
--- /dev/null
+++ b/src/components/v-card/v-card-actions.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/src/components/v-card/v-card-subtitle.vue b/src/components/v-card/v-card-subtitle.vue
new file mode 100644
index 0000000000..659ded2d9f
--- /dev/null
+++ b/src/components/v-card/v-card-subtitle.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/src/components/v-card/v-card-text.vue b/src/components/v-card/v-card-text.vue
new file mode 100644
index 0000000000..152bca6643
--- /dev/null
+++ b/src/components/v-card/v-card-text.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/src/components/v-card/v-card-title.vue b/src/components/v-card/v-card-title.vue
new file mode 100644
index 0000000000..87cff239bd
--- /dev/null
+++ b/src/components/v-card/v-card-title.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/src/components/v-card/v-card.story.ts b/src/components/v-card/v-card.story.ts
new file mode 100644
index 0000000000..c9a39a8e71
--- /dev/null
+++ b/src/components/v-card/v-card.story.ts
@@ -0,0 +1,93 @@
+import { withKnobs, boolean } from '@storybook/addon-knobs';
+import Vue from 'vue';
+import VCard from './v-card.vue';
+import VCardTitle from './v-card-title.vue';
+import VCardSubtitle from './v-card-subtitle.vue';
+import VCardText from './v-card-text.vue';
+import VCardActions from './v-card-actions.vue';
+import markdown from './readme.md';
+import withPadding from '../../../.storybook/decorators/with-padding';
+import { defineComponent, ref } from '@vue/composition-api';
+
+Vue.component('v-card', VCard);
+Vue.component('v-card-title', VCardTitle);
+Vue.component('v-card-subtitle', VCardSubtitle);
+Vue.component('v-card-text', VCardText);
+Vue.component('v-card-actions', VCardActions);
+
+export default {
+ title: 'Components / Card',
+ decorators: [withKnobs, withPadding],
+ parameters: {
+ notes: markdown
+ }
+};
+
+export const basic = () =>
+ defineComponent({
+ props: {
+ disabled: {
+ default: boolean('Disabled', false)
+ },
+ tile: {
+ default: boolean('Tile', false)
+ }
+ },
+ template: `
+
+ Hello World!
+ This is the subtitle
+ Black ray-bans, you know she's with the band. Such a sight to see and it's all for me. Heaven is jealous of our love, angels are crying from up above. Turned the bedroom into a fair (a fair!) It’s in the palm of your hand now baby.
+
+ `
+ });
+
+export const withImage = () =>
+ defineComponent({
+ template: `
+
+
+ Hello World!
+ This is the subtitle
+ Black ray-bans, you know she's with the band. Such a sight to see and it's all for me. Heaven is jealous of our love, angels are crying from up above. Turned the bedroom into a fair (a fair!) It’s in the palm of your hand now baby.
+
+ `
+ });
+
+export const withActions = () =>
+ defineComponent({
+ template: `
+
+ Hello World!
+ This is the subtitle
+ Black ray-bans, you know she's with the band. Such a sight to see and it's all for me. Heaven is jealous of our love, angels are crying from up above. Turned the bedroom into a fair (a fair!) It’s in the palm of your hand now baby.
+
+ Click me
+ Click me
+
+
+ `
+ });
+
+export const asDialog = () =>
+ defineComponent({
+ setup() {
+ const active = ref(false);
+ return { active };
+ },
+ template: `
+
+
+ Show dialog
+
+
+ Are you sure you want to quit?
+ All unsaved changes will be lost.
+
+ Cancel
+ Quit
+
+
+
+ `
+ });
diff --git a/src/components/v-card/v-card.test.ts b/src/components/v-card/v-card.test.ts
new file mode 100644
index 0000000000..e2ad1604fd
--- /dev/null
+++ b/src/components/v-card/v-card.test.ts
@@ -0,0 +1,13 @@
+import VCard from './v-card.vue';
+import VueCompositionAPI from '@vue/composition-api';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+const localVue = createLocalVue();
+localVue.use(VueCompositionAPI);
+
+describe('Components / Card', () => {
+ it('Renders', () => {
+ const component = shallowMount(VCard, { localVue });
+ expect(component.isVueInstance()).toBe(true);
+ });
+});
diff --git a/src/components/v-card/v-card.vue b/src/components/v-card/v-card.vue
new file mode 100644
index 0000000000..2f202d58bb
--- /dev/null
+++ b/src/components/v-card/v-card.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/v-dialog/v-dialog.vue b/src/components/v-dialog/v-dialog.vue
index cf209a323d..56e911408d 100644
--- a/src/components/v-dialog/v-dialog.vue
+++ b/src/components/v-dialog/v-dialog.vue
@@ -68,6 +68,11 @@ export default defineComponent({
transition: opacity var(--medium) var(--transition);
pointer-events: none;
+ .v-card {
+ --v-card-min-width: 400px;
+ --v-card-padding: 24px;
+ }
+
.v-sheet {
--v-sheet-padding: 24px;
}
diff --git a/src/styles/mixins/type-styles.scss b/src/styles/mixins/type-styles.scss
index 81768cb38e..524708e954 100644
--- a/src/styles/mixins/type-styles.scss
+++ b/src/styles/mixins/type-styles.scss
@@ -76,3 +76,26 @@
font-family: var(--family-sans-serif);
line-height: 26px;
}
+
+@mixin type-card-title {
+ color: var(--heading-text-color);
+ font-weight: 500;
+ font-size: 1.25rem;
+ line-height: 2rem;
+ letter-spacing: 0.0125em;
+}
+
+@mixin type-card-subtitle {
+ color: var(--foreground-color-secondary);
+ font-weight: 400;
+ font-size: 0.875rem;
+ line-height: 1.375rem;
+ letter-spacing: 0.007em;
+}
+
+@mixin type-card-text {
+ font-weight: 400;
+ font-size: 0.875rem;
+ line-height: 1.375rem;
+ letter-spacing: 0.007em;
+}
diff --git a/src/views/private-view/drawer-detail/drawer-detail.test.ts b/src/views/private-view/drawer-detail/drawer-detail.test.ts
index e774d33c1e..6ffcc125eb 100644
--- a/src/views/private-view/drawer-detail/drawer-detail.test.ts
+++ b/src/views/private-view/drawer-detail/drawer-detail.test.ts
@@ -3,10 +3,12 @@ import VueCompositionAPI, { ref } from '@vue/composition-api';
import DrawerDetail from './drawer-detail.vue';
import * as GroupableComposition from '@/compositions/groupable/groupable';
import VIcon from '@/components/v-icon';
+import TransitionExpand from '@/components/transition/expand';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-icon', VIcon);
+localVue.component('transition-expand', TransitionExpand);
describe('Drawer Detail', () => {
it('Uses the useGroupable composition', () => {