import { INodeTypeData, INodeTypeNameVersion, LoggerProxy } from 'n8n-workflow';
import * as Db from '@/Db';
import { InstalledNodes } from '@db/entities/InstalledNodes';
import { InstalledPackages } from '@db/entities/InstalledPackages';

export async function findInstalledPackage(
	packageName: string,
): Promise<InstalledPackages | undefined> {
	return Db.collections.InstalledPackages.findOne(packageName, { relations: ['installedNodes'] });
}

export async function isPackageInstalled(packageName: string): Promise<boolean> {
	const installedPackage = await findInstalledPackage(packageName);
	return installedPackage !== undefined;
}

export async function getAllInstalledPackages(): Promise<InstalledPackages[]> {
	return Db.collections.InstalledPackages.find({ relations: ['installedNodes'] });
}

export async function removePackageFromDatabase(
	packageName: InstalledPackages,
): Promise<InstalledPackages> {
	return Db.collections.InstalledPackages.remove(packageName);
}

export async function persistInstalledPackageData(
	installedPackageName: string,
	installedPackageVersion: string,
	installedNodes: INodeTypeNameVersion[],
	loadedNodeTypes: INodeTypeData,
	authorName?: string,
	authorEmail?: string,
): Promise<InstalledPackages> {
	let installedPackage: InstalledPackages;

	try {
		await Db.transaction(async (transactionManager) => {
			const promises = [];

			const installedPackagePayload = Object.assign(new InstalledPackages(), {
				packageName: installedPackageName,
				installedVersion: installedPackageVersion,
				authorName,
				authorEmail,
			});
			installedPackage = await transactionManager.save<InstalledPackages>(installedPackagePayload);
			installedPackage.installedNodes = [];

			promises.push(
				...installedNodes.map(async (loadedNode) => {
					const installedNodePayload = Object.assign(new InstalledNodes(), {
						name: loadedNodeTypes[loadedNode.name].type.description.displayName,
						type: loadedNode.name,
						latestVersion: loadedNode.version,
						package: installedPackageName,
					});
					installedPackage.installedNodes.push(installedNodePayload);
					return transactionManager.save<InstalledNodes>(installedNodePayload);
				}),
			);

			return promises;
		});
		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
		return installedPackage!;
	} catch (error) {
		LoggerProxy.error('Failed to save installed packages and nodes', {
			// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
			error,
			packageName: installedPackageName,
		});
		throw error;
	}
}


keep keep keep keep keep replace replace replace replace replace replace replace replace replace replace replace replace replace keep keep keep keep keep

import { INodeProperties } from 'n8n-workflow';

export const cardOperations: INodeProperties[] = [
	// ----------------------------------
	//         card
	// ----------------------------------
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		noDataExpression: true,
		displayOptions: {
			show: {
				resource: ['card'],
			},
		},
		options: [
			{
				name: 'Create',
				value: 'create',
				description: 'Create a new card',
				action: 'Create a card',
			},
			{
				name: 'Delete',
				value: 'delete',
				description: 'Delete a card',
				action: 'Delete a card',
			},
			{
				name: 'Get',
				value: 'get',
				description: 'Get a card',
				action: 'Get a card',
			},
			{
				name: 'Get Many',
				value: 'getAll',
				description: 'Get many cards',
				action: 'Get many cards',
			},
			{
				name: 'Update',
				value: 'update',
				description: 'Update a card',
				action: 'Update a card',
			},
		],
		default: 'create',
	},
];

export const cardFields: INodeProperties[] = [
	// ----------------------------------
	//         card:create
	// ----------------------------------
	{
		displayName: 'Board Name or ID',
		name: 'boardId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getBoards',
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['create'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the board that list belongs to. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'List Name or ID',
		name: 'listId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getLists',
			loadOptionsDependsOn: ['boardId'],
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['create'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the list to create card in. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'Title',
		name: 'title',
		type: 'string',
		default: '',
		placeholder: 'My card',
		required: true,
		displayOptions: {
			show: {
				operation: ['create'],
				resource: ['card'],
			},
		},
		description: 'The title of the card',
	},
	{
		displayName: 'Swimlane Name or ID',
		name: 'swimlaneId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getSwimlanes',
			loadOptionsDependsOn: ['boardId'],
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['create'],
				resource: ['card'],
			},
		},
		description:
			'The swimlane ID of the new card. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'Author Name or ID',
		name: 'authorId',
		type: 'options',
		description:
			'Choose from the list, or specify an ID using an expression',
		typeOptions: {
			loadOptionsMethod: 'getUsers',
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['create'],
				resource: ['card'],
			},
		},
	},
	{
		displayName: 'Additional Fields',
		name: 'additionalFields',
		type: 'collection',
		placeholder: 'Add Field',
		displayOptions: {
			show: {
				operation: ['create'],
				resource: ['card'],
			},
		},
		default: {},
		options: [
			{
				displayName: 'Assignee Names or IDs',
				name: 'assignees',
				type: 'multiOptions',
				typeOptions: {
					loadOptionsMethod: 'getUsers',
				},
				default: [],
				description:
					'The new list of assignee IDs attached to the card. Choose from the list, or specify IDs using an expression.',
			},
			{
				displayName: 'Description',
				name: 'description',
				type: 'string',
				default: '',
				description: 'The new description of the card',
			},
			{
				displayName: 'Member Names or IDs',
				name: 'members',
				type: 'multiOptions',
				typeOptions: {
					loadOptionsMethod: 'getUsers',
				},
				default: [],
				description:
					'The new list of member IDs attached to the card. Choose from the list, or specify IDs using an expression.',
			},
		],
	},

	// ----------------------------------
	//         card:delete
	// ----------------------------------
	{
		displayName: 'Board Name or ID',
		name: 'boardId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getBoards',
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['delete'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the board that list belongs to. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'List Name or ID',
		name: 'listId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getLists',
			loadOptionsDependsOn: ['boardId'],
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['delete'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the list that card belongs to. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'Card Name or ID',
		name: 'cardId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getCards',
			loadOptionsDependsOn: ['boardId', 'listId'],
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['delete'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the card to delete. Choose from the list, or specify an ID using an expression.',
	},

	// ----------------------------------
	//         card:get
	// ----------------------------------
	{
		displayName: 'Board Name or ID',
		name: 'boardId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getBoards',
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['get'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the board that list belongs to. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'List Name or ID',
		name: 'listId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getLists',
			loadOptionsDependsOn: ['boardId'],
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['get'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the list that card belongs to. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'Card ID',
		name: 'cardId',
		type: 'string',
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['get'],
				resource: ['card'],
			},
		},
		description: 'The ID of the card to get',
	},

	// ----------------------------------
	//         card:getAll
	// ----------------------------------
	{
		displayName: 'Board Name or ID',
		name: 'boardId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getBoards',
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['getAll'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the board that list belongs to. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'From Object',
		name: 'fromObject',
		type: 'options',
		required: true,
		displayOptions: {
			show: {
				operation: ['getAll'],
				resource: ['card'],
			},
		},
		options: [
			{
				name: 'List',
				value: 'list',
			},
			{
				name: 'Swimlane',
				value: 'swimlane',
			},
		],
		default: '',
	},
	{
		displayName: 'List Name or ID',
		name: 'listId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getLists',
			loadOptionsDependsOn: ['boardId'],
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				fromObject: ['list'],
				operation: ['getAll'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the list that card belongs to. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'Swimlane Name or ID',
		name: 'swimlaneId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getSwimlanes',
			loadOptionsDependsOn: ['boardId'],
		},
		default: '',
		displayOptions: {
			show: {
				fromObject: ['swimlane'],
				operation: ['getAll'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the swimlane that card belongs to. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		displayOptions: {
			show: {
				operation: ['getAll'],
				resource: ['card'],
			},
		},
		default: false,
		description: 'Whether to return all results or only up to a given limit',
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		displayOptions: {
			show: {
				operation: ['getAll'],
				resource: ['card'],
				returnAll: [false],
			},
		},
		typeOptions: {
			minValue: 1,
			maxValue: 200,
		},
		default: 100,
		description: 'Max number of results to return',
	},

	// ----------------------------------
	//         card:update
	// ----------------------------------
	{
		displayName: 'Board Name or ID',
		name: 'boardId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getBoards',
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['update'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the board that list belongs to. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'List Name or ID',
		name: 'listId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getLists',
			loadOptionsDependsOn: ['boardId'],
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['update'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the list that card belongs to. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'Card Name or ID',
		name: 'cardId',
		type: 'options',
		typeOptions: {
			loadOptionsMethod: 'getCards',
			loadOptionsDependsOn: ['boardId', 'listId'],
		},
		default: '',
		required: true,
		displayOptions: {
			show: {
				operation: ['update'],
				resource: ['card'],
			},
		},
		description:
			'The ID of the card to update. Choose from the list, or specify an ID using an expression.',
	},
	{
		displayName: 'Update Fields',
		name: 'updateFields',
		type: 'collection',
		placeholder: 'Add Field',
		displayOptions: {
			show: {
				operation: ['update'],
				resource: ['card'],
			},
		},
		default: {},
		options: [
			{
				displayName: 'Author Name or ID',
				name: 'authorId',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getUsers',
				},
				default: '',
				description:
					'Update the owner of the card. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Assignee Names or IDs',
				name: 'assignees',
				type: 'multiOptions',
				typeOptions: {
					loadOptionsMethod: 'getUsers',
				},
				default: [],
				description:
					'The new list of assignee IDs attached to the card. Choose from the list, or specify IDs using an expression.',
			},
			{
				displayName: 'Color',
				name: 'color',
				type: 'options',
				options: [
					{
						value: 'black',
						name: 'Black',
					},
					{
						value: 'blue',
						name: 'Blue',
					},
					{
						value: 'crimson',
						name: 'Crimson',
					},
					{
						value: 'darkgreen',
						name: 'Darkgreen',
					},
					{
						value: 'gold',
						name: 'Gold',
					},
					{
						value: 'gray',
						name: 'Gray',
					},
					{
						value: 'green',
						name: 'Green',
					},
					{
						value: 'indigo',
						name: 'Indigo',
					},
					{
						value: 'lime',
						name: 'Lime',
					},
					{
						value: 'magenta',
						name: 'Magenta',
					},
					{
						value: 'mistyrose',
						name: 'Mistyrose',
					},
					{
						value: 'navy',
						name: 'Navy',
					},
					{
						value: 'orange',
						name: 'Orange',
					},
					{
						value: 'paleturquoise',
						name: 'Paleturquoise',
					},
					{
						value: 'peachpuff',
						name: 'Peachpuff',
					},
					{
						value: 'pink',
						name: 'Pink',
					},
					{
						value: 'plum',
						name: 'Plum',
					},
					{
						value: 'purple',
						name: 'Purple',
					},
					{
						value: 'red',
						name: 'Red',
					},
					{
						value: 'saddlebrown',
						name: 'Saddlebrown',
					},
					{
						value: 'silver',
						name: 'Silver',
					},
					{
						value: 'sky',
						name: 'Sky',
					},
					{
						value: 'slateblue',
						name: 'Slateblue',
					},
					{
						value: 'white',
						name: 'White',
					},
					{
						value: 'yellow',
						name: 'Yellow',
					},
				],
				default: '',
				description: 'The new color of the card',
			},
			{
				displayName: 'Description',
				name: 'description',
				type: 'string',
				default: '',
				description: 'The new description of the card',
			},
			{
				displayName: 'Due At',
				name: 'dueAt',
				type: 'dateTime',
				default: '',
				description: 'The new due at field of the card',
			},
			{
				displayName: 'End At',
				name: 'endAt',
				type: 'dateTime',
				default: '',
				description: 'The new end at field of the card',
			},
			{
				displayName: 'Label IDs',
				name: 'labelIds',
				type: 'string',
				default: '',
				description: 'The label IDs attached to the card',
			},
			{
				displayName: 'List Name or ID',
				name: 'listId',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getLists',
					loadOptionsDependsOn: ['boardId'],
				},
				default: '',
				description:
					'The new list ID of the card (move operation). Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Member Names or IDs',
				name: 'members',
				type: 'multiOptions',
				typeOptions: {
					loadOptionsMethod: 'getUsers',
				},
				default: [],
				description:
					'The new list of member IDs attached to the card. Choose from the list, or specify IDs using an expression.',
			},
			{
				displayName: 'Over Time',
				name: 'isOverTime',
				type: 'boolean',
				default: false,
				// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
				description: 'The new over time field of the card',
			},
			{
				displayName: 'Parent Name or ID',
				name: 'parentId',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getCards',
					loadOptionsDependsOn: ['boardId', 'listId'],
				},
				default: '',
				description:
					'The parent of the card. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Received At',
				name: 'receivedAt',
				type: 'dateTime',
				default: '',
				description: 'The new received at field of the card',
			},
			{
				displayName: 'Sort',
				name: 'sort',
				type: 'number',
				default: 0,
				description: 'The internally used sort value of a card',
			},
			{
				displayName: 'Spent Time',
				name: 'spentTime',
				type: 'number',
				typeOptions: {
					minValue: 0,
				},
				default: '',
				description: 'The new spent time field of the card',
			},
			{
				displayName: 'Start At',
				name: 'startAt',
				type: 'dateTime',
				default: '',
				description: 'The new start at field of the card',
			},
			{
				displayName: 'Swimlane Name or ID',
				name: 'swimlaneId',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getSwimlanes',
					loadOptionsDependsOn: ['boardId'],
				},
				default: '',
				description:
					'The new swimlane ID of the card. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Title',
				name: 'title',
				type: 'string',
				default: '',
				description: 'The new title of the card',
			},
		],
	},
];


"lint:fix": "vue-cli-service lint && yarn fix:md",

"build:all": "yarn build:client && yarn build:server && yarn prerender",

"build:server": "cross-env WEBPACK_TARGET=node yarn build --no-clean",

"fix:md": "markdownlint --config .markdownlintrc src/pages/en --fix",

"lint:md": "markdownlint --config .markdownlintrc src/pages/en",

"prod": "http-server dist --proxy http://localhost:8080/?"

keep keep keep keep replace keep keep keep keep keep

"build:server": "cross-env VUE_APP_WEBPACK_TARGET=node yarn build --no-clean",

// Vue
import Vue from 'vue'
import Vuex from 'vuex'
import pathify from '@/plugins/vuex-pathify'
import pwaEvents from '@/plugins/pwa-events'

// Modules
import * as modules from './modules'

// Globals
import { IN_BROWSER, IS_SERVER } from '@/util/globals'

Vue.use(Vuex)

export function createStore () {
  const store = new Vuex.Store({
    modules,
    plugins: [
      pathify.plugin,
      pwaEvents,
    ],
  })

  store.subscribe(mutation => {
    if (!mutation.type.startsWith('user/')) return

    store.dispatch('user/update', mutation)
  })

  if (!IS_SERVER && IN_BROWSER) {
    store.dispatch('app/init')
  }

  return store
}

export const ROOT_DISPATCH = Object.freeze({ root: true })


<template>
  <v-data-table
    item-key="name"
    class="elevation-1"
    loading
    loading-text="Loading... Please wait"
  ></v-data-table>
</template>


@import '../tools/_functions.sass';

$utilities: () !default;
$utilities: map-deep-merge(
  (
    // Display utilities
    "overflow": (
      property: overflow,
      values: auto hidden visible,
    ),
    "overflow-x": (
      property: overflow-x,
      values: auto hidden
    ),
    "overflow-y": (
      property: overflow-y,
      values: auto hidden
    ),
    "display": (
      responsive: true,
      print: true,
      property: display,
      class: d,
      values: none inline inline-block block table table-row table-cell flex inline-flex
    ),
    "float": (
      responsive: true,
      print: true,
      property: float,
      class: float,
      values: none left right
    ),

    // Flex utilities
    "flex": (
      responsive: true,
      property: flex,
      values: (fill: 1 1 auto)
    ),
    "flex-direction": (
      responsive: true,
      property: flex-direction,
      class: flex,
      values: row column row-reverse column-reverse
    ),
    "flex-grow": (
      responsive: true,
      property: flex-grow,
      class: flex,
      values: (
        grow-0: 0,
        grow-1: 1,
      )
    ),
    "flex-shrink": (
      responsive: true,
      property: flex-shrink,
      class: flex,
      values: (
        shrink-0: 0,
        shrink-1: 1,
      )
    ),
    "flex-wrap": (
      responsive: true,
      property: flex-wrap,
      class: flex,
      values: wrap nowrap wrap-reverse
    ),
    "justify-content": (
      responsive: true,
      property: justify-content,
      class: justify,
      values: (
        start: flex-start,
        end: flex-end,
        center: center,
        space-between: space-between,
        space-around: space-around,
      )
    ),
    "align-items": (
      responsive: true,
      property: align-items,
      class: align,
      values: (
        start: flex-start,
        end: flex-end,
        center: center,
        baseline: baseline,
        stretch: stretch,
      )
    ),
    "align-content": (
      responsive: true,
      property: align-content,
      values: (
        start: flex-start,
        end: flex-end,
        center: center,
        space-between: space-between,
        space-around: space-around,
        stretch: stretch,
      )
    ),
    "align-self": (
      responsive: true,
      property: align-self,
      values: (
        auto: auto,
        start: flex-start,
        end: flex-end,
        center: center,
        baseline: baseline,
        stretch: stretch,
      )
    ),
    "order": (
      responsive: true,
      property: order,
      values: (
        first: -1,
        0: 0,
        1: 1,
        2: 2,
        3: 3,
        4: 4,
        5: 5,
        6: 6,
        7: 7,
        8: 8,
        9: 9,
        10: 10,
        11: 11,
        12: 12,
        last: 13,
      ),
    ),

    // Margin utilities
    "margin": (
      responsive: true,
      property: margin,
      class: ma,
      values: map-merge($spacers, (auto: auto))
    ),
    "margin-x": (
      responsive: true,
      property: margin-right margin-left,
      class: mx,
      values: map-merge($spacers, (auto: auto))
    ),
    "margin-y": (
      responsive: true,
      property: margin-top margin-bottom,
      class: my,
      values: map-merge($spacers, (auto: auto))
    ),
    "margin-top": (
      responsive: true,
      property: margin-top,
      class: mt,
      values: map-merge($spacers, (auto: auto))
    ),
    "margin-right": (
      responsive: true,
      property: margin-right,
      class: mr,
      values: map-merge($spacers, (auto: auto))
    ),
    "margin-bottom": (
      responsive: true,
      property: margin-bottom,
      class: mb,
      values: map-merge($spacers, (auto: auto))
    ),
    "margin-left": (
      responsive: true,
      property: margin-left,
      class: ml,
      values: map-merge($spacers, (auto: auto))
    ),
    "margin-start": (
      responsive: true,
      property: (ltr: margin-left, rtl: margin-right),
      class: ms,
      values: map-merge($spacers, (auto: auto))
    ),
    "margin-end": (
      responsive: true,
      property: (ltr: margin-right, rtl: margin-left),
      class: me,
      values: map-merge($spacers, (auto: auto))
    ),

    // Negative margin utilities
    "negative-margin": (
      responsive: true,
      property: margin,
      class: ma,
      values: $negative-spacers
    ),
    "negative-margin-x": (
      responsive: true,
      property: margin-right margin-left,
      class: mx,
      values: $negative-spacers
    ),
    "negative-margin-y": (
      responsive: true,
      property: margin-top margin-bottom,
      class: my,
      values: $negative-spacers
    ),
    "negative-margin-top": (
      responsive: true,
      property: margin-top,
      class: mt,
      values: $negative-spacers
    ),
    "negative-margin-right": (
      responsive: true,
      property: margin-right,
      class: mr,
      values: $negative-spacers
    ),
    "negative-margin-bottom": (
      responsive: true,
      property: margin-bottom,
      class: mb,
      values: $negative-spacers
    ),
    "negative-margin-left": (
      responsive: true,
      property: margin-left,
      class: ml,
      values: $negative-spacers
    ),
    "negative-margin-start": (
      responsive: true,
      property: (ltr: margin-left, rtl: margin-right),
      class: ms,
      values: $negative-spacers
    ),
    "negative-margin-end": (
      responsive: true,
      property: (ltr: margin-right, rtl: margin-left),
      class: me,
      values: $negative-spacers
    ),

    // Padding utilities
    "padding": (
      responsive: true,
      property: padding,
      class: pa,
      values: $spacers
    ),
    "padding-x": (
      responsive: true,
      property: padding-right padding-left,
      class: px,
      values: $spacers
    ),
    "padding-y": (
      responsive: true,
      property: padding-top padding-bottom,
      class: py,
      values: $spacers
    ),
    "padding-top": (
      responsive: true,
      property: padding-top,
      class: pt,
      values: $spacers
    ),
    "padding-right": (
      responsive: true,
      property: padding-right,
      class: pr,
      values: $spacers
    ),
    "padding-bottom": (
      responsive: true,
      property: padding-bottom,
      class: pb,
      values: $spacers
    ),
    "padding-left": (
      responsive: true,
      property: padding-left,
      class: pl,
      values: $spacers
    ),
    "padding-start": (
      responsive: true,
      property: (ltr: padding-left, rtl: padding-right),
      class: ps,
      values: $spacers
    ),
    "padding-end": (
      responsive: true,
      property: (ltr: padding-right, rtl: padding-left),
      class: pe,
      values: $spacers
    ),

    // Border
    "rounded": (
      property: border-radius,
      class: rounded,
      values: $rounded
    ),
    "rounded-top": (
      property: border-top-left-radius border-top-right-radius,
      class: rounded-t,
      values: $rounded
    ),
    "rounded-right": (
      property: border-top-right-radius border-bottom-right-radius,
      class: rounded-r,
      values: $rounded
    ),
    "rounded-bottom": (
      property: border-bottom-left-radius border-bottom-right-radius,
      class: rounded-b,
      values: $rounded
    ),
    "rounded-left": (
      property: border-top-left-radius border-bottom-left-radius,
      class: rounded-l,
      values: $rounded
    ),
    "rounded-top-left": (
      property: border-top-left-radius,
      class: rounded-tl,
      values: $rounded
    ),
    "rounded-top-right": (
      property: border-top-right-radius,
      class: rounded-tr,
      values: $rounded
    ),
    "rounded-bottom-right": (
      property: border-bottom-right-radius,
      class: rounded-br,
      values: $rounded
    ),
    "rounded-bottom-left": (
      property: border-bottom-left-radius,
      class: rounded-bl,
      values: $rounded
    ),

    // Text
    "text-align": (
      responsive: true,
      property: text-align,
      class: text,
      values: left right center justify start end
    ),
    "text-decoration": (
      property: text-decoration,
      class: text-decoration,
      values: line-through none overline underline
    ),
    "white-space": (
      property: white-space,
      class: text,
      values: (
        wrap: normal,
        no-wrap: nowrap,
      )
    ),
    "overflow-wrap": (
      property: overflow-wrap word-break, // word-break for IE & < Edge 18
      class: text,
      values: (break: break-word)
    ),
    "text-overflow": (
      property: white-space overflow text-overflow,
      class: text,
      values: (truncate: nowrap hidden ellipsis)
    ),
    "text-transform": (
      property: text-transform,
      class: text,
      values: none capitalize lowercase uppercase
    ),
    "typography": (
      responsive: true,
      property: (
        font-size,
        font-weight,
        line-height,
        letter-spacing,
        font-family,
        text-transform
      ),
      class: text,
      unimportant: (
        font-weight,
        line-height
      ),
      values: $typography
    )
  ),
  $utilities
);


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`theme-utilities.ts should generate css vars 1`] = `
":root {
  --v-anchor-base: #c42742;
  --v-primary-base: #c42742;
  --v-primary-lighten5: #2c0447;
  --v-primary-lighten4: #cfa854;
  --v-primary-lighten3: #dd88cc;
  --v-primary-lighten2: #b49921;
  --v-primary-lighten1: #f899c7;
  --v-primary-darken1: #0169dc;
  --v-primary-darken2: #c28fd0;
  --v-primary-darken3: #fbe002;
  --v-primary-darken4: #33303a;
}

.v-application a { color: var(--v-anchor-base); }
.v-application .primary {
  background-color: var(--v-primary-base) !important;
  border-color: var(--v-primary-base) !important;
}
.v-application .primary--text {
  color: var(--v-primary-base) !important;
  caret-color: var(--v-primary-base) !important;
}
.v-application .primary.lighten-5 {
  background-color: var(--v-primary-lighten5) !important;
  border-color: var(--v-primary-lighten5) !important;
}
.v-application .primary--text.text--lighten-5 {
  color: var(--v-primary-lighten5) !important;
  caret-color: var(--v-primary-lighten5) !important;
}
.v-application .primary.lighten-4 {
  background-color: var(--v-primary-lighten4) !important;
  border-color: var(--v-primary-lighten4) !important;
}
.v-application .primary--text.text--lighten-4 {
  color: var(--v-primary-lighten4) !important;
  caret-color: var(--v-primary-lighten4) !important;
}
.v-application .primary.lighten-3 {
  background-color: var(--v-primary-lighten3) !important;
  border-color: var(--v-primary-lighten3) !important;
}
.v-application .primary--text.text--lighten-3 {
  color: var(--v-primary-lighten3) !important;
  caret-color: var(--v-primary-lighten3) !important;
}
.v-application .primary.lighten-2 {
  background-color: var(--v-primary-lighten2) !important;
  border-color: var(--v-primary-lighten2) !important;
}
.v-application .primary--text.text--lighten-2 {
  color: var(--v-primary-lighten2) !important;
  caret-color: var(--v-primary-lighten2) !important;
}
.v-application .primary.lighten-1 {
  background-color: var(--v-primary-lighten1) !important;
  border-color: var(--v-primary-lighten1) !important;
}
.v-application .primary--text.text--lighten-1 {
  color: var(--v-primary-lighten1) !important;
  caret-color: var(--v-primary-lighten1) !important;
}
.v-application .primary.darken-1 {
  background-color: var(--v-primary-darken1) !important;
  border-color: var(--v-primary-darken1) !important;
}
.v-application .primary--text.text--darken-1 {
  color: var(--v-primary-darken1) !important;
  caret-color: var(--v-primary-darken1) !important;
}
.v-application .primary.darken-2 {
  background-color: var(--v-primary-darken2) !important;
  border-color: var(--v-primary-darken2) !important;
}
.v-application .primary--text.text--darken-2 {
  color: var(--v-primary-darken2) !important;
  caret-color: var(--v-primary-darken2) !important;
}
.v-application .primary.darken-3 {
  background-color: var(--v-primary-darken3) !important;
  border-color: var(--v-primary-darken3) !important;
}
.v-application .primary--text.text--darken-3 {
  color: var(--v-primary-darken3) !important;
  caret-color: var(--v-primary-darken3) !important;
}
.v-application .primary.darken-4 {
  background-color: var(--v-primary-darken4) !important;
  border-color: var(--v-primary-darken4) !important;
}
.v-application .primary--text.text--darken-4 {
  color: var(--v-primary-darken4) !important;
  caret-color: var(--v-primary-darken4) !important;
}"
`;

exports[`theme-utilities.ts should generate styles 1`] = `
".v-application a { color: #c42742; }
.v-application .primary {
  background-color: #c42742 !important;
  border-color: #c42742 !important;
}
.v-application .primary--text {
  color: #c42742 !important;
  caret-color: #c42742 !important;
}
.v-application .primary.lighten-5 {
  background-color: #2c0447 !important;
  border-color: #2c0447 !important;
}
.v-application .primary--text.text--lighten-5 {
  color: #2c0447 !important;
  caret-color: #2c0447 !important;
}
.v-application .primary.lighten-4 {
  background-color: #cfa854 !important;
  border-color: #cfa854 !important;
}
.v-application .primary--text.text--lighten-4 {
  color: #cfa854 !important;
  caret-color: #cfa854 !important;
}
.v-application .primary.lighten-3 {
  background-color: #dd88cc !important;
  border-color: #dd88cc !important;
}
.v-application .primary--text.text--lighten-3 {
  color: #dd88cc !important;
  caret-color: #dd88cc !important;
}
.v-application .primary.lighten-2 {
  background-color: #b49921 !important;
  border-color: #b49921 !important;
}
.v-application .primary--text.text--lighten-2 {
  color: #b49921 !important;
  caret-color: #b49921 !important;
}
.v-application .primary.lighten-1 {
  background-color: #f899c7 !important;
  border-color: #f899c7 !important;
}
.v-application .primary--text.text--lighten-1 {
  color: #f899c7 !important;
  caret-color: #f899c7 !important;
}
.v-application .primary.darken-1 {
  background-color: #0169dc !important;
  border-color: #0169dc !important;
}
.v-application .primary--text.text--darken-1 {
  color: #0169dc !important;
  caret-color: #0169dc !important;
}
.v-application .primary.darken-2 {
  background-color: #c28fd0 !important;
  border-color: #c28fd0 !important;
}
.v-application .primary--text.text--darken-2 {
  color: #c28fd0 !important;
  caret-color: #c28fd0 !important;
}
.v-application .primary.darken-3 {
  background-color: #fbe002 !important;
  border-color: #fbe002 !important;
}
.v-application .primary--text.text--darken-3 {
  color: #fbe002 !important;
  caret-color: #fbe002 !important;
}
.v-application .primary.darken-4 {
  background-color: #33303a !important;
  border-color: #33303a !important;
}
.v-application .primary--text.text--darken-4 {
  color: #33303a !important;
  caret-color: #33303a !important;
}"
`;

exports[`theme-utilities.ts should parse a theme or theme item 1`] = `
Object {
  "anchor": "#000000",
  "primary": Object {
    "base": "#000000",
    "darken1": "#000000",
    "darken2": "#000000",
    "darken3": "#000000",
    "darken4": "#000000",
    "lighten1": "#1b1b1b",
    "lighten2": "#303030",
    "lighten3": "#474747",
    "lighten4": "#5e5e5e",
    "lighten5": "#777777",
  },
  "secondary": Object {
    "base": "#ffffff",
    "darken1": "#e2e2e2",
    "darken2": "#c6c6c6",
    "darken3": "#ababab",
    "darken4": "#919191",
    "lighten1": "#ffffff",
    "lighten2": "#ffffff",
    "lighten3": "#ffffff",
    "lighten4": "#ffffff",
    "lighten5": "#ffffff",
  },
}
`;

exports[`theme-utilities.ts should parse a theme or theme item 2`] = `
Object {
  "anchor": "#c42742",
  "primary": Object {
    "base": "#c42742",
    "darken1": "#0169dc",
    "darken2": "#c28fd0",
    "darken3": "#fbe002",
    "darken4": "#33303a",
    "lighten1": "#f899c7",
    "lighten2": "#b49921",
    "lighten3": "#dd88cc",
    "lighten4": "#cfa854",
    "lighten5": "#2c0447",
  },
}
`;


import { IS_SERVER } from '@/util/globals'

// Globals
import { IN_BROWSER, IS_SERVER } from '@/util/globals'

export async function useMeta (app) {
  if (IS_SERVER || !IN_BROWSER) return

  const Meta = await import('vue-meta')

  app.use(Meta)
}


---
meta:
  title: Bottom sheet component
  description: The bottom sheet component is used for elevating content above other elements in a dialog style fashion.
  keywords: bottom sheets, vuetify bottom sheet component, vue bottom sheet component
related:
  - /components/dialogs/
  - /components/lists/
  - /components/menus/
---

# Bottom sheets

The bottom sheet is a modified `v-dialog` that slides from the bottom of the screen, similar to a `v-bottom-navigation`. Whereas a bottom navigation component is for buttons and specific application level actions, a bottom sheet can contain anything.

<entry-ad />

## Usage

Here we display an example list of actions that could be present in an application.

<usage name="v-bottom-sheet" />

## API

- [v-bottom-sheet](/api/v-bottom-sheet)

## Examples

### Props

#### Inset

Bottom sheets can be inset, reducing their maximum width on desktop to 70%. This can be further reduced manually using the **width** prop.

<example file="v-bottom-sheet/prop-inset" />

#### Model

Bottom sheets can be controlled using **v-model**. You can use it to close them or if you can't use `activator` slot.

<example file="v-bottom-sheet/prop-model" />

#### Persistent

Persistent bottom sheets can't be closed by clicking outside them.

<example file="v-bottom-sheet/prop-persistent" />

### Misc

#### Music Player

Using a inset bottom sheet, you can make practical components such as this simple music player.

<example file="v-bottom-sheet/misc-player" />

#### Open In List

By combining a functional list into a bottom sheet, you can create a simple 'open in' component.

<example file="v-bottom-sheet/misc-open-in-list" />

<backmatter />


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VDataTableHeader.ts desktop should render 1`] = `

        Dessert (100g serving)

        Calories

        $sort

        Fat (g)

        $sort

        Carbs (g)

        $sort

        Protein (g)

        $sort

        Iron (%)

        $sort

`;

exports[`VDataTableHeader.ts desktop should work with multiSort 1`] = `

        Dessert (100g serving)

        Calories

        $sort

        Fat (g)

        $sort

        Carbs (g)

        $sort

        Protein (g)

        $sort

        Iron (%)

        $sort

        1

`;

exports[`VDataTableHeader.ts desktop should work with showGroupBy 1`] = `

        Dessert (100g serving)

        group

        Calories

        $sort

        group

        Fat (g)

        $sort

        group

        Carbs (g)

        $sort

        group

        Protein (g)

        $sort

        group

        Iron (%)

        $sort

        group

`;

exports[`VDataTableHeader.ts desktop should work with sortBy correctly 1`] = `

        Dessert (100g serving)

        Calories

        $sort

        Fat (g)

        $sort

        Carbs (g)

        $sort

        Protein (g)

        $sort

        Iron (%)

        $sort

`;

exports[`VDataTableHeader.ts desktop should work with sortDesc correctly 1`] = `

        Dessert (100g serving)

        Calories

        $sort

        Fat (g)

        $sort

        Carbs (g)

        $sort

        Protein (g)

        $sort

        Iron (%)

        $sort

`;

exports[`VDataTableHeader.ts mobile should render 1`] = `

                  Sort by

                  <input id="input-37"
                         readonly="readonly"
                         type="text"
                         aria-readonly="false"
                         autocomplete="off"
                  >

                      $dropdown

                <input type="hidden">

`;

exports[`VDataTableHeader.ts mobile should render with data-table-select header 1`] = `

              $checkboxOff

                  Sort by

                  <input id="input-69"
                         readonly="readonly"
                         type="text"
                         aria-readonly="false"
                         autocomplete="off"
                  >

                      $dropdown

                <input type="hidden">

`;

exports[`VDataTableHeader.ts mobile should work with multiSort 1`] = `

                  Sort by

                      Iron (%)

                          $sort

                  <input id="input-47"
                         readonly="readonly"
                         type="text"
                         aria-readonly="false"
                         autocomplete="off"
                  >

                      $dropdown

                <input type="hidden"
                       value="iron"
                >

`;

exports[`VDataTableHeader.ts mobile should work with showGroupBy 1`] = `

                  Sort by

                  <input id="input-42"
                         readonly="readonly"
                         type="text"
                         aria-readonly="false"
                         autocomplete="off"
                  >

                      $dropdown

                <input type="hidden">

`;

exports[`VDataTableHeader.ts mobile should work with sortBy correctly 1`] = `

                  Sort by

                      Iron (%)

                          $sort

                  <input id="input-54"
                         readonly="readonly"
                         type="text"
                         aria-readonly="false"
                         autocomplete="off"
                  >

                      $dropdown

                <input type="hidden"
                       value="iron"
                >

`;

exports[`VDataTableHeader.ts mobile should work with sortDesc correctly 1`] = `

                  Sort by

                      Iron (%)

                          $sort

                  <input id="input-61"
                         readonly="readonly"
                         type="text"
                         aria-readonly="false"
                         autocomplete="off"
                  >

                      $dropdown

                <input type="hidden"
                       value="iron"
                >

`;


import Vue from 'vue'

export type MouseHandler = (e: MouseEvent | TouchEvent) => any

export type MouseEvents = {
  [event: string]: {
    event: string
    passive?: boolean
    capture?: boolean
    once?: boolean
    stop?: boolean
    prevent?: boolean
    button?: number
    result?: any
  }
}

export type MouseEventsMap = {
  [event: string]: MouseHandler | MouseHandler[]
}

export default Vue.extend({
  name: 'mouse',

  methods: {
    getDefaultMouseEventHandlers (suffix: string, getEvent: MouseHandler): MouseEventsMap {
      return this.getMouseEventHandlers({
        ['click' + suffix]: { event: 'click' },
        ['contextmenu' + suffix]: { event: 'contextmenu', prevent: true, result: false },
        ['mousedown' + suffix]: { event: 'mousedown' },
        ['mousemove' + suffix]: { event: 'mousemove' },
        ['mouseup' + suffix]: { event: 'mouseup' },
        ['mouseenter' + suffix]: { event: 'mouseenter' },
        ['mouseleave' + suffix]: { event: 'mouseleave' },
        ['touchstart' + suffix]: { event: 'touchstart' },
        ['touchmove' + suffix]: { event: 'touchmove' },
        ['touchend' + suffix]: { event: 'touchend' },
      }, getEvent)
    },
    getMouseEventHandlers (events: MouseEvents, getEvent: MouseHandler): MouseEventsMap {
      const on: MouseEventsMap = {}

      for (const event in events) {
        const eventOptions = events[event]

        if (!this.$listeners[event]) continue

        // TODO somehow pull in modifiers

        const prefix = eventOptions.passive ? '&' : ((eventOptions.once ? '~' : '') + (eventOptions.capture ? '!' : ''))
        const key = prefix + eventOptions.event

        const handler: MouseHandler = e => {
          const mouseEvent: MouseEvent = e as MouseEvent
          if (eventOptions.button === undefined || (mouseEvent.buttons > 0 && mouseEvent.button === eventOptions.button)) {
            if (eventOptions.prevent) {
              e.preventDefault()
            }
            if (eventOptions.stop) {
              e.stopPropagation()
            }
            this.$emit(event, getEvent(e))
          }

          return eventOptions.result
        }

        if (key in on) {
          /* istanbul ignore next */
          if (Array.isArray(on[key])) {
            (on[key] as MouseHandler[]).push(handler)
          } else {
            on[key] = [on[key], handler] as MouseHandler[]
          }
        } else {
          on[key] = handler
        }
      }

      return on
    },
  },
})


/**
 * plugins/webfontloader.js
 *
 * webfontloader documentation: https://github.com/typekit/webfontloader
 */

// Globals
import { IN_BROWSER, IS_SERVER } from '@/util/globals'

export async function loadFonts () {
  if (IS_SERVER || !IN_BROWSER) return

  const WebFontLoader = await import('webfontloader')

  WebFontLoader.load({
    google: {
      families: ['Roboto:100,300,400,500,700,900&display=swap'],
    },
    custom: {
      families: [
        'Material Design Icons',
        'Font Awesome 5',
      ],
      urls: [
        'https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css',
        'https://use.fontawesome.com/releases/v5.0.8/css/all.css',
      ],
    },
  })
}


---
meta:
  title: Get started with Vuetify
  description: Get started with Vue and Vuetify in no time. Support for Vue CLI, Webpack, Nuxt and more.
  keywords: quick start, vuetify templates, installing vuetify, install vuetify
related:
  - /introduction/why-vuetify/
  - /introduction/frequently-asked-questions/
  - /beginners-guide/pre-amble/
---

# Installation

Get started with Vuetify, the world’s most popular Vue.js framework for building feature rich, blazing fast application.

<promoted-ad slug="vuemastery-getting-started" />

## Vue CLI Install

<alert type="warning">For information on how to use Vue CLI, visit the [official documentation](https://cli.vuejs.org/).</alert>

If you have not already created a new Vue.js project using **Vue CLI**, you can do so by typing:

```bash
vue create my-app
# navigate to new project directory
cd my-app
```

Now that you have an instantiated project, you can add the Vuetify [Vue CLI package](https://github.com/vuetifyjs/vue-cli-plugins/tree/master/packages/vue-cli-plugin-vuetify-cli) using the cli.

```bash
vue add vuetify
```

### Vue UI install

Vuetify can also be installed using **Vue UI**, the new visual application for Vue CLI. Ensure that you have the latest version of Vue CLI installed, then from your terminal type:

```bash
# ensure Vue CLI is >= 3.0
vue --version

# Then start the UI
vue ui
```

This will start the Vue User Interface and open a new window in your browser. On the left side of your screen, click on **Plugins**. Once there, search for Vuetify in the input field and install the plugin.

![Install Vuetify Plugin](https://cdn.vuetifyjs.com/images/quick-start/vue_ui.png "Vue UI Vuetify Plugin")

## Nuxt install

Vuetify can be added by installing the Nuxt Vuetify module.

```bash
yarn add @nuxtjs/vuetify -D
# OR
npm install @nuxtjs/vuetify -D
```

Once installed, update your nuxt.config.js file to include the Vuetify module in the build.

```js
// nuxt.config.js
{
  buildModules: [
    // Simple usage
    '@nuxtjs/vuetify',

    // With options
    ['@nuxtjs/vuetify', { /* module options */ }]
  ]
}
```

<highlighted-ad slug="nuxt-community-vuetify-module" />

## Webpack install

To install Vuetify into a Webpack project you need to add a few dependencies:

```bash
yarn add vuetify
# OR
npm install vuetify
```

```bash
yarn add sass sass-loader fibers deepmerge -D
# OR
npm install sass sass-loader fibers deepmerge -D
```

Once installed, locate your `webpack.config.js` file and copy the snippet below into the rules array. If you have an existing sass rule configured, you may need to apply some or all of the changes below. If you are you looking to utilize the vuetify-loader for treeshaking, ensure that you are on version >=4 of Webpack. You can find more information on setting it up with webpack on the A-la-carte page.

```js
// webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.s(c|a)ss$/,
        use: [
          'vue-style-loader',
          'css-loader',
          {
            loader: 'sass-loader',
            // Requires sass-loader@^7.0.0
            options: {
              implementation: require('sass'),
              fiber: require('fibers'),
              indentedSyntax: true // optional
            },
            // Requires sass-loader@^8.0.0
            options: {
              implementation: require('sass'),
              sassOptions: {
                fiber: require('fibers'),
                indentedSyntax: true // optional
              },
            },
          },
        ],
      },
    ],
  }
}
```

Create a plugin file for Vuetify, `src/plugins/vuetify.js` with the below content:

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'

Vue.use(Vuetify)

const opts = {}

export default new Vuetify(opts)
```

If using vuetify-loader use the content below:

```js
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

const opts = {}

export default new Vuetify(opts)
```

Navigate to your main entry point where you instantiate your Vue instance and pass the Vuetify object in as an option.

```js
// src/main.js

import Vue from 'vue'
import vuetify from '@/plugins/vuetify' // path to vuetify export

new Vue({
  vuetify,
}).$mount('#app')
```

### Font installation

Vuetify uses Google's Roboto font and Material Design Icons. The simplest way to install these are to include their CDN's in your main `index.html` file.

```html
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
```

## CDN usage

To test using Vuetify.js without installing a template from Vue CLI, copy the code below into your `index.html` file. This will pull the latest version of Vue and Vuetify, allowing you to start playing with components. You can also use the [Vuetify starter](https://template.vuetifyjs.com) on Codepen. While not recommended, if you need to utilize the CDN packages in a production environment, it is recommended that you scope the versions of your assets. For more information on how to do this, navigate to the jsdelivr website.

<alert type="info">In order for your application to work properly, you must wrap it in a v-app component. See the Application component page for more information.</alert>

```html
<!DOCTYPE html>
<html>
<head>
  <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
</head>
<body>
  <div id="app">
    <v-app>
      <v-main>
        <v-container>Hello world</v-container>
      </v-main>
    </v-app>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
  <script>
    new Vue({
      el: '#app',
      vuetify: new Vuetify(),
    })
  </script>
</body>
</html>
```

## Usage with Electron

To use Vuetify with Electron, add the electron-builder plugin via Vue CLI.

```bash
# Install
vue add electron-builder

# Usage
yarn electron:build
yarn electron:serve
```

## Usage with PWA

If you are creating a new app with Vue CLI, you have the option to select Progressive Web App (PWA) Support in the first prompt after initiating vue create my-app. This package can also be installed into existing Vue CLI projects by entering the following command:

```bash
vue add pwa
```

## Usage with Cordova

To use Vuetify with Cordova, add the Cordova plugin via Vue CLI:

```bash
# If cordova is not already installed
yarn global add cordova

# Install
vue add cordova

# Usage
yarn cordova-serve-android # Development Android
yarn cordova-build-android # Build Android
yarn cordova-serve-ios # Development IOS
yarn cordova-build-ios # Build IOS
yarn cordova-serve-browser # Development Browser
yarn cordova-build-browser # Build Browser
```

## Usage with Capacitor

To use Vuetify with **Capacitor**, add the [Capacitor](?) plugin via Vue CLI:

```bash
# Install
$ vue add @nklayman/capacitor

# Usage
$ yarn capacitor:serve
```

## Usage with Vuepress

There are 2 ways we can use Vuetify with default **vuepress** theme. Either by  registering vuetify as a plugin in [vuepress](https://vuepress.vuejs.org/) `.vuepress/enhanceApp.js` file (code sample below), or by using vuetify directly from CDN:

```js
// register vuetify as a global plugin with vuepress
// .vuepress/enhanceApp.js
import Vuetify from 'vuetify'

export default ({
  Vue,      // the version of Vue being used in the VuePress app
  options,  // the options for the root Vue instance
  router,   // the router instance for the app
  siteData  // site metadata
}) => {
  Vue.use(Vuetify)
}

// Alternatively, use vuetify directly from CDN.
// Update head section in .vuepress/config.js as follows
module.exports = {
  head: [
    ['link', {
      rel: 'stylesheet',
      href: `https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css`

    }],
    ['script', { src: `https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js` }],
    ['script', { src: `https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js` }],
  ]
}
```

<backmatter />


module.exports = {
  'v-list-group': {
    slots: [
      {
        name: 'activator',
        props: undefined,
      },
      {
        name: 'appendIcon',
        props: undefined,
      },
      {
        name: 'prependIcon',
        props: undefined,
      },
    ],
    events: [
      {
        name: 'click',
        value: 'MouseEvent',
      },
    ],
  },
}


Removes elevation (box-shadow) and adds a *thin* border.

Applies a large border radius on the top left and bottom right of the card.

Specify a custom tag used on the root element.


// Imports
import Router from 'vue-router'
import scrollBehavior from './scroll-behavior'
import Vue from 'vue'
import VueGtag from 'vue-gtag'
import { localeLookup } from '@/i18n/util'
import redirects from './301.json'

// Globals
import { IS_PROD, IS_SERVER } from '@/util/globals'

import {
  abort,
  locale,
  layout,
  route,
  rpath,
  redirect,
} from '@/util/routes'

// Setup
Vue.use(Router)

export function createRouter (vuetify, store, i18n) {
  const loadedLocales = ['en']
  const router = new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    scrollBehavior: (...args) => scrollBehavior(vuetify, ...args),
    routes: [
      locale([
        ...Object.keys(redirects).map(k => ({
          path: k.replace(/^\//, ''),
          redirect: () => redirects[k].replace(/^\//, ''),
        })),
        layout('Home', [route('Home')]),
        layout('Default', [route('Documentation')], ':category/:page/'),
        route('Whiteframes', 'examples/whiteframes/:whiteframe/'),
        layout('Default', [abort()]),
      ]),
      // Redirect for language fallback
      redirect('/:locale(%s)/*', to => to.params.pathMatch),
      // The previous one doesn't match if there's no slash after the language code
      redirect('/:locale(%s)'),
      redirect(),
    ],
  })

  function loadLocale (locale) {
    if (
      !locale ||
      i18n.locale === locale ||
      loadedLocales.includes(locale)
    ) return Promise.resolve()

    const messagesFile = localeLookup(locale)

    return import(
      /* webpackChunkName: "locale-[request]" */
      `@/i18n/messages/${messagesFile}.json`
    ).then(messages => {
      i18n.setLocaleMessage(locale, messages.default)
      loadedLocales.push(locale)
      i18n.locale = locale
    })
  }

  router.beforeEach(({ path }, from, next) => {
    return path.endsWith('/') ? next() : next(rpath(path))
  })

  router.beforeEach((to, _, next) => {
    loadLocale(to.params.locale).then(() => next())
  })

  router.afterEach((to, from) => {
    if (
      to.params.locale !== from.params.locale &&
      [to.params.locale, from.params.locale].includes('eo-UY')
    ) setTimeout(() => location.reload(), 250)
  })

  Vue.use(VueGtag, {
    bootstrap: IS_PROD && !IS_SERVER,
    config: { id: 'UA-75262397-3' },
  }, router)

  return router
}


<template>

      d-inline

      d-inline

</template>


@import '../../styles/styles.sass';
@import '../../styles/tools/_functions.sass';

$chip-avatar-size: 24px !default;
$chip-close-size: 18px !default;
$chip-icon-margin-after: 6px !default;
$chip-icon-margin-before: -6px !default;
$chip-icon-right-margin-after: -4px !default;
$chip-icon-right-margin-before: 8px !default;
$chip-icon-size: 24px !default;
$chip-label-border-radius: $border-radius-root !default;
$chip-link-focus-opacity: 0.32 !default;
$chip-pill-avatar-margin-after: 8px !default;
$chip-pill-avatar-margin-before: -12px !default;
$chip-pill-avatar-size: 32px !default;
$chip-pill-filter-margin: 0 16px 0 0 !default;
$chip-transition-duration: 0.28s !default;
$chip-transition-fn: map-get($transition, 'fast-out-slow-in') !default;
$icon-outlined-border-width: thin !default;
$chip-line-height: 20px !default;
$chip-padding: 0 12px !default;
$chip-white-space: nowrap !default;
$chip-disabled-opacity: 0.4 !default;
$chip-filter-max-width: 24px !default;
$chip-outlined-active-opacity: 0.08 !default;
$chip-selected-opacity: 0.28 !default;
$icon-sizes: () !default;
$icon-sizes: map-deep-merge(
  (
    'x-small': (
      'font-size': 10,
      'height': 16
    ),
    'small': (
      'font-size': 12,
      'height': 24
    ),
    'default': (
      'font-size': 14,
      'height': 32
    ),
    'large': (
      'font-size': 16,
      'height': 54
    ),
    'x-large': (
      'font-size': 18,
      'height': 66
    )
  ),
  $icon-sizes
);


const WebFontLoader = await import('webfontloader')


import VData from '../VData'
import {
  mount,
  MountOptions,
  Wrapper,
} from '@vue/test-utils'

describe('VData.ts', () => {
  type Instance = InstanceType<typeof VData>
  let mountFunction: (options?: MountOptions<Instance>) => Wrapper<Instance>
  beforeEach(() => {
    mountFunction = (options?: MountOptions<Instance>) => {
      return mount(VData, {
        ...options,
        sync: false,
      })
    }
  })

  it('should render data through default scoped slot', async () => {
    const wrapper = mountFunction({
      propsData: {
        items: [
          { id: 1, text: 'foo' },
          { id: 2, text: 'bar' },
        ],
      },
      scopedSlots: {
        default (data) {
          return this.$createElement('div', data.items.map(item => this.$createElement('div', [item.text])))
        },
      },
    })

    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should ignore items length if using server-items-length', async () => {
    const render = jest.fn()
    const wrapper = mountFunction({
      propsData: {
        items: [
          { id: 1, text: 'foo' },
          { id: 2, text: 'bar' },
        ],
      },
      scopedSlots: {
        default: render,
      },
    })

    await wrapper.vm.$nextTick()
    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      pagination: expect.objectContaining({
        itemsLength: 2,
      }),
    }))

    wrapper.setProps({
      serverItemsLength: 10,
    })

    await wrapper.vm.$nextTick()
    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      pagination: expect.objectContaining({
        itemsLength: 10,
      }),
    }))
  })

  it('should group items', async () => {
    const render = jest.fn()
    const items = [
      { id: 1, text: 'foo', baz: 'one' },
      { id: 2, text: 'bar', baz: 'two' },
      { id: 3, text: 'baz', baz: 'one' },
    ]

    const wrapper = mountFunction({
      propsData: {
        items,
        groupBy: ['baz'],
      },
      scopedSlots: {
        default: render,
      },
    })

    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      groupedItems: [
        {
          name: 'one',
          items: [items[0], items[2]],
        },
        {
          name: 'two',
          items: [items[1]],
        },
      ],
    }))
  })

  it('should group items by deep keys', async () => {
    const render = jest.fn()
    const items = [
      { id: 1, text: 'foo', foo: { bar: 'one' } },
      { id: 2, text: 'bar', foo: { bar: 'two' } },
      { id: 3, text: 'baz', foo: { bar: 'one' } },
    ]

    const wrapper = mountFunction({
      propsData: {
        items,
        groupBy: ['foo.bar'],
      },
      scopedSlots: {
        default: render,
      },
    })

    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      groupedItems: [
        {
          name: 'one',
          items: [items[0], items[2]],
        },
        {
          name: 'two',
          items: [items[1]],
        },
      ],
    }))
  })

  it('should group items with a custom group function', async () => {
    const render = jest.fn()
    const items = [
      { id: 1, text: 'foo', value: 1 },
      { id: 2, text: 'bar', value: 4 },
      { id: 3, text: 'baz', value: 3 },
    ]

    const wrapper = mountFunction({
      propsData: {
        items,
        groupBy: ['value'],
        customGroup: function evenOddGrouper (items: any[], groupBy: string[]) {
          const key = groupBy[0]
          return items.reduce((rv, x) => {
            const group = x[key] % 2 ? 'odd' : 'even';
            (rv[group] = rv[group] || []).push(x)
            return rv
          }, {})
        },
      },
      scopedSlots: {
        default: render,
      },
    })

    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      groupedItems: {
        even: [items[1]],
        odd: [items[0], items[2]],
      },
    }))
  })

  it('should sort items', () => {
    const render = jest.fn()

    const unsorted = [
      { id: 1, text: 'c' },
      { id: 2, text: 'a' },
      { id: 3, text: 'd' },
      { id: 4, text: 'b' },
    ]

    const sorted = [
      { id: 2, text: 'a' },
      { id: 4, text: 'b' },
      { id: 1, text: 'c' },
      { id: 3, text: 'd' },
    ]

    const wrapper = mountFunction({
      propsData: {
        items: unsorted,
        sortBy: ['text'],
      },
      scopedSlots: {
        default: render,
      },
    })

    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      items: sorted,
    }))
  })

  it('should sort items by multiple properties', async () => {
    const render = jest.fn()

    const unsorted = [
      { id: 1, foo: 'a', bar: 'b' },
      { id: 2, foo: 'b', bar: 'b' },
      { id: 3, foo: 'b', bar: 'a' },
      { id: 4, foo: 'a', bar: 'a' },
    ]

    const sorted = [
      { id: 4, foo: 'a', bar: 'a' },
      { id: 1, foo: 'a', bar: 'b' },
      { id: 3, foo: 'b', bar: 'a' },
      { id: 2, foo: 'b', bar: 'b' },
    ]

    const wrapper = mountFunction({
      propsData: {
        items: unsorted,
        sortBy: ['foo'],
      },
      scopedSlots: {
        default: render,
      },
    })

    await wrapper.vm.$nextTick()

    wrapper.setProps({
      sortBy: ['foo', 'bar'],
    })
    await wrapper.vm.$nextTick()

    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      items: sorted,
    }))
  })

  it('should paginate items', () => {
    const render = jest.fn()

    const items = [
      { id: 1, foo: 'a' },
      { id: 2, foo: 'b' },
      { id: 3, foo: 'c' },
      { id: 4, foo: 'd' },
      { id: 5, foo: 'e' },
      { id: 6, foo: 'f' },
      { id: 7, foo: 'g' },
      { id: 8, foo: 'h' },
      { id: 9, foo: 'i' },
      { id: 10, foo: 'j' },
    ]

    const wrapper = mountFunction({
      propsData: {
        items,
        itemsPerPage: 5,
        page: 2,
      },
      scopedSlots: {
        default: render,
      },
    })

    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      items: items.slice(5),
    }))
  })

  it('should not sort items if disableSort is active', () => {
    const render = jest.fn()

    const items = [
      { text: 'Foo', id: 1 },
      { text: 'Bar', id: 2 },
      { text: 'Fizz', id: 3 },
      { text: 'Buzz', id: 4 },
      { text: 'Fizzbuzz', id: 5 },
    ]

    const wrapper = mountFunction({
      propsData: {
        items,
        sortBy: ['text'],
        disableSort: true,
      },
      scopedSlots: {
        default: render,
      },
    })

    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      items,
    }))
  })

  it('should conditionally paginate items', async () => {
    const render = jest.fn()
    const items = [
      { text: 'Foo', id: 1 },
      { text: 'Bar', id: 2 },
      { text: 'Fizz', id: 3 },
      { text: 'Buzz', id: 4 },
      { text: 'Fizzbuzz', id: 5 },
    ]

    const wrapper = mountFunction({
      propsData: { items },
      scopedSlots: {
        default: render,
      },
    })

    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      items,
    }))

    wrapper.setProps({ itemsPerPage: 2 })
    await wrapper.vm.$nextTick()

    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      items: items.slice(0, 2),
    }))

    wrapper.setProps({ disablePagination: true })
    await wrapper.vm.$nextTick()

    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      items,
    }))
  })

  it('should toggle sorting', async () => {
    const unsorted = [
      { id: 1, text: 'c' },
      { id: 2, text: 'a' },
      { id: 3, text: 'd' },
      { id: 4, text: 'b' },
    ]

    const wrapper = mountFunction({
      propsData: {
        items: unsorted,
      },
      scopedSlots: {
        default (props) {
          const items = props.items.map(item => this.$createElement('div', [item.text]))
          return this.$createElement('div', {
            attrs: {
              id: 'wrapper',
            },
            on: {
              click: () => props.sort('text'),
            },
          }, items)
        },
      },
    })

    expect(wrapper.html()).toMatchSnapshot()

    const el = wrapper.find('#wrapper').element
    el.click()
    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()

    el.click()
    await wrapper.vm.$nextTick()
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should toggle sorting on multiple properties', async () => {
    const unsorted = [
      { id: 1, text: 'c', group: 'foo' },
      { id: 2, text: 'a', group: 'bar' },
      { id: 3, text: 'd', group: 'foo' },
      { id: 4, text: 'b', group: 'bar' },
    ]

    const wrapper = mountFunction({
      propsData: {
        items: unsorted,
      },
      scopedSlots: {
        default (props) {
          const items = props.items.map(item => this.$createElement('div', [`${item.group}-${item.text}`]))
          return this.$createElement('div', {
            attrs: {
              id: 'wrapper',
            },
            on: {
              click: () => props.sort(['group', 'text']),
            },
          }, items)
        },
      },
    })

    expect(wrapper.html()).toMatchSnapshot()

    const el = wrapper.find('#wrapper').element
    el.click()
    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should toggle grouping', async () => {
    const unsorted = [
      { id: 1, text: 'c', group: 'foo' },
      { id: 4, text: 'a', group: 'bar' },
      { id: 3, text: 'd', group: 'foo' },
      { id: 2, text: 'b', group: 'bar' },
    ]

    const wrapper = mountFunction({
      propsData: {
        items: unsorted,
      },
      scopedSlots: {
        default (props) {
          const items = props.groupedItems
            ? props.groupedItems.map(group => group.name)
            : props.items.map(item => item.text)

          return this.$createElement('div', {
            attrs: {
              id: 'wrapper',
            },
            on: {
              click: () => props.group('group'),
            },
          }, items.map(item => this.$createElement('div', [item])))
        },
      },
    })

    expect(wrapper.html()).toMatchSnapshot()

    const el = wrapper.find('#wrapper').element
    el.click()
    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
  })

  // https://github.com/vuetifyjs/vuetify/issues/10372
  it('should handle setting itemsPerPage to zero', async () => {
    const render = jest.fn()
    const wrapper = mountFunction({
      propsData: {
        items: [
          { id: 1, text: 'foo' },
          { id: 2, text: 'bar' },
        ],
        itemsPerPage: 0,
      },
      scopedSlots: {
        default: render,
      },
    })

    await wrapper.vm.$nextTick()
    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      pagination: expect.objectContaining({
        itemsPerPage: 0,
        page: 1,
        pageCount: 1,
        pageStart: 0,
        pageStop: 0,
      }),
    }))

    wrapper.setProps({
      itemsPerPage: 1,
    })

    await wrapper.vm.$nextTick()
    expect(render).toHaveBeenCalledWith(expect.objectContaining({
      pagination: expect.objectContaining({
        itemsPerPage: 1,
        page: 1,
        pageCount: 2,
        pageStart: 0,
        pageStop: 1,
      }),
    }))
  })

  // https://github.com/vuetifyjs/vuetify/issues/10627
  it('should sort grouped column', async () => {
    const unsorted = [
      { id: 1, text: 'c', group: 'foo' },
      { id: 4, text: 'a', group: 'bar' },
      { id: 3, text: 'd', group: 'foo' },
      { id: 2, text: 'b', group: 'bar' },
    ]

    const wrapper = mountFunction({
      propsData: {
        items: unsorted,
        groupBy: ['text'],
      },
      scopedSlots: {
        default (props) {
          return this.$createElement('div', {
            attrs: {
              id: 'wrapper',
            },
            on: {
              click: () => props.group('group'),
            },
          }, props.groupedItems.map(group => this.$createElement('div', [group.name])))
        },
      },
    })

    wrapper.setProps({ groupDesc: [false] })
    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()

    wrapper.setProps({ groupDesc: [true] })
    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
  })
})


<template>
  <v-row justify="center">
    <v-btn
      color="primary"
      dark
      @click.stop="dialog = true"
    >
      Open Dialog
    </v-btn>

    <v-dialog
      v-model="dialog"
      max-width="290"
    >
      <v-card>
        <v-card-title class="headline">
          Use Google's location service?
        </v-card-title>

        <v-card-text>
          Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.
        </v-card-text>

        <v-card-actions>
          <v-spacer></v-spacer>

          <v-btn
            color="green darken-1"
            text
            @click="dialog = false"
          >
            Disagree
          </v-btn>

          <v-btn
            color="green darken-1"
            text
            @click="dialog = false"
          >
            Agree
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-row>
</template>

<script>
  export default {
    data () {
      return {
        dialog: false,
      }
    },
  }
</script>


// Styles
import './VTabs.sass'

// Components
import VTabsBar from './VTabsBar'
import VTabsItems from './VTabsItems'
import VTabsSlider from './VTabsSlider'

// Mixins
import Colorable from '../../mixins/colorable'
import Proxyable from '../../mixins/proxyable'
import Themeable from '../../mixins/themeable'

// Directives
import Resize from '../../directives/resize'

// Utilities
import { convertToUnit } from '../../util/helpers'
import { ExtractVue } from './../../util/mixins'
import mixins from '../../util/mixins'

// Types
import { VNode } from 'vue/types'

const baseMixins = mixins(
  Colorable,
  Proxyable,
  Themeable
)

interface options extends ExtractVue<typeof baseMixins> {
  $refs: {
    items: InstanceType<typeof VTabsBar>
  }
}

export default baseMixins.extend<options>().extend({
  name: 'v-tabs',

  directives: {
    Resize,
  },

  props: {
    activeClass: {
      type: String,
      default: '',
    },
    alignWithTitle: Boolean,
    backgroundColor: String,
    centerActive: Boolean,
    centered: Boolean,
    fixedTabs: Boolean,
    grow: Boolean,
    height: {
      type: [Number, String],
      default: undefined,
    },
    hideSlider: Boolean,
    iconsAndText: Boolean,
    mobileBreakpoint: [String, Number],
    nextIcon: {
      type: String,
      default: '$next',
    },
    optional: Boolean,
    prevIcon: {
      type: String,
      default: '$prev',
    },
    right: Boolean,
    showArrows: [Boolean, String],
    sliderColor: String,
    sliderSize: {
      type: [Number, String],
      default: 2,
    },
    vertical: Boolean,
  },

  data () {
    return {
      resizeTimeout: 0,
      slider: {
        height: null as null | number,
        left: null as null | number,
        right: null as null | number,
        top: null as null | number,
        width: null as null | number,
      },
      transitionTime: 300,
    }
  },

  computed: {
    classes (): object {
      return {
        'v-tabs--align-with-title': this.alignWithTitle,
        'v-tabs--centered': this.centered,
        'v-tabs--fixed-tabs': this.fixedTabs,
        'v-tabs--grow': this.grow,
        'v-tabs--icons-and-text': this.iconsAndText,
        'v-tabs--right': this.right,
        'v-tabs--vertical': this.vertical,
        ...this.themeClasses,
      }
    },
    isReversed (): boolean {
      return this.$vuetify.rtl && this.vertical
    },
    sliderStyles (): object {
      return {
        height: convertToUnit(this.slider.height),
        left: this.isReversed ? undefined : convertToUnit(this.slider.left),
        right: this.isReversed ? convertToUnit(this.slider.right) : undefined,
        top: this.vertical ? convertToUnit(this.slider.top) : undefined,
        transition: this.slider.left != null ? null : 'none',
        width: convertToUnit(this.slider.width),
      }
    },
    computedColor (): string {
      if (this.color) return this.color
      else if (this.isDark && !this.appIsDark) return 'white'
      else return 'primary'
    },
  },

  watch: {
    alignWithTitle: 'callSlider',
    centered: 'callSlider',
    centerActive: 'callSlider',
    fixedTabs: 'callSlider',
    grow: 'callSlider',
    right: 'callSlider',
    showArrows: 'callSlider',
    vertical: 'callSlider',
    '$vuetify.application.left': 'onResize',
    '$vuetify.application.right': 'onResize',
    '$vuetify.rtl': 'onResize',
  },

  mounted () {
    this.$nextTick(() => {
      window.setTimeout(this.callSlider, 30)
    })
  },

  methods: {
    callSlider () {
      if (
        this.hideSlider ||
        !this.$refs.items ||
        !this.$refs.items.selectedItems.length
      ) {
        this.slider.width = 0
        return false
      }

      this.$nextTick(() => {
        // Give screen time to paint
        const activeTab = this.$refs.items.selectedItems[0]
        /* istanbul ignore if */
        if (!activeTab || !activeTab.$el) {
          this.slider.width = 0
          this.slider.left = 0
          return
        }
        const el = activeTab.$el as HTMLElement

        this.slider = {
          height: !this.vertical ? Number(this.sliderSize) : el.scrollHeight,
          left: this.vertical ? 0 : el.offsetLeft,
          right: this.vertical ? 0 : el.offsetLeft + el.offsetWidth,
          top: el.offsetTop,
          width: this.vertical ? Number(this.sliderSize) : el.scrollWidth,
        }
      })

      return true
    },
    genBar (items: VNode[], slider: VNode | null) {
      const data = {
        style: {
          height: convertToUnit(this.height),
        },
        props: {
          activeClass: this.activeClass,
          centerActive: this.centerActive,
          dark: this.dark,
          light: this.light,
          mandatory: !this.optional,
          mobileBreakpoint: this.mobileBreakpoint,
          nextIcon: this.nextIcon,
          prevIcon: this.prevIcon,
          showArrows: this.showArrows,
          value: this.internalValue,
        },
        on: {
          'call:slider': this.callSlider,
          change: (val: any) => {
            this.internalValue = val
          },
        },
        ref: 'items',
      }

      this.setTextColor(this.computedColor, data)
      this.setBackgroundColor(this.backgroundColor, data)

      return this.$createElement(VTabsBar, data, [
        this.genSlider(slider),
        items,
      ])
    },
    genItems (items: VNode | null, item: VNode[]) {
      // If user provides items
      // opt to use theirs
      if (items) return items

      // If no tabs are provided
      // render nothing
      if (!item.length) return null

      return this.$createElement(VTabsItems, {
        props: {
          value: this.internalValue,
        },
        on: {
          change: (val: any) => {
            this.internalValue = val
          },
        },
      }, item)
    },
    genSlider (slider: VNode | null) {
      if (this.hideSlider) return null

      if (!slider) {
        slider = this.$createElement(VTabsSlider, {
          props: { color: this.sliderColor },
        })
      }

      return this.$createElement('div', {
        staticClass: 'v-tabs-slider-wrapper',
        style: this.sliderStyles,
      }, [slider])
    },
    onResize () {
      if (this._isDestroyed) return

      clearTimeout(this.resizeTimeout)
      this.resizeTimeout = window.setTimeout(this.callSlider, 0)
    },
    parseNodes () {
      let items = null
      let slider = null
      const item = []
      const tab = []
      const slot = this.$slots.default || []
      const length = slot.length

      for (let i = 0; i < length; i++) {
        const vnode = slot[i]

        if (vnode.componentOptions) {
          switch (vnode.componentOptions.Ctor.options.name) {
            case 'v-tabs-slider': slider = vnode
              break
            case 'v-tabs-items': items = vnode
              break
            case 'v-tab-item': item.push(vnode)
              break
            // case 'v-tab' - intentionally omitted
            default: tab.push(vnode)
          }
        } else {
          tab.push(vnode)
        }
      }

      /**
       * tab: array of `v-tab`
       * slider: single `v-tabs-slider`
       * items: single `v-tabs-items`
       * item: array of `v-tab-item`
       */
      return { tab, slider, items, item }
    },
  },

  render (h): VNode {
    const { tab, slider, items, item } = this.parseNodes()

    return h('div', {
      staticClass: 'v-tabs',
      class: this.classes,
      directives: [{
        name: 'resize',
        modifiers: { quiet: true },
        value: this.onResize,
      }],
    }, [
      this.genBar(tab, slider),
      this.genItems(items, item),
    ])
  },
})


) setTimeout(() => location.reload(), 250)

keep keep keep keep keep replace keep keep keep


<template>
  <v-btn
    :aria-label="$t('become-a-sponsor')"
    :to="{
      name: 'Documentation',
      params: {
        category: 'introduction',
        page: 'sponsors-and-backers'
      }
    }"
    color="primary"
    outlined
    rounded
    small
    v-bind="$attrs"
    v-on="$listeners"
  >
    <span
      class="text-capitalize font-weight-regular"
      v-text="$t('become-a-sponsor')"
    />
  </v-btn>
</template>

<script>
  export default { name: 'SponsorLink' }
</script>


import Vue from 'vue'

/**
 * SSRBootable
 *
 * @mixin
 *
 * Used in layout components (drawer, toolbar, content)
 * to avoid an entry animation when using SSR
 */
export default Vue.extend({
  name: 'ssr-bootable',

  data: () => ({
    isBooted: false,
  }),

  mounted () {
    // Use setAttribute instead of dataset
    // because dataset does not work well
    // with unit tests
    window.requestAnimationFrame(() => {
      this.$el.setAttribute('data-booted', 'true')
      this.isBooted = true
    })
  },
})


import Vue from 'vue'

export default Vue.extend({
  name: 'localable',

  props: {
    locale: String,
  },

  computed: {
    currentLocale (): string {
      return this.locale || this.$vuetify.lang.current
    },
  },
})


keep keep keep keep replace keep keep keep

vuetifyjs.com

A Vue.js project

vue-cli-service lint --no-fix && yarn lint:md

yarn build:client && yarn build:server && yarn prerender

cross-env WEBPACK_TARGET=node yarn build --no-clean

markdownlint --config .markdownlintrc src/pages/en --fix

http-server dist --proxy http://localhost:8080/?


import VDatePickerYears from '../VDatePickerYears'
import {
  mount,
  MountOptions,
  Wrapper,
} from '@vue/test-utils'

describe('VDatePickerYears.ts', () => {
  type Instance = InstanceType<typeof VDatePickerYears>
  let mountFunction: (options?: MountOptions<Instance>) => Wrapper<Instance>
  beforeEach(() => {
    mountFunction = (options?: MountOptions<Instance>) => {
      return mount(VDatePickerYears, {
        ...options,
        mocks: {
          $vuetify: {
            rtl: false,
            lang: {
              t: () => {},
            },
          },
        },
      })
    }
  })

  it('should render component and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        value: '2000',
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should respect min/max props', async () => {
    const wrapper = mountFunction({
      propsData: {
        min: 1234,
        max: 1238,
      },
    })

    expect(wrapper.findAll('li:first-child').at(0).element.textContent).toBe('1238')
    expect(wrapper.findAll('li:last-child').at(0).element.textContent).toBe('1234')
  })

  it('should not allow min to be greater then max', async () => {
    const wrapper = mountFunction({
      propsData: {
        min: 1238,
        max: 1234,
      },
    })
    expect(wrapper.findAll('li')).toHaveLength(1)
    expect(wrapper.findAll('li').at(0).element.textContent).toBe('1234')
    expect(wrapper.findAll('li').at(0).element.textContent).toBe('1234')
  })

  it('should emit event on year click', async () => {
    const wrapper = mountFunction({
      propsData: {
        value: 1999,
      },
    })

    const input = jest.fn()
    wrapper.vm.$on('input', input)

    wrapper.findAll('li.active + li').at(0).trigger('click')
    expect(input).toHaveBeenCalledWith(1998)
  })

  it('should format years', async () => {
    const wrapper = mountFunction({
      propsData: {
        format: year => `(${year})`,
        min: 1001,
        max: 1001,
      },
    })

    expect(wrapper.findAll('li').at(0).element.textContent).toBe('(1001)')
  })
})


import VBanner from './VBanner'

export { VBanner }
export default VBanner


@import './_variables.scss'

// Theme
+theme(v-card) using ($material)
  background-color: map-get($material, 'cards')
  color: map-deep-get($material, 'text', 'primary')

  > .v-card__text,
  .v-card__subtitle
    color: map-deep-get($material, 'text', 'secondary')

// Sheet
+sheet(v-card, $card-elevation, $card-border-radius, $card-shaped-border-radius)

// Block
.v-card
  border-width: $card-outlined-border-width
  display: block
  max-width: 100%
  outline: none
  text-decoration: none
  transition-property: box-shadow, opacity
  overflow-wrap: $card-overflow-wrap
  position: relative
  white-space: $card-white-space

  > *:first-child:not(.v-btn):not(.v-chip),
  > .v-card__progress + *:not(.v-btn):not(.v-chip)
    border-top-left-radius: inherit
    border-top-right-radius: inherit

  > *:last-child:not(.v-btn):not(.v-chip)
    border-bottom-left-radius: inherit
    border-bottom-right-radius: inherit

// Element
.v-card__progress
  top: 0
  left: 0
  right: 0
  overflow: hidden

.v-card__subtitle
  + .v-card__text
    padding-top: $card-adjacent-sibling-text-padding-top

.v-card__subtitle,
.v-card__text
  font-size: $card-text-font-size
  font-weight: $card-text-font-weight
  line-height: $card-text-line-height
  letter-spacing: $card-text-letter-spacing

.v-card__subtitle,
.v-card__text,
.v-card__title
  padding: $card-subtitle-padding

.v-card__title
  align-items: center
  display: flex
  flex-wrap: wrap
  font-size: $card-title-font-size
  font-weight: $card-title-font-weight
  letter-spacing: $card-title-letter-spacing
  line-height: $card-title-line-height
  word-break: $card-title-word-break

  + .v-card__subtitle,
  + .v-card__text
    padding-top: $card-adjacent-sibling-text-padding-top

  + .v-card__subtitle
    margin-top: $card-title-adjacent-sibling-subtitle-margin-top

.v-card__text
  width: 100%

.v-card__actions
  align-items: center
  display: flex
  padding: $card-actions-padding

  > .v-btn.v-btn
    padding: $card-btn-padding

    // Buttons have reduced padding in card actions per spec
    // and need to compensate the icon margins accordingly
    +ltr()
      + .v-btn
        margin-left: $card-btn-margin-x

      .v-icon--left
        margin-left: $card-btn-small-margin-x

      .v-icon--right
        margin-right: $card-btn-small-margin-x

    +rtl()
      + .v-btn
        margin-right: $card-btn-margin-x

      .v-icon--left
        margin-right: $card-btn-small-margin-x

      .v-icon--right
        margin-left: $card-btn-small-margin-x

// @deprecated
// TODO: Remove in v3
.v-card--flat
  +elevation(0, true)

.v-card--hover
  cursor: pointer
  transition: box-shadow .4s cubic-bezier(.25, .8, .25, 1)

  &:hover, &:focus
    +elevation($card-hover-elevation)

.v-card--link
  cursor: pointer

  // https://github.com/vuetifyjs/vuetify/issues/3925
  .v-chip
    cursor: pointer

  &:focus:before
    opacity: $card-link-focus-opacity

  &:before
    background: currentColor
    bottom: 0
    content: ''
    left: 0
    opacity: 0
    pointer-events: none
    position: absolute
    right: 0
    top: 0
    transition: .2s opacity map-get($transition, 'fast-in-slow-out')

.v-card--disabled
  pointer-events: none
  user-select: none

  > *:not(.v-card__progress)
    opacity: $card-disabled-opacity
    transition: inherit

.v-card--loading
  overflow: hidden

.v-card--raised
  +elevation($card-raised-elevation)


const IN_BROWSER = typeof window !== 'undefined'

const IS_DEBUG = process.env.DEBUG === 'true'

const IS_PROD = process.env.NODE_ENV === 'production'

const IS_SERVER = process.env.WEBPACK_TARGET === 'node'

keep keep keep replace keep keep keep keep

const IS_SERVER = process.env.VUE_APP_WEBPACK_TARGET === 'node'


declare module 'vuetify/es5/util/colors' {
  interface BaseColor {
    readonly base: string
    readonly lighten5: string
    readonly lighten4: string
    readonly lighten3: string
    readonly lighten2: string
    readonly lighten1: string
    readonly darken1: string
    readonly darken2: string
    readonly darken3: string
    readonly darken4: string
  }

  interface Color extends BaseColor {
    readonly accent1: string
    readonly accent2: string
    readonly accent3: string
    readonly accent4: string
  }

  interface Shade {
    readonly black: string
    readonly white: string
    readonly transparent: string
  }

  export interface Colors {
    red: Color
    pink: Color
    purple: Color
    deepPurple: Color
    indigo: Color
    blue: Color
    lightBlue: Color
    cyan: Color
    teal: Color
    green: Color
    lightGreen: Color
    lime: Color
    yellow: Color
    amber: Color
    orange: Color
    deepOrange: Color
    brown: BaseColor
    blueGrey: BaseColor
    grey: BaseColor
    shades: Shade
  }

  const colors: Colors
  export default colors
}

declare module 'vuetify/lib/util/colors' {
  interface BaseColor {
    readonly base: string
    readonly lighten5: string
    readonly lighten4: string
    readonly lighten3: string
    readonly lighten2: string
    readonly lighten1: string
    readonly darken1: string
    readonly darken2: string
    readonly darken3: string
    readonly darken4: string
  }

  export interface Color extends BaseColor {
    readonly accent1: string
    readonly accent2: string
    readonly accent3: string
    readonly accent4: string
  }

  interface Shade {
    readonly black: string
    readonly white: string
    readonly transparent: string
  }

  export interface Colors {
    red: Color
    pink: Color
    purple: Color
    deepPurple: Color
    indigo: Color
    blue: Color
    lightBlue: Color
    cyan: Color
    teal: Color
    green: Color
    lightGreen: Color
    lime: Color
    yellow: Color
    amber: Color
    orange: Color
    deepOrange: Color
    brown: BaseColor
    blueGrey: BaseColor
    grey: BaseColor
    shades: Shade
  }

  const colors: Colors
  export default colors
}


import VMenu from './VMenu'

export { VMenu }
export default VMenu


import VTabs from './VTabs'
import VTab from './VTab'
import VTabsItems from './VTabsItems'
import VTabItem from './VTabItem'
import VTabsSlider from './VTabsSlider'

export { VTabs, VTab, VTabItem, VTabsItems, VTabsSlider }

export default {
  $_vuetify_subcomponents: {
    VTabs,
    VTab,
    VTabsItems,
    VTabItem,
    VTabsSlider,
  },
}