@mixin treeview-shaped($size, $margin)
  .v-treeview-node__root,
  .v-treeview-node__root:before
    border-bottom-right-radius: #{$size * 0.5} !important
    border-top-right-radius: #{$size * 0.5} !important

  .v-treeview-node__root
    margin-top: $margin
    margin-bottom: $margin

@mixin treeview-rounded($size, $margin)
  .v-treeview-node__root,
  .v-treeview-node__root:before
    border-radius: #{$size * 0.5} !important

  .v-treeview-node__root
    margin-top: $margin
    margin-bottom: $margin


signature: '(date: string, locale: string): string',

module.exports = {
  'v-ripple': {
    options: [
      {
        name: 'value',
        default: '{}',
        type: 'object',
      },
      {
        name: 'center',
        default: 'false',
        type: 'boolean',
      },
      {
        name: 'class',
        default: '""',
        type: 'string',
      },
    ],
  },
}


keep keep replace replace keep keep keep

name: 'dblclick:month',
value: 'string',

module.exports = {
  'v-date-picker': {
    events: [
      {
        name: 'input',
        value: 'string',
      },
    ],
    functions: [
      {
        name: 'titleDateFormat',
        signature: '(date: string, locale: string): string',
      },
      {
        name: 'dayFormat',
        signature: '(date: string, locale: string): string',
      },
      {
        name: 'headerDateFormat',
        signature: '(date: string, locale: string): string',
      },
      {
        name: 'monthFormat',
        signature: '(date: string, locale: string): string',
      },
      {
        name: 'yearFormat',
        signature: '(date: string, locale: string): string',
      },
      {
        name: 'allowedDates',
        signature: '(date: string): boolean',
      },
    ],
  },
}


Vuetify拥有一个12点的栅格系统，它使用<kbd>flex-box</kbd>构建，栅格用于布局应用程序的内容。它包含5种类型的媒体断点，用于定位特定的屏幕大小或方向。栅格组件的属性实际上是从它们定义的属性派生的类，这允许您轻松地将这些辅助类指定为属性，同时仍然提供在任何地方使用的类。

偏移对于补偿可能还不可见的元素或控制内容的位置很有用。就像断点一样，您可以为仍和可用的大小设置偏移量，这使您可以根据需要精确调整应用程序布局。

您可以控制栅格子项目的排序。与偏移一样，您可以为不同的尺寸设置不同的排序，设计适合任何应用程序的专门的屏幕布局。

以各种方式指定方向和对齐方式。所有可用的<kbd>flex-box</kbd> api都可以通过直观的辅助器属性获得。

更具分辨率动态更改布局。**（调整您的屏幕，并观看布局更改为一个`行`的小断点）**

栅格可以被嵌套，类似于其它框架，以实现非常自定义的布局。

Vuetify栅格系统的强大和灵活性使您可以创建出色的用户界面。

当您想要填充可用空间或在两个组件之间留出空间时，`v-spacer`组件会很有用。

有的时候您会想要将一个布局项指定为特定的标签，比如一个`section`或者`li`元素。


// Modeled after ITCSS https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/

@import './settings/_index'
@import './tools/_index'
@import './generic/_index'
@import './elements/_index'
@import './components/_index'
@import './utilities/_index'


The `v-app-bar` component is pivotal to any graphical user interface (GUI), as it generally is the primary source of site navigation. The app-bar component works great in conjunction with a [v-navigation-drawer](/components/navigation-drawers) for providing site navigation in your application.

The `v-app-bar` component is used for application-wide actions and information.

You can make **app-bar** dense. A dense app bar has lower height than regular one

An `v-app-bar` with the `prominent` prop can opt to have its height shrunk as the user scrolls down. This provides a smooth transition to taking up less visual space when the user is scrolling through content. Shrink height has 2 possible options, **dense** (48px) and **short** (56px) sizes.

`v-app-bar` can contain background images. You can set source via the `src` prop. If you need to customize the `v-img` properties, the app-bar provides you with an **img** slot.

### Prominent w/ scroll shrink and image, fading on scroll

The background image of a `v-app-bar` can fade on scroll. Use the `fade-img-on-scroll` property for this.

`v-app-bar` can be hidden on scroll. Use the `hide-on-scroll` property for this.

`v-app-bar` can have scroll threshold. It will start reacting to scroll only after defined via `scroll-threshold` property amount of pixels.

With the **collapse** and **collapse-on-scroll** props you can easily control the state of toolbar that the user interacts with.

When using the **elevate-on-scroll** prop, the `v-app-bar` will rest at an elevation of 0dp until the user begins to scroll down. Once scrolling, the bar raises to 4dp.

When using the **inverted-scroll** property, the bar will hide until the user scrolls past the designated threshold. Once past the threshold, the `v-app-bar` will continue to display until the users scrolls up past the threshold. If no **scroll-threshold** value is supplied a default value of _0_ will be used.

You can easily extend the functionality of app bar by adding `VMenu` there. Click on last icon to see it in action.

Designates that the application's `v-navigation-drawer` that is positioned on the left is below the app-bar.

Designates that the application's `v-navigation-drawer` that is positioned on the right is below the app-bar.

Puts the app-bar into a collapsed state when scrolling.

When using the **src** prop or `img` slot, will fade the image when scrolling.

Hides the app-bar when scrolling. Will still show the `extension` slot.

Hides the app-bar when scrolling down and displays it when scrolling up.

Hides the app-bar when scrolling. Will **NOT** show the `extension` slot.

Shrinks a **prominent** toolbar to a **dense** or **short** (default) one when scrolling.


"reactive": "Updates the picker model when changing months/years automatically",

"readonly": "Makes the picker readonly (doesnt't allow to select new date)",

"scrollable": "Allows changing displayed month with mouse scroll",

"showCurrent": "Toggles visibility of the current date/month outline or shows the provided date/month as a current",

"showWeek": "Toggles visibility of the week numbers in the body of the calendar",

"titleDateFormat": "Allows you to customize the format of the date string that appears in the title of the date picker. Called with date (ISO 8601 string) arguments.",

"type": "Determines the type of the picker - `date` for date picker, `month` for month picker",

"value": "Date picker model (ISO 8601 format, YY-mm-dd or YY-mm)",

"selectedItemText": "Text used for translating the number of selected dates when using *multiple* prop. Can also be customizing globally in [Internationalization](/customization/internationalization).",


<template>
  <v-container>
    <v-layout column>
      Icons

      <core-title>
        Basic
      </core-title>
      <core-section>
        <v-layout
          justify-space-around
          class="mb-2"
        >

            <v-icon>mdi-home</v-icon>
            <v-icon>mdi-calendar</v-icon>
            <v-icon>mdi-information</v-icon>

            <v-icon dark>
              mdi-folder-open
            </v-icon>
            <v-icon dark>
              mdi-widgets
            </v-icon>
            <v-icon dark>
              mdi-gavel
            </v-icon>

        </v-layout>

        <v-layout
          justify-space-around
          class="mb-2"
        >

            <v-icon medium>
              mdi-home
            </v-icon>
            <v-icon medium>
              mdi-calendar
            </v-icon>
            <v-icon medium>
              mdi-information
            </v-icon>

            <v-icon
              medium
              dark
            >
              mdi-folder-open
            </v-icon>
            <v-icon
              medium
              dark
            >
              mdi-widgets
            </v-icon>
            <v-icon
              medium
              dark
            >
              mdi-gavel
            </v-icon>

        </v-layout>

        <v-layout
          justify-space-around
          class="mb-2"
        >

            <v-icon large>
              mdi-home
            </v-icon>
            <v-icon large>
              mdi-calendar
            </v-icon>
            <v-icon large>
              mdi-information
            </v-icon>

            <v-icon
              large
              dark
            >
              mdi-folder-open
            </v-icon>
            <v-icon
              large
              dark
            >
              mdi-widgets
            </v-icon>
            <v-icon
              large
              dark
            >
              mdi-gavel
            </v-icon>

        </v-layout>

        <v-layout justify-space-around>

            <v-icon x-large>
              mdi-home
            </v-icon>
            <v-icon x-large>
              mdi-calendar
            </v-icon>
            <v-icon x-large>
              mdi-information
            </v-icon>

            <v-icon
              x-large
              dark
            >
              mdi-folder-open
            </v-icon>
            <v-icon
              x-large
              dark
            >
              mdi-widgets
            </v-icon>
            <v-icon
              x-large
              dark
            >
              mdi-gavel
            </v-icon>

        </v-layout>
      </core-section>

      <core-title>
        Colors
      </core-title>
      <core-section>
        <v-layout
          justify-space-around
          class="mb-2"
        >

            <v-icon color="primary">
              mdi-home
            </v-icon>
            <v-icon color="primary">
              mdi-calendar
            </v-icon>
            <v-icon color="primary">
              mdi-information
            </v-icon>

            <v-icon color="teal">
              mdi-folder-open
            </v-icon>
            <v-icon color="teal">
              mdi-widgets
            </v-icon>
            <v-icon color="teal">
              mdi-gavel
            </v-icon>

        </v-layout>

        <v-layout
          justify-space-around
          class="mb-2"
        >

            <v-icon
              color="primary"
              medium
            >
              mdi-home
            </v-icon>
            <v-icon
              color="primary"
              medium
            >
              mdi-calendar
            </v-icon>
            <v-icon
              color="primary"
              medium
            >
              mdi-information
            </v-icon>

            <v-icon
              medium
              color="teal"
            >
              mdi-folder-open
            </v-icon>
            <v-icon
              medium
              color="teal"
            >
              mdi-widgets
            </v-icon>
            <v-icon
              medium
              color="teal"
            >
              mdi-gavel
            </v-icon>

        </v-layout>

        <v-layout
          justify-space-around
          class="mb-2"
        >

            <v-icon
              color="primary"
              large
            >
              mdi-home
            </v-icon>
            <v-icon
              color="primary"
              large
            >
              mdi-calendar
            </v-icon>
            <v-icon
              color="primary"
              large
            >
              mdi-information
            </v-icon>

            <v-icon
              large
              color="teal"
            >
              mdi-folder-open
            </v-icon>
            <v-icon
              large
              color="teal"
            >
              mdi-widgets
            </v-icon>
            <v-icon
              large
              color="teal"
            >
              mdi-gavel
            </v-icon>

        </v-layout>

        <v-layout justify-space-around>

            <v-icon
              color="primary"
              x-large
            >
              mdi-home
            </v-icon>
            <v-icon
              color="primary"
              x-large
            >
              mdi-calendar
            </v-icon>
            <v-icon
              color="primary"
              x-large
            >
              mdi-information
            </v-icon>

            <v-icon
              x-large
              color="teal"
            >
              mdi-folder-open
            </v-icon>
            <v-icon
              x-large
              color="teal"
            >
              mdi-widgets
            </v-icon>
            <v-icon
              x-large
              color="teal"
            >
              mdi-gavel
            </v-icon>

        </v-layout>
      </core-section>

      <core-title>
        Dense
      </core-title>
      <core-section>
        <v-layout
          justify-space-around
          class="mb-2"
        >

            <v-icon
              color="primary"
              dense
            >
              mdi-home
            </v-icon>
            <v-icon
              color="primary"
              dense
            >
              mdi-calendar
            </v-icon>
            <v-icon
              color="primary"
              dense
            >
              mdi-information
            </v-icon>

            <v-icon
              color="teal"
              dense
            >
              mdi-folder-open
            </v-icon>
            <v-icon
              color="teal"
              dense
            >
              mdi-widgets
            </v-icon>
            <v-icon
              color="teal"
              dense
            >
              mdi-gavel
            </v-icon>

        </v-layout>

        <v-layout
          justify-space-around
          class="mb-2"
        >

            <v-icon
              color="primary"
              medium
              dense
            >
              mdi-home
            </v-icon>
            <v-icon
              color="primary"
              medium
              dense
            >
              mdi-calendar
            </v-icon>
            <v-icon
              color="primary"
              medium
              dense
            >
              mdi-information
            </v-icon>

            <v-icon
              medium
              color="teal"
              dense
            >
              mdi-folder-open
            </v-icon>
            <v-icon
              medium
              color="teal"
              dense
            >
              mdi-widgets
            </v-icon>
            <v-icon
              medium
              color="teal"
              dense
            >
              mdi-gavel
            </v-icon>

        </v-layout>

        <v-layout
          justify-space-around
          class="mb-2"
        >

            <v-icon
              color="primary"
              large
              dense
            >
              mdi-home
            </v-icon>
            <v-icon
              color="primary"
              large
              dense
            >
              mdi-calendar
            </v-icon>
            <v-icon
              color="primary"
              large
              dense
            >
              mdi-information
            </v-icon>

            <v-icon
              large
              color="teal"
              dense
            >
              mdi-folder-open
            </v-icon>
            <v-icon
              large
              color="teal"
              dense
            >
              mdi-widgets
            </v-icon>
            <v-icon
              large
              color="teal"
              dense
            >
              mdi-gavel
            </v-icon>

        </v-layout>

        <v-layout justify-space-around>

            <v-icon
              color="primary"
              x-large
              dense
            >
              mdi-home
            </v-icon>
            <v-icon
              color="primary"
              x-large
              dense
            >
              mdi-calendar
            </v-icon>
            <v-icon
              color="primary"
              x-large
              dense
            >
              mdi-information
            </v-icon>

            <v-icon
              x-large
              color="teal"
              dense
            >
              mdi-folder-open
            </v-icon>
            <v-icon
              x-large
              color="teal"
              dense
            >
              mdi-widgets
            </v-icon>
            <v-icon
              x-large
              color="teal"
              dense
            >
              mdi-gavel
            </v-icon>

        </v-layout>
      </core-section>
    </v-layout>
  </v-container>
</template>


Agregar al final un ícono al componente, utiliza la misma sintaxis que `v-icon`

Obliga a la pista a estar visible siempre.

Configura el texto de placeholder del input.

Agregar al inicio un ícono al componente, utiliza la misma sintaxis que `v-icon`

Pone el input en estado de sólo lectura.

Denota que el input es requerido. Agrega un asterisco al final de la etiqueta. No lleva a cabo ninguna validación.

Arreglo de códigos de tecla que variarán el valor del input (si soporta variaciones en el valor).


When you run into a roadblock, you need assistance right away. Vuetify offers support in our massive community on Discord.

Vibrant Community

Be prepared for an armada of specialized components at your disposal. With over 80 in total, there is a solution to any situation.

Semantic Material Components

Vuetify supports the future of Vue tooling through its vue-cli-3 plugin. With one command you are ready to start building your next great idea.

Ready-Made Project Scaffolding


"weekdayFormat": "Allows you to customize the format of the weekday string that appears in the body of the calendar. Called with date (ISO 8601 string) arguments.",

"yearFormat": "Allows you to customize the format of the year string that appears in the header of the calendar. Called with date (ISO 8601 string) arguments.",

"yearIcon": "Sets the icon in the year selection button"

You can specify allowed dates using arrays, objects, and functions.

Starting with year picker by default, resticting dates range and closing the picker menu after selecting the day make the perfect birthday picker.

Date picker colors can be set using the `color` and `header-color` props. If `header-color` prop is not provided header will use the `color` prop value.

By default the current date is displayed using outline button - **show-current** prop allows you to remove the border or select different date to be displayed as the current one.

### Date pickers - current date indicator

When integrating a picker into a `v-text-field`, it is recommended to use the **readonly** prop. This will prevent mobile keyboards from triggering. To save vertical space, you can also hide the picker title.

  Pickers expose a slot that allow you to hook into save and cancel functionality. This will maintain an old value which can be replaced if the user cancels.

You can specify events using arrays, objects or functions. To change the default color of the event use **event-color** prop. Your **events** function or object can return an array of colors (material or css) in case you want to display multiple event indicators.

If you need to display date in the custom format (different than YYYY-MM-DD) you need to use the formatting function.

Formatting dates is possible also with external libs such as Moment.js or date-fns

### Date pickers - formatting date using external libs

You can override the default icons used in the picker.

The date picker supports internationalization through the JavaScript Date object. Specify a BCP 47 language tag using the `locale` prop, and then set the first day of the week with the `first-day-of-week` prop.

Date picker can now select multiple dates with the `multiple` prop. If using `multiple` then date picker expects its model to be an array.

You can watch the `pickerDate` which is the displayed month/year (depending on the picker type and active view) to perform some action when it changes.

### Date pickers - react to displayed month/year change

Selecting new date could be disabled by adding **readonly** prop.

You can specify allowed the picker's width or make it full width.

You can specify allowed months using arrays, objects or functions.

Month picker colors can be set using the `color` and `header-color` props. If `header-color` prop is not provided header will use the `color` prop value.

By default the current month is displayed using outline button - **show-current** prop allows you to remove the border or select different month to be displayed as the current one.

### Month pickers - current month indicator

The month picker supports internationalization through the JavaScript Date object. Specify a BCP 47 language tag using the `locale` prop, and then set the first day of the week with the `first-day-of-week` prop.

### Month pickers - Internationalization

Month pickers come in two orientation variations, portrait **(default)** and landscape.

Month pickers can now select multiple months with the `multiple` prop. If using `multiple` then the month picker expects its model to be an array.

### Month pickers - Setting picker width

Date pickers come in two orientation variations, portrait **(default)** and landscape. By default they are emitting `input` event when the day (for date picker) or month (for month picker), but with **reactive** prop they can update the model even after clicking year/month.

The `v-date-picker` is stand-alone component that can be utilized in many existing Vuetify components. It offers the user a visual representation for selecting date/month.

Allows you to customize the format of the day string that appears in the date table. Called with date (ISO 8601 string) arguments.

Sets the color for event dot. It can be string (all events will have the same color) or `object` where attribute is the event date and value is boolean/color/array of colors for specified date or `function` taking date as a parameter and returning boolean/color/array of colors for that date

Array of dates or object defining events or colors or function returning boolean/color/array of colors

Sets the first day of the week, starting with 0 for Sunday.

Allows you to customize the format of the month string that appears in the header of the calendar. Called with date (ISO 8601 string) arguments.

Sets the locale. Accepts a string with a BCP 47 language tag.

Maximum allowed date/month (ISO 8601 format)

Minimum allowed date/month (ISO 8601 format)

Formatting function used for displaying months in the months table. Called with date (ISO 8601 string) arguments.

Sets the icon for next month/year button

Sets the icon for previous month/year button

Updates the picker model when changing months/years automatically

Makes the picker readonly (doesnt't allow to select new date)

Allows changing displayed month with mouse scroll

Toggles visibility of the current date/month outline or shows the provided date/month as a current

Toggles visibility of the week numbers in the body of the calendar

Allows you to customize the format of the date string that appears in the title of the date picker. Called with date (ISO 8601 string) arguments.

Determines the type of the picker - `date` for date picker, `month` for month picker

Date picker model (ISO 8601 format, YY-mm-dd or YY-mm)

Formats day string that appears in the date table to specified locale

Formats current date string that appears in the header to specified locale

Formats month string that appears in the months table to specified locale

Formats date string that appears in the title to specified locale

Formats year string that appears in the header to specified locale


<template>
  <v-layout justify-space-between wrap>
    <v-flex xs12 sm6 class="my-4">

Defined by array

      <v-date-picker
        v-model="date1"
        :events="arrayEvents"
        event-color="green lighten-1"
        type="month"
      ></v-date-picker>
    </v-flex>
    <v-flex xs12 sm6 class="my-4">

Defined by function

      <v-date-picker
        v-model="date2"
        :events="functionEvents"
        type="month"
      ></v-date-picker>
    </v-flex>
  </v-layout>
</template>


<template>
  <div>
    <v-expansion-panels class="mb-6">
      <v-expansion-panel
        v-for="(item,i) in 5"
        :key="i"
      >
        <v-expansion-panel-header expand-icon="mdi-menu-down">Item</v-expansion-panel-header>
        <v-expansion-panel-content>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</v-expansion-panel-content>
      </v-expansion-panel>
    </v-expansion-panels>

    <v-expansion-panels>
      <v-expansion-panel>
        <v-expansion-panel-header>
          Item
          <template v-slot:actions>
            <v-icon color="primary">$vuetify.icons.expand</v-icon>
          </template>
        </v-expansion-panel-header>
        <v-expansion-panel-content>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
        </v-expansion-panel-content>
      </v-expansion-panel>

      <v-expansion-panel>
        <v-expansion-panel-header disable-icon-rotate>
          Item
          <template v-slot:actions>
            <v-icon color="teal">done</v-icon>
          </template>
        </v-expansion-panel-header>
        <v-expansion-panel-content>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
        </v-expansion-panel-content>
      </v-expansion-panel>

      <v-expansion-panel>
        <v-expansion-panel-header disable-icon-rotate>
          Item
          <template v-slot:actions>
            <v-icon color="error">error</v-icon>
          </template>
        </v-expansion-panel-header>
        <v-expansion-panel-content>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
        </v-expansion-panel-content>
      </v-expansion-panel>
    </v-expansion-panels>
  </div>
</template>


// Components
import VListItemAvatar from '../VListItemAvatar'

// Utilities
import {
  mount,
  Wrapper,
} from '@vue/test-utils'

// Types
import { ExtractVue } from '../../../util/mixins'

describe('VListItemAvatar.ts', () => {
  type Instance = ExtractVue<typeof VListItemAvatar>
  let mountFunction: (options?: object) => Wrapper<Instance>

  beforeEach(() => {
    mountFunction = (options = {}) => {
      return mount(VListItemAvatar, {
        ...options,
      })
    }
  })

  it('should render component and match snapshot', () => {
    const wrapper = mountFunction()

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


"allowedDates": "Checks if date is allowed by picker",

"dayFormat": "Formats day string that appears in the date table to specified locale",

"headerDateFormat": "Formats current date string that appears in the header to specified locale",

"monthFormat": "Formats month string that appears in the months table to specified locale",

"titleDateFormat": "Formats date string that appears in the title to specified locale",

"yearFormat": "Formats year string that appears in the header to specified locale"

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

"update:picker-date": "The `.sync` event for `picker-date` prop",

"click:date": "Emitted when the date button is clicked",

"dblclick:date": "Emitted when the date button is double clicked",

"click:month": "Emitted when the month button is clicked",

"dblclick:month": "Emitted when the month button is double clicked. Usable only for `month` pickers.",

"change": "Reactive date picker emits `input` even when any part of the date (year/month/day) changes, but `change` event is emitted only when the day (for date pickers) or month (for month pickers) changes."

The `v-time-picker` is stand-alone component that can be utilized in many existing Vuetify components. It offers the user a visual representation for selecting the time.

Time pickers have the light theme enabled by default.

Time picker colors can be set using the `color` and `header-color` props. If `header-color` prop is not provided  header will use the `color` prop value.

Due to the flexibility of pickers, you can really dial in the experience exactly how you want it.

A time picker can be switched to 24hr format. Note that the `format` prop defines only the way the picker is displayed, picker's value (model) is always in 24hr format.

You can specify allowed times using arrays, objects, and functions. You can also specify time step/precision/interval - e.g. 10 minutes.

You can move AM/PM switch to picker's title.

You can't interact with disabled picker.

Read-only picker behaves same as disabled one, but looks like default one.

You can edit time picker's value using mouse wheel.

This is an example of joining pickers together using `min` and `max` prop.

Defines the format of a time displayed in picker. Available options are `ampm` and `24hr`.

Allows changing hour/minute with mouse scroll

Time picker model (ISO 8601 format, 24hr hh:mm)

Place AM/PM switch in title, not near the clock.


流畅的动画有助于给用户界面带来很棒的感觉。使用Vue的过渡系统和可复用的功能组件，您可以轻松地控制应用程序的动画效果。大多数组件可以通过`transition`属性来改变他们的过渡。

滑动x过渡可沿水平方向移动。

许多Vuetify组件都包含一个`transition`属性允许你指定你自己的效果

另一个淡入淡出过渡的示例可以在圆盘传送带组件上找到。

通过使用一个简单的属性来可编程地控制过渡原点。

您可以用Vuetify的过渡辅助器函数来创建您自己的自定义的过渡效果，这个函数会返回一个可以导入到Vue的对象。使用Vue的 [函数式组件](https://vuejs.org/v2/guide/render-function.html#Functional-Components) 配置将确保您的过渡效果尽可能高效。只需要导入函数：

其中`createSimpleTransition`函数接受1个参数，即名称。这将是您可以与您的样式挂钩的名称。这是一个展示`v-fade-transition`的示例：


"readonly": "Puts picker in readonly state",

"useSeconds": "Toggles the use of seconds in picker",

"value": "Time picker model (ISO 8601 format, 24hr hh:mm)",

"ampmInTitle": "Place AM/PM switch in title, not near the clock."

You can specify allowed dates using arrays, objects, and functions.

Starting with year picker by default, resticting dates range and closing the picker menu after selecting the day make the perfect birthday picker.

Date picker colors can be set using the `color` and `header-color` props. If `header-color` prop is not provided header will use the `color` prop value.

By default the current date is displayed using outline button - **show-current** prop allows you to remove the border or select different date to be displayed as the current one.

When integrating a picker into a `v-text-field`, it is recommended to use the **readonly** prop. This will prevent mobile keyboards from triggering. To save vertical space, you can also hide the picker title.

  Pickers expose a slot that allow you to hook into save and cancel functionality. This will maintain an old value which can be replaced if the user cancels.

You can specify events using arrays, objects or functions. To change the default color of the event use **event-color** prop. Your **events** function or object can return an array of colors (material or css) in case you want to display multiple event indicators.

If you need to display date in the custom format (different than YYYY-MM-DD) you need to use the formatting function.

Formatting dates is possible also with external libs such as Moment.js or date-fns

You can override the default icons used in the picker.

The date picker supports internationalization through the JavaScript Date object. Specify a BCP 47 language tag using the `locale` prop, and then set the first day of the week with the `first-day-of-week` prop.

Date picker can now select multiple dates with the `multiple` prop. If using `multiple` then date picker expects its model to be an array.

You can watch the `pickerDate` which is the displayed month/year (depending on the picker type and active view) to perform some action when it changes.

Selecting new date could be disabled by adding **readonly** prop.

You can specify allowed the picker's width or make it full width.

You can specify allowed months using arrays, objects or functions.

Month picker colors can be set using the `color` and `header-color` props. If `header-color` prop is not provided header will use the `color` prop value.

By default the current month is displayed using outline button - **show-current** prop allows you to remove the border or select different month to be displayed as the current one.

The month picker supports internationalization through the JavaScript Date object. Specify a BCP 47 language tag using the `locale` prop, and then set the first day of the week with the `first-day-of-week` prop.

Month pickers come in two orientation variations, portrait **(default)** and landscape.

Month pickers can now select multiple months with the `multiple` prop. If using `multiple` then the month picker expects its model to be an array.

Date pickers come in two orientation variations, portrait **(default)** and landscape. By default they are emitting `input` event when the day (for date picker) or month (for month picker), but with **reactive** prop they can update the model even after clicking year/month.


Configure the active CSS class applied when the link is active. You can find more information about the [**active-class** prop](https://router.vuejs.org/api/#active-class) on the vue-router documentation.

Setting **append** prop always appends the relative path to the current path. You can find more information about the [**append** prop](https://router.vuejs.org/api/#append) on the vue-router documentation.

Removes the ability to click or target the component.

Exactly match the link. Without this, '/' will match every route. You can find more information about the [**exact** prop](https://router.vuejs.org/api/#exact) on the vue-router documentation.

Configure the active CSS class applied when the link is active with exact match. You can find more information about the [**exact-active-class** prop](https://router.vuejs.org/api/#exact-active-class) on the vue-router documentation.

Designates the component as anchor and applies the **href** attribute.

Designates that the component is a link. This is automatic when using the **href** or **to** prop.

Specifies the link is a `nuxt-link`. For use with the [nuxt framework](https://nuxtjs.org/api/components-nuxt-link/).

Setting **replace** prop will call `router.replace()` instead of `router.push()` when clicked, so the navigation will not leave a history record. You can find more information about the [**replace** prop](https://router.vuejs.org/api/#replace) on the vue-router documentation.

Denotes the target route of the link. You can find more information about the [**to** prop](https://router.vuejs.org/api/#to) on the vue-router documentation.

Designates the target attribute. This should only be applied when using the **href** prop.


<template>
  <v-container>
    <v-layout column>
      <main-header>Sparklines</main-header>

      <core-title>
        Basic
      </core-title>
      <core-section>
        <v-sparkline
          :value="value"
          auto-draw
        />
      </core-section>

      <core-title>
        Gradient
      </core-title>
      <core-section>
        <v-sparkline
          :value="value"
          :gradient="['#00c6ff', '#F0F', '#FF0']"
          smooth="10"
          line-width="5"
          padding="8"
          stroke-linecap="round"
          auto-draw
        />
      </core-section>

      <core-title>
        With labels
      </core-title>
      <core-section>
        <v-sparkline
          :labels="labels"
          :value="value"
          color="red"
          line-width="2"
          padding="16"
          smooth="5"
        />
      </core-section>
    </v-layout>
  </v-container>
</template>

<script>
  export default {
    name: 'Sparklines',

    data: () => ({
      value: [0, 2, 5, 9, 5, 10, 3, 5, 0, 0, 1, 8, 2, 9, 0],
      labels: [
        '1',
        '2',
        '3',
        '4',
        '5',
        '6',
        '7',
        '8',
        '9',
        '10',
        '11',
        '12',
        '13',
        '14',
        '15',
      ],
    }),
  }
</script>


툴바가 일반적으로 가장 중요한 사이트 탐색 방법인 것 처럼 `v-toolbar` 컴포넌트 역시 모든 그래픽 환경에서 충추적인 컴포넌트 입니다. The toolbar component works great in conjunction with a navigation drawer for hiding links and presenting an activator to open the sidebar on mobile.

툴바는 여러 방법으로 쓰일 수 있는 유연한 컨테이너 입니다. 기본적으로 툴바는 데스크탑에서 64px, 세로 모바일에서 56px, 가로 모바일에서 48px 높이를 가집니다. 툴바와 함께 쓰일 수 있는 `v-app-bar-nav-icon`, `v-toolbar-title`, `v-toolbar-items` 같은 여러 헬퍼 컴포넌트가 있습니다.

툴바는 라이트/다크 두개의 변형이 있습니다. 라이트 툴바는 다크 버튼과 다크 텍스트를 가지고 다크 툴바는 흰색 버튼과 흰색 텍스트를 가집니다.

유연한 툴바와 카드 툴바 (Flexible toolbar and card toolbar)

소수의 커스텀 스타일로 놀라운 인터페이스를 만들 수 있습니다.

플로팅 툴바는 툴바가 참조하는 컨텡츠 위에 위치할 수 있습니다.

앱바는 테마와 헬퍼 클라스들을 적용하여 다양한 변형을 만들 수 있습니다. 라이트, 다크 테마부터 색상, 투명한 변형도 가능합니다.

두드러지는 툴바는 확정된 공간에 요소를 추가할 수 있는 슬롯을 추가합니다. 컨텐츠와 확장 공간은 64px 높이로 고정됩니다.

밀집된 툴바는 48px 높이를 사용합니다. 이는 가로 모바일 모드의 기본 작동방식이기도 합니다.

밀집되고 두드러진 툴바

더 작은 툴바를 위해서 두드러짐과 밀집됨을 결합할 수 있습니다.

툴바는 스크롤시에 트렌지션되어 사라질 수(transition off) 될 수 있습니다. 이 예제는 어떻게 스크롤 타겟을 정의할 수 있는지(기본값은 window) 보여줍니다. 이 예제에서 일반적인 어플리케이션에 아마도 필요없을 특별한 마크업이 사용되었다는 것을 기억하세요

시스템 바 - 상태/윈도우 (System bars - status/window)

시스템바는 모바일과 데스크탑에서 알람 아이콘을 보여주는데 유용합니다. For live examples of, head to the pre-defined layouts.

툴바 컨텢츠와 확장의 높이를 줄임

툴바가 슨롯을 사용하지 않고 확장을 생성하도록 강제 (Force the toolbar to generate the extension without using the slot)

툴바의 박스 그림자를 제거

시스템바의 투명도를 줄임

Increases the height of the toolbar content and extension

시스템 바의 높이를 감소

시스템 바의 높이를 증가


"click:hour": "Emitted when user selects the hour",

"click:minute": "Emitted when user selects the minute",

"click:second": "Emitted when user selects the second",

"change": "Emitted when the time selection is done (when user changes the minute for HH:MM picker and the second for HH:MM:SS picker"


<template>

 0"
      class="px-2"
    >
      MISSING ITEMS:
      {{ missingItems.join(', ') }}

    <v-data-iterator
      :search="search"
      :items="computedItems"
      sort-by="name"
      :items-per-page="-1"
      class="component-parameters"
      hide-default-footer
    >
      <template #default="{ items }">

          <template v-for="(item, i) in items">
            <doc-api-item
              :key="item.name"
              :headers="headers"
              :item="item"
            />
            <v-divider
              v-if="i + 1!== items.length"
              :key="`divider-${i}`"
            />
          </template>

      </template>
    </v-data-iterator>

</template>


this.tryEnsureIndex({ instanceId: 1, sessionId: 1, userId: 1 });

this.tryEnsureIndex({ instanceId: 1, sessionId: 1 });

this.tryEnsureIndex({ year: 1, month: 1, day: 1, type: 1 });

this.tryEnsureIndex({ ip: 1, loginAt: 1 });

import { Promise } from 'meteor/promise';

import { Migrations } from '../../../app/migrations';
import { Sessions } from '../../../app/models/server/raw';
import { getMostImportantRole } from '../../../app/statistics/server/lib/getMostImportantRole';

async function migrateSessions() {
	const cursor = Sessions.col.aggregate([{
		$match: { $or: [{ mostImportantRole: { $exists: 0 } }, { mostImportantRole: null }] },
	}, {
		$group: {
			_id: '$userId',
		},
	}, {
		$lookup: {
			from: 'users',
			localField: '_id',
			foreignField: '_id',
			as: 'user',
		},
	}, {
		$unwind: '$user',
	}, {
		$project: {
			'user.roles': 1,
		},
	}, {
		$match: { 'user.roles.0': { $exists: 1 } },
	}]);

	for await (const session of cursor) {
		await Sessions.col.updateMany({ userId: session._id }, { $set: { mostImportantRole: getMostImportantRole(session.user.roles) } });
	}
}

Migrations.add({
	version: 211,
	up() {
		Promise.await(migrateSessions());
	},
});


import { Meteor } from 'meteor/meteor';

import { settings } from '../../settings';

Meteor.startup(function() {
	const enableQuery = {
		_id: 'AutoLinker',
		value: true,
	};

	settings.add('AutoLinker', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nLabel: 'Enabled' });

	settings.add('AutoLinker_StripPrefix', false, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nDescription: 'AutoLinker_StripPrefix_Description', enableQuery });
	settings.add('AutoLinker_Urls_Scheme', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery });
	settings.add('AutoLinker_Urls_www', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery });
	settings.add('AutoLinker_Urls_TLD', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery });
	settings.add('AutoLinker_UrlsRegExp', '(://|www\\.).+', { type: 'string', group: 'Message', section: 'AutoLinker', public: true, enableQuery });
	settings.add('AutoLinker_Email', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery });
	settings.add('AutoLinker_Phone', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nDescription: 'AutoLinker_Phone_Description', enableQuery });
});


import { Meteor } from 'meteor/meteor';

import { Permissions } from '../../models';

Meteor.startup(() => {
	if (Permissions) {
		if (!Permissions.findOne({ _id: 'auto-translate' })) {
			Permissions.insert({ _id: 'auto-translate', roles: ['admin'] });
		}
	}
});


import { callbacks } from '../../../../../app/callbacks';
import { settings } from '../../../../../app/settings';
import { dispatchWaitingQueueStatus } from '../lib/Helper';
import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager';

const onCloseLivechat = (room) => {
	if (!settings.get('Livechat_waiting_queue')) {
		return room;
	}

	const { departmentId } = room || {};
	if (!RoutingManager.getConfig().autoAssignAgent) {
		dispatchWaitingQueueStatus(departmentId);
		return room;
	}

	return room;
};

callbacks.add('livechat.closeRoom', onCloseLivechat, callbacks.priority.HIGH, 'livechat-waiting-queue-monitor-close-room');


$match: { 'user.roles.0': { $exists: 1 } },


import { Tracker } from 'meteor/tracker';
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';

import { roomTypes } from '../../../../utils/client';
import { call } from '../../../../ui-utils/client';
import './CreateDirectMessage.html';

Template.CreateDirectMessage.helpers({
	onSelectUser() {
		return Template.instance().onSelectUser;
	},
	createIsDisabled() {
		return Template.instance().selectedUsers.get().length === 0 ? 'disabled' : '';
	},
	parentChannel() {
		const instance = Template.instance();
		return instance.parentChannel.get();
	},
	selectedUsers() {
		const myUsername = Meteor.user().username;
		const { message } = this;
		const users = Template.instance().selectedUsers.get().map((e) => e);
		if (message) {
			users.unshift(message.u);
		}
		return users.filter(({ username }) => myUsername !== username);
	},

	onClickTagUser() {
		return Template.instance().onClickTagUser;
	},
	deleteLastItemUser() {
		return Template.instance().deleteLastItemUser;
	},
	onClickTagRoom() {
		return Template.instance().onClickTagRoom;
	},
	selectedRoom() {
		return Template.instance().selectedRoom.get();
	},
	userModifier() {
		return (filter, text = '') => {
			const f = filter.get();
			return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `<strong>${ part }</strong>`) }`;
		};
	},
	nameSuggestion() {
		return Template.instance().discussionName.get();
	},
});

const waitUntilRoomBeInserted = async (rid) => new Promise((resolve) => {
	Tracker.autorun((c) => {
		const room = roomTypes.findRoom('d', rid, Meteor.user());
		if (room) {
			c.stop();
			return resolve(room);
		}
	});
});

Template.CreateDirectMessage.events({
	async 'submit #create-dm, click .js-save-dm'(event, instance) {
		event.preventDefault();
		const users = instance.selectedUsers.get().map(({ username }) => username).filter((value, index, self) => self.indexOf(value) === index);

		const result = await call('createDirectMessage', ...users);

		if (instance.data.onCreate) {
			instance.data.onCreate(result);
		}

		await waitUntilRoomBeInserted(result.rid);

		const user = Meteor.user();
		roomTypes.openRouteLink(result.t, { ...result, name: result.usernames.filter((username) => username !== user.username).join(', ') });
	},
});

Template.CreateDirectMessage.onRendered(function() {
	this.find('#directMessageUsers').focus();
});

Template.CreateDirectMessage.onCreated(function() {
	this.selectedUsers = new ReactiveVar([]);

	this.onSelectUser = ({ item: user }) => {
		if (user.username === Meteor.user().username) {
			return;
		}
		const users = this.selectedUsers.get();
		if (!users.find((u) => user.username === u.username)) {
			this.selectedUsers.set([...users, user]);
		}
	};

	this.onClickTagUser = ({ username }) => {
		this.selectedUsers.set(this.selectedUsers.get().filter((user) => user.username !== username));
	};

	this.deleteLastItemUser = () => {
		const arr = this.selectedUsers.get();
		arr.pop();
		this.selectedUsers.set(arr);
	};
});


import s from 'underscore.string';

import { BaseRaw } from './BaseRaw';

export class RoomsRaw extends BaseRaw {
	findOneByRoomIdAndUserId(rid, uid, options) {
		const query = {
			rid,
			'u._id': uid,
		};

		return this.findOne(query, options);
	}

	async getMostRecentAverageChatDurationTime(numberMostRecentChats, department) {
		const aggregate = [
			{
				$match: {
					t: 'l',
					closedAt: { $exists: true },
					metrics: { $exists: true },
					'metrics.chatDuration': { $exists: true },
					...department && { departmentId: department },
				},
			},
			{ $sort: { closedAt: -1 } },
			{ $limit: numberMostRecentChats },
			{ $group: { _id: null, chats: { $sum: 1 }, sumChatDuration: { $sum: '$metrics.chatDuration' } } },
			{ $project: { _id: '$_id', avgChatDuration: { $divide: ['$sumChatDuration', '$chats'] } } },
		];

		const [statistic] = await this.col.aggregate(aggregate).toArray();
		return statistic;
	}

	findByNameContainingAndTypes(name, types, discussion = false, options = {}) {
		const nameRegex = new RegExp(s.escapeRegExp(name).trim(), 'i');
		const query = {
			t: {
				$in: types,
			},
			prid: { $exists: discussion },
			$or: [
				{ name: nameRegex },
				{
					t: 'd',
					usernames: nameRegex,
				},
			],
		};
		return this.find(query, options);
	}

	findByTypes(types, discussion = false, options = {}) {
		const query = {
			t: {
				$in: types,
			},
			prid: { $exists: discussion },
		};
		return this.find(query, options);
	}

	findByNameContaining(name, discussion = false, options = {}) {
		const nameRegex = new RegExp(s.escapeRegExp(name).trim(), 'i');

		const query = {
			prid: { $exists: discussion },
			$or: [
				{ name: nameRegex },
				{
					t: 'd',
					usernames: nameRegex,
				},
			],
		};
		return this.find(query, options);
	}

	findChannelAndPrivateByNameStarting(name, options) {
		const nameRegex = new RegExp(`^${ s.escapeRegExp(name).trim() }`, 'i');

		const query = {
			t: {
				$in: ['c', 'p'],
			},
			name: nameRegex,
		};

		return this.find(query, options);
	}

	findChannelsWithNumberOfMessagesBetweenDate({ start, end, startOfLastWeek, endOfLastWeek, onlyCount = false, options = {} }) {
		const lookup = {
			$lookup: {
				from: 'rocketchat_analytics',
				localField: '_id',
				foreignField: 'room._id',
				as: 'messages',
			},
		};
		const messagesProject = {
			$project: {
				room: '$$ROOT',
				messages: {
					$filter: {
						input: '$messages',
						as: 'message',
						cond: {
							$and: [
								{ $gte: ['$$message.date', start] },
								{ $lte: ['$$message.date', end] },
							],
						},
					},
				},
				lastWeekMessages: {
					$filter: {
						input: '$messages',
						as: 'message',
						cond: {
							$and: [
								{ $gte: ['$$message.date', startOfLastWeek] },
								{ $lte: ['$$message.date', endOfLastWeek] },
							],
						},
					},
				},
			},
		};
		const messagesUnwind = {
			$unwind: {
				path: '$messages',
				preserveNullAndEmptyArrays: true,
			},
		};
		const messagesGroup = {
			$group: {
				_id: {
					_id: '$room._id',
				},
				room: { $first: '$room' },
				messages: { $sum: '$messages.messages' },
				lastWeekMessages: { $first: '$lastWeekMessages' },
			},
		};
		const lastWeekMessagesUnwind = {
			$unwind: {
				path: '$lastWeekMessages',
				preserveNullAndEmptyArrays: true,
			},
		};
		const lastWeekMessagesGroup = {
			$group: {
				_id: {
					_id: '$room._id',
				},
				room: { $first: '$room' },
				messages: { $first: '$messages' },
				lastWeekMessages: { $sum: '$lastWeekMessages.messages' },
			},
		};
		const presentationProject = {
			$project: {
				_id: 0,
				room: {
					_id: '$_id._id',
					name: { $ifNull: ['$room.name', '$room.fname'] },
					ts: '$room.ts',
					t: '$room.t',
					_updatedAt: '$room._updatedAt',
					usernames: '$room.usernames',
				},
				messages: '$messages',
				lastWeekMessages: '$lastWeekMessages',
				diffFromLastWeek: { $subtract: ['$messages', '$lastWeekMessages'] },
			},
		};
		const firstParams = [lookup, messagesProject, messagesUnwind, messagesGroup, lastWeekMessagesUnwind, lastWeekMessagesGroup, presentationProject];
		const sort = { $sort: options.sort || { messages: -1 } };
		const params = [...firstParams, sort];
		if (onlyCount) {
			params.push({ $count: 'total' });
			return this.col.aggregate(params);
		}
		if (options.offset) {
			params.push({ $skip: options.offset });
		}
		if (options.count) {
			params.push({ $limit: options.count });
		}

		return this.col.aggregate(params).toArray();
	}
}


<template name="otrFlexTab">

{{_ "Off_the_record_conversation"}}

		{{#if otrAvailable}}
			<form>

					{{#if userIsOnline}}
						{{#if established}}

								<button class="rc-button rc-button--secondary refresh">{{_ "Refresh_keys"}}</button>

								<button class="rc-button rc-button--secondary end">{{_ "End_OTR"}}</button>

						{{else}} {{#if establishing}}
							{{_ "Please_wait_while_OTR_is_being_established"}}
						{{else}}
							<button class="rc-button rc-button--secondary start">{{_ "Start_OTR"}}</button>
						{{/if}} {{/if}}
					{{else}}

{{_ "OTR_is_only_available_when_both_users_are_online"}}

					{{/if}}

			</form>
		{{else}}

{{_ "Off_the_record_conversation_is_not_available_for_your_browser_or_device"}}

		{{/if}}

</template>


await Sessions.col.updateMany({ userId: session._id }, { $set: { mostImportantRole: getMostImportantRole(session.user.roles) } });

update: { $set: { mostImportantRole: getMostImportantRole(session.user.roles) } },

await Sessions.col.bulkWrite(actions, { ordered: false });

import { Base } from './_Base';
import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred';

export const aggregates = {
	dailySessionsOfYesterday(collection, { year, month, day }) {
		return collection.aggregate([{
			$match: {
				userId: { $exists: true },
				lastActivityAt: { $exists: true },
				device: { $exists: true },
				type: 'session',
				$or: [{
					year: { $lt: year },
				}, {
					year,
					month: { $lt: month },
				}, {
					year,
					month,
					day: { $lte: day },
				}],
			},
		}, {
			$sort: {
				_id: 1,
			},
		}, {
			$project: {
				userId: 1,
				device: 1,
				day: 1,
				month: 1,
				year: 1,
				mostImportantRole: 1,
				time: { $trunc: { $divide: [{ $subtract: ['$lastActivityAt', '$loginAt'] }, 1000] } },
			},
		}, {
			$match: {
				time: { $gt: 0 },
			},
		}, {
			$group: {
				_id: {
					userId: '$userId',
					device: '$device',
					day: '$day',
					month: '$month',
					year: '$year',
				},
				mostImportantRole: { $first: '$mostImportantRole' },
				time: { $sum: '$time' },
				sessions: { $sum: 1 },
			},
		}, {
			$group: {
				_id: {
					userId: '$_id.userId',
					day: '$_id.day',
					month: '$_id.month',
					year: '$_id.year',
				},
				mostImportantRole: { $first: '$mostImportantRole' },
				time: { $sum: '$time' },
				sessions: { $sum: '$sessions' },
				devices: {
					$push: {
						sessions: '$sessions',
						time: '$time',
						device: '$_id.device',
					},
				},
			},
		}, {
			$project: {
				_id: 0,
				type: { $literal: 'user_daily' },
				_computedAt: { $literal: new Date() },
				day: '$_id.day',
				month: '$_id.month',
				year: '$_id.year',
				userId: '$_id.userId',
				mostImportantRole: 1,
				time: 1,
				sessions: 1,
				devices: 1,
			},
		}], { allowDiskUse: true });
	},

	getUniqueUsersOfYesterday(collection, { year, month, day }) {
		return collection.aggregate([{
			$match: {
				year,
				month,
				day,
				type: 'user_daily',
			},
		}, {
			$group: {
				_id: {
					day: '$day',
					month: '$month',
					year: '$year',
					mostImportantRole: '$mostImportantRole',
				},
				count: {
					$sum: 1,
				},
				sessions: {
					$sum: '$sessions',
				},
				time: {
					$sum: '$time',
				},
			},
		}, {
			$group: {
				_id: {
					day: '$day',
					month: '$month',
					year: '$year',
				},
				roles: {
					$push: {
						role: '$_id.mostImportantRole',
						count: '$count',
						sessions: '$sessions',
						time: '$time',
					},
				},
				count: {
					$sum: '$count',
				},
				sessions: {
					$sum: '$sessions',
				},
				time: {
					$sum: '$time',
				},
			},
		}, {
			$project: {
				_id: 0,
				count: 1,
				sessions: 1,
				time: 1,
				roles: 1,
			},
		}]).toArray();
	},

	getUniqueUsersOfLastMonth(collection, { year, month, day }) {
		return collection.aggregate([{
			$match: {
				type: 'user_daily',
				...aggregates.getMatchOfLastMonthToday({ year, month, day }),
			},
		}, {
			$group: {
				_id: {
					userId: '$userId',
				},
				mostImportantRole: { $first: '$mostImportantRole' },
				sessions: {
					$sum: '$sessions',
				},
				time: {
					$sum: '$time',
				},
			},
		}, {
			$group: {
				_id: {
					mostImportantRole: '$mostImportantRole',
				},
				count: {
					$sum: 1,
				},
				sessions: {
					$sum: '$sessions',
				},
				time: {
					$sum: '$time',
				},
			},
		}, {
			$group: {
				_id: 1,
				roles: {
					$push: {
						role: '$_id.mostImportantRole',
						count: '$count',
						sessions: '$sessions',
						time: '$time',
					},
				},
				count: {
					$sum: '$count',
				},
				sessions: {
					$sum: '$sessions',
				},
				time: {
					$sum: '$time',
				},
			},
		}, {
			$project: {
				_id: 0,
				count: 1,
				roles: 1,
				sessions: 1,
				time: 1,
			},
		}], { allowDiskUse: true }).toArray();
	},

	getMatchOfLastMonthToday({ year, month, day }) {
		const pastMonthLastDay = new Date(year, month - 1, 0).getDate();
		const currMonthLastDay = new Date(year, month, 0).getDate();

		const lastMonthToday = new Date(year, month - 1, day);
		lastMonthToday.setMonth(lastMonthToday.getMonth() - 1, (currMonthLastDay === day ? pastMonthLastDay : Math.min(pastMonthLastDay, day)) + 1);
		const lastMonthTodayObject = {
			year: lastMonthToday.getFullYear(),
			month: lastMonthToday.getMonth() + 1,
			day: lastMonthToday.getDate(),
		};

		if (year === lastMonthTodayObject.year && month === lastMonthTodayObject.month) {
			return {
				year,
				month,
				day: { $gte: lastMonthTodayObject.day, $lte: day },
			};
		}

		if (year === lastMonthTodayObject.year) {
			return {
				year,
				$and: [{
					$or: [{
						month: { $gt: lastMonthTodayObject.month },
					}, {
						month: lastMonthTodayObject.month,
						day: { $gte: lastMonthTodayObject.day },
					}],
				}, {
					$or: [{
						month: { $lt: month },
					}, {
						month,
						day: { $lte: day },
					}],
				}],
			};
		}

		return {
			$and: [{
				$or: [{
					year: { $gt: lastMonthTodayObject.year },
				}, {
					year: lastMonthTodayObject.year,
					month: { $gt: lastMonthTodayObject.month },
				}, {
					year: lastMonthTodayObject.year,
					month: lastMonthTodayObject.month,
					day: { $gte: lastMonthTodayObject.day },
				}],
			}, {
				$or: [{
					year: { $lt: year },
				}, {
					year,
					month: { $lt: month },
				}, {
					year,
					month,
					day: { $lte: day },
				}],
			}],
		};
	},

	getUniqueDevicesOfLastMonth(collection, { year, month, day }) {
		return collection.aggregate([{
			$match: {
				type: 'user_daily',
				...aggregates.getMatchOfLastMonthToday({ year, month, day }),
			},
		}, {
			$unwind: '$devices',
		}, {
			$group: {
				_id: {
					type: '$devices.device.type',
					name: '$devices.device.name',
					version: '$devices.device.version',
				},
				count: {
					$sum: '$devices.sessions',
				},
				time: {
					$sum: '$devices.time',
				},
			},
		}, {
			$project: {
				_id: 0,
				type: '$_id.type',
				name: '$_id.name',
				version: '$_id.version',
				count: 1,
				time: 1,
			},
		}], { allowDiskUse: true }).toArray();
	},

	getUniqueDevicesOfYesterday(collection, { year, month, day }) {
		return collection.aggregate([{
			$match: {
				year,
				month,
				day,
				type: 'user_daily',
			},
		}, {
			$unwind: '$devices',
		}, {
			$group: {
				_id: {
					type: '$devices.device.type',
					name: '$devices.device.name',
					version: '$devices.device.version',
				},
				count: {
					$sum: '$devices.sessions',
				},
				time: {
					$sum: '$devices.time',
				},
			},
		}, {
			$project: {
				_id: 0,
				type: '$_id.type',
				name: '$_id.name',
				version: '$_id.version',
				count: 1,
				time: 1,
			},
		}]).toArray();
	},

	getUniqueOSOfLastMonth(collection, { year, month, day }) {
		return collection.aggregate([{
			$match: {
				type: 'user_daily',
				'devices.device.os.name': {
					$exists: true,
				},
				...aggregates.getMatchOfLastMonthToday({ year, month, day }),
			},
		}, {
			$unwind: '$devices',
		}, {
			$group: {
				_id: {
					name: '$devices.device.os.name',
					version: '$devices.device.os.version',
				},
				count: {
					$sum: '$devices.sessions',
				},
				time: {
					$sum: '$devices.time',
				},
			},
		}, {
			$project: {
				_id: 0,
				name: '$_id.name',
				version: '$_id.version',
				count: 1,
				time: 1,
			},
		}], { allowDiskUse: true }).toArray();
	},

	getUniqueOSOfYesterday(collection, { year, month, day }) {
		return collection.aggregate([{
			$match: {
				year,
				month,
				day,
				type: 'user_daily',
				'devices.device.os.name': {
					$exists: true,
				},
			},
		}, {
			$unwind: '$devices',
		}, {
			$group: {
				_id: {
					name: '$devices.device.os.name',
					version: '$devices.device.os.version',
				},
				count: {
					$sum: '$devices.sessions',
				},
				time: {
					$sum: '$devices.time',
				},
			},
		}, {
			$project: {
				_id: 0,
				name: '$_id.name',
				version: '$_id.version',
				count: 1,
				time: 1,
			},
		}]).toArray();
	},
};

export class Sessions extends Base {
	constructor(...args) {
		super(...args);

		this.tryEnsureIndex({ instanceId: 1, sessionId: 1, year: 1, month: 1, day: 1 });
		this.tryEnsureIndex({ instanceId: 1, sessionId: 1, userId: 1 });
		this.tryEnsureIndex({ instanceId: 1, sessionId: 1 });
		this.tryEnsureIndex({ sessionId: 1 });
		this.tryEnsureIndex({ year: 1, month: 1, day: 1, type: 1 });
		this.tryEnsureIndex({ type: 1 });
		this.tryEnsureIndex({ ip: 1, loginAt: 1 });
		this.tryEnsureIndex({ _computedAt: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 45 });

		const db = this.model.rawDatabase();
		this.secondaryCollection = db.collection(this.model._name, { readPreference: readSecondaryPreferred(db) });
	}

	getUniqueUsersOfYesterday() {
		const date = new Date();
		date.setDate(date.getDate() - 1);

		const year = date.getFullYear();
		const month = date.getMonth() + 1;
		const day = date.getDate();

		return {
			year,
			month,
			day,
			data: Promise.await(aggregates.getUniqueUsersOfYesterday(this.secondaryCollection, { year, month, day })),
		};
	}

	getUniqueUsersOfLastMonth() {
		const date = new Date();
		date.setMonth(date.getMonth() - 1);

		const year = date.getFullYear();
		const month = date.getMonth() + 1;
		const day = date.getDate();

		return {
			year,
			month,
			day,
			data: Promise.await(aggregates.getUniqueUsersOfLastMonth(this.secondaryCollection, { year, month, day })),
		};
	}

	getUniqueDevicesOfYesterday() {
		const date = new Date();
		date.setDate(date.getDate() - 1);

		const year = date.getFullYear();
		const month = date.getMonth() + 1;
		const day = date.getDate();

		return {
			year,
			month,
			day,
			data: Promise.await(aggregates.getUniqueDevicesOfYesterday(this.secondaryCollection, { year, month, day })),
		};
	}

	getUniqueDevicesOfLastMonth() {
		const date = new Date();
		date.setDate(date.getDate() - 1);

		const year = date.getFullYear();
		const month = date.getMonth() + 1;
		const day = date.getDate();

		return {
			year,
			month,
			day,
			data: Promise.await(aggregates.getUniqueDevicesOfLastMonth(this.secondaryCollection, { year, month, day })),
		};
	}

	getUniqueOSOfYesterday() {
		const date = new Date();
		date.setDate(date.getDate() - 1);

		const year = date.getFullYear();
		const month = date.getMonth() + 1;
		const day = date.getDate();

		return {
			year,
			month,
			day,
			data: Promise.await(aggregates.getUniqueOSOfYesterday(this.secondaryCollection, { year, month, day })),
		};
	}

	getUniqueOSOfLastMonth() {
		const date = new Date();
		date.setDate(date.getDate() - 1);

		const year = date.getFullYear();
		const month = date.getMonth() + 1;
		const day = date.getDate();

		return {
			year,
			month,
			day,
			data: Promise.await(aggregates.getUniqueOSOfLastMonth(this.secondaryCollection, { year, month, day })),
		};
	}

	createOrUpdate(data = {}) {
		const { year, month, day, sessionId, instanceId } = data;

		if (!year || !month || !day || !sessionId || !instanceId) {
			return;
		}

		const now = new Date();

		return this.upsert({ instanceId, sessionId, year, month, day }, {
			$set: data,
			$setOnInsert: {
				createdAt: now,
			},
		});
	}

	closeByInstanceIdAndSessionId(instanceId, sessionId) {
		const query = {
			instanceId,
			sessionId,
			closedAt: { $exists: 0 },
		};

		const closeTime = new Date();
		const update = {
			$set: {
				closedAt: closeTime,
				lastActivityAt: closeTime,
			},
		};

		return this.update(query, update);
	}

	updateActiveSessionsByDateAndInstanceIdAndIds({ year, month, day } = {}, instanceId, sessions, data = {}) {
		const query = {
			instanceId,
			year,
			month,
			day,
			sessionId: { $in: sessions },
			closedAt: { $exists: 0 },
		};

		const update = {
			$set: data,
		};

		return this.update(query, update, { multi: true });
	}

	logoutByInstanceIdAndSessionIdAndUserId(instanceId, sessionId, userId) {
		const query = {
			instanceId,
			sessionId,
			userId,
			logoutAt: { $exists: 0 },
		};

		const logoutAt = new Date();
		const update = {
			$set: {
				logoutAt,
			},
		};

		return this.update(query, update, { multi: true });
	}

	createBatch(sessions) {
		if (!sessions || sessions.length === 0) {
			return;
		}

		const ops = [];
		sessions.forEach((doc) => {
			const { year, month, day, sessionId, instanceId } = doc;
			delete doc._id;

			ops.push({
				updateOne: {
					filter: { year, month, day, sessionId, instanceId },
					update: {
						$set: doc,
					},
					upsert: true,
				},
			});
		});

		return this.model.rawCollection().bulkWrite(ops, { ordered: false });
	}
}

export default new Sessions('sessions');


import { Meteor } from 'meteor/meteor';

import { hasPermission } from '../../../authorization';
import { Livechat } from '../lib/Livechat';

Meteor.methods({
	'livechat:getAgentOverviewData'(options) {
		if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
			throw new Meteor.Error('error-not-allowed', 'Not allowed', {
				method: 'livechat:getAgentOverviewData',
			});
		}

		if (!(options.chartOptions && options.chartOptions.name)) {
			console.log('Incorrect analytics options');
			return;
		}

		return Livechat.Analytics.getAgentOverviewData(options);
	},
});


//! moment.js locale configuration
//! locale : Georgian [ka]
//! author : Irakli Janiashvili : https://github.com/irakli-janiashvili

;(function (global, factory) {
   typeof exports === 'object' && typeof module !== 'undefined'
       && typeof require === 'function' ? factory(require('../moment')) :
   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
   factory(global.moment)
}(this, function (moment) { 'use strict';

    var ka = moment.defineLocale('ka', {
        months : {
            standalone: 'იანვარი_თებერვალი_მარტი_აპრილი_მაისი_ივნისი_ივლისი_აგვისტო_სექტემბერი_ოქტომბერი_ნოემბერი_დეკემბერი'.split('_'),
            format: 'იანვარს_თებერვალს_მარტს_აპრილის_მაისს_ივნისს_ივლისს_აგვისტს_სექტემბერს_ოქტომბერს_ნოემბერს_დეკემბერს'.split('_')
        },
        monthsShort : 'იან_თებ_მარ_აპრ_მაი_ივნ_ივლ_აგვ_სექ_ოქტ_ნოე_დეკ'.split('_'),
        weekdays : {
            standalone: 'კვირა_ორშაბათი_სამშაბათი_ოთხშაბათი_ხუთშაბათი_პარასკევი_შაბათი'.split('_'),
            format: 'კვირას_ორშაბათს_სამშაბათს_ოთხშაბათს_ხუთშაბათს_პარასკევს_შაბათს'.split('_'),
            isFormat: /(წინა|შემდეგ)/
        },
        weekdaysShort : 'კვი_ორშ_სამ_ოთხ_ხუთ_პარ_შაბ'.split('_'),
        weekdaysMin : 'კვ_ორ_სა_ოთ_ხუ_პა_შა'.split('_'),
        longDateFormat : {
            LT : 'h:mm A',
            LTS : 'h:mm:ss A',
            L : 'DD/MM/YYYY',
            LL : 'D MMMM YYYY',
            LLL : 'D MMMM YYYY h:mm A',
            LLLL : 'dddd, D MMMM YYYY h:mm A'
        },
        calendar : {
            sameDay : '[დღეს] LT[-ზე]',
            nextDay : '[ხვალ] LT[-ზე]',
            lastDay : '[გუშინ] LT[-ზე]',
            nextWeek : '[შემდეგ] dddd LT[-ზე]',
            lastWeek : '[წინა] dddd LT-ზე',
            sameElse : 'L'
        },
        relativeTime : {
            future : function (s) {
                return (/(წამი|წუთი|საათი|წელი)/).test(s) ?
                    s.replace(/ი$/, 'ში') :
                    s + 'ში';
            },
            past : function (s) {
                if ((/(წამი|წუთი|საათი|დღე|თვე)/).test(s)) {
                    return s.replace(/(ი|ე)$/, 'ის წინ');
                }
                if ((/წელი/).test(s)) {
                    return s.replace(/წელი$/, 'წლის წინ');
                }
            },
            s : 'რამდენიმე წამი',
            m : 'წუთი',
            mm : '%d წუთი',
            h : 'საათი',
            hh : '%d საათი',
            d : 'დღე',
            dd : '%d დღე',
            M : 'თვე',
            MM : '%d თვე',
            y : 'წელი',
            yy : '%d წელი'
        },
        ordinalParse: /0|1-ლი|მე-\d{1,2}|\d{1,2}-ე/,
        ordinal : function (number) {
            if (number === 0) {
                return number;
            }
            if (number === 1) {
                return number + '-ლი';
            }
            if ((number < 20) || (number <= 100 && (number % 20 === 0)) || (number % 100 === 0)) {
                return 'მე-' + number;
            }
            return number + '-ე';
        },
        week : {
            dow : 1,
            doy : 7
        }
    });

    return ka;

}));


import { expect } from 'chai';

import { getCredentials, api, request, credentials } from '../../../data/api-data.js';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';

describe('LIVECHAT - custom fields', function() {
	this.retries(0);

	before((done) => getCredentials(done));

	before((done) => {
		updateSetting('Livechat_enabled', true).then(done);
	});

	describe('livechat/custom-fields', () => {
		it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
			updatePermission('view-l-room', []).then(() => {
				request.get(api('livechat/custom-fields'))
					.set(credentials)
					.expect('Content-Type', 'application/json')
					.expect(400)
					.expect((res) => {
						expect(res.body).to.have.property('success', false);
						expect(res.body.error).to.be.equal('error-not-authorized');
					})
					.end(done);
			});
		});
		it('should return an array of custom fields', (done) => {
			updatePermission('view-l-room', ['admin'])
				.then(() => {
					request.get(api('livechat/custom-fields'))
						.set(credentials)
						.expect('Content-Type', 'application/json')
						.expect(200)
						.expect((res) => {
							expect(res.body).to.have.property('success', true);
							expect(res.body.customFields).to.be.an('array');
							expect(res.body).to.have.property('offset');
							expect(res.body).to.have.property('total');
							expect(res.body).to.have.property('count');
						})
						.end(done);
				});
		});
	});

	describe('livechat/custom-fields/id', () => {
		it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
			updatePermission('view-l-room', []).then(() => {
				request.get(api('livechat/custom-fields/invalid-id'))
					.set(credentials)
					.expect('Content-Type', 'application/json')
					.expect(400)
					.expect((res) => {
						expect(res.body).to.have.property('success', false);
						expect(res.body.error).to.be.equal('error-not-authorized');
					})
					.end(() => updatePermission('view-l-room', ['admin']).then(done));
			});
		});
	});
});


import ButtonGroup from '../../mixins/button-group'

import Applicationable from '../../mixins/applicationable'

require('../../stylus/components/_snackbars.styl')

import {
  VSlideYTransition,
  VSlideYReverseTransition
} from '../transitions'

import Colorable from '../../mixins/colorable'
import Toggleable from '../../mixins/toggleable'

export default {
  name: 'v-snackbar',

  components: {
    VSlideYTransition,
    VSlideYReverseTransition
  },

  mixins: [Colorable, Toggleable],

  data () {
    return {
      activeTimeout: {}
    }
  },

  props: {
    absolute: Boolean,
    bottom: Boolean,
    left: Boolean,
    multiLine: Boolean,
    right: Boolean,
    top: Boolean,
    // TODO: change this to closeDelay to match other API in delayable.js
    timeout: {
      type: Number,
      default: 6000
    },
    vertical: Boolean
  },

  computed: {
    classes () {
      return this.addBackgroundColorClassChecks({
        'snack--active': this.isActive,
        'snack--absolute': this.absolute,
        'snack--bottom': this.bottom || !this.top,
        'snack--left': this.left,
        'snack--multi-line': this.multiLine && !this.vertical,
        'snack--right': this.right,
        'snack--top': this.top,
        'snack--vertical': this.vertical
      })
    },
    computedTransition () {
      return this.top ? 'v-slide-y-transition' : 'v-slide-y-reverse-transition'
    }
  },

  watch: {
    isActive () {
      this.setTimeout()
    }
  },

  methods: {
    setTimeout () {
      clearTimeout(this.activeTimeout)

      if (this.isActive && this.timeout) {
        this.activeTimeout = setTimeout(() => {
          this.isActive = false
        }, this.timeout)
      }
    }
  },

  mounted () {
    this.setTimeout()
  },

  render (h) {
    const children = []

    if (this.isActive) {
      children.push(h('div', {
        staticClass: 'snack__content'
      }, this.$slots.default))
    }

    return h('div', {
      staticClass: 'snack',
      'class': this.classes,
      on: this.$listeners
    }, [h(this.computedTransition, children)])
  }
}


require('../../stylus/components/_tabs.styl')

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

export default {
  name: 'v-tabs',

  directives: {
    Resize
  },

  provide () {
    return {
      registerContent: this.registerContent,
      unregisterContent: this.unregisterContent,
      registerTabItem: this.registerTabItem,
      unregisterTabItem: this.unregisterTabItem,
      next: this.next,
      prev: this.prev,
      slider: this.slider,
      tabClick: this.tabClick,
      isScrollable: () => this.scrollable,
      isMobile: () => this.isMobile
    }
  },

  data () {
    return {
      activeIndex: null,
      content: [],
      isBooted: false,
      resizeTimeout: null,
      reverse: false,
      tabItems: [],
      tabsContainer: null,
      tabsSlider: null,
      target: null,
      targetEl: null,
      transitionTime: 300
    }
  },

  props: {
    centered: Boolean,
    fixed: Boolean,
    grow: Boolean,
    icons: Boolean,
    mobileBreakPoint: {
      type: [Number, String],
      default: 1280
    },
    value: String,
    scrollable: {
      type: Boolean,
      default: true
    }
  },

  computed: {
    classes () {
      return {
        'tabs': true,
        'tabs--centered': this.centered,
        'tabs--fixed': this.fixed,
        'tabs--grow': this.grow,
        'tabs--icons': this.icons,
        'tabs--mobile': this.$vuetify.breakpoint.width < this.mobileBreakPoint,
        'tabs--scroll-bars': this.scrollable
      }
    }
  },

  watch: {
    value () {
      this.tabClick(this.value)
    },
    activeIndex () {
      this.updateTabs()
      this.$nextTick(() => (this.isBooted = true))
    },
    tabItems (newItems, oldItems) {
      // Tab item got removed
      if (oldItems.length > newItems.length) {
        if (!newItems.find(o => o.id === this.target)) {
          const i = oldItems.findIndex(o => o.id === this.target)

          this.$nextTick(() => {
            this.activeIndex = this.tabItems[i > 0 ? i - 1 : 0].id
            this.target = this.activeIndex
          })
        }
      }
      this.slider()
    },
    '$vuetify.application.left' () {
      this.onContainerResize()
    },
    '$vuetify.application.right' () {
      this.onContainerResize()
    }
  },

  mounted () {
    // This is a workaround to detect if link is active
    // when being used as a router or nuxt link
    const i = this.tabItems.findIndex(({ el }) => {
      return el.firstChild.classList.contains('tabs__item--active')
    })

    const tab = this.value || (this.tabItems[i !== -1 ? i : 0] || {}).id

    tab && this.tabClick(tab)
  },

  methods: {
    registerContent (id, toggle) {
      this.content.push({ id, toggle })
    },
    registerTabItem (id, toggle, el) {
      this.tabItems.push({ id, toggle, el })
    },
    unregisterContent (id) {
      this.content = this.content.filter(o => o.id !== id)
    },
    unregisterTabItem (id) {
      this.tabItems = this.tabItems.filter(o => o.id !== id)
    },
    next (cycle) {
      let nextIndex = this.activeIndex + 1

      if (!this.content[nextIndex]) {
        if (!cycle) return
        nextIndex = 0
      }

      this.tabClick(this.tabItems[nextIndex].id)
    },
    prev (cycle) {
      let prevIndex = this.activeIndex - 1

      if (!this.content[prevIndex]) {
        if (!cycle) return
        prevIndex = this.content.length - 1
      }

      this.tabClick(this.tabItems[prevIndex].id)
    },
    onResize () {
      this.slider()
    },
    /**
     * When v-navigation-drawer changes the
     * width of the container, call resize
     * after the transition is complete
     *
     * @return {Void}
     */
    onContainerResize () {
      clearTimeout(this.resizeTimeout)
      this.resizeTimeout = setTimeout(this.onResize, this.transitionTime)
    },
    slider (el) {
      this.tabsSlider = this.tabsSlider ||
        !!this.$el && this.$el.querySelector('.tabs__slider')

      this.tabsContainer = this.tabsContainer ||
        !!this.$el && this.$el.querySelector('.tabs__container')

      if (!this.tabsSlider || !this.tabsContainer) return

      this.targetEl = el || this.targetEl

      if (!this.targetEl) return

      // Gives DOM time to paint when
      // processing slider for
      // dynamic tabs
      this.$nextTick(() => {
        // #684 Calculate width as %
        const width = (
          this.targetEl.scrollWidth /
          this.tabsContainer.clientWidth *
          100
        )

        this.tabsSlider.style.width = `${width}%`
        this.tabsSlider.style.left = `${this.targetEl.offsetLeft}px`
      })
    },
    tabClick (target) {
      const setActiveIndex = index => {
        if (this.activeIndex === index) {
          // #762 update tabs display
          // In case tabs count got changed but activeIndex didn't
          this.updateTabs()
        } else {
          this.activeIndex = index
        }
      }

      this.target = target

      this.$nextTick(() => {
        const nextIndex = this.content.findIndex(o => o.id === target)
        this.reverse = nextIndex < this.activeIndex
        setActiveIndex(nextIndex)

        this.$emit('input', this.target)
      })
    },
    updateTabs () {
      this.content.forEach(({ toggle }) => {
        toggle(this.target, this.reverse, this.isBooted)
      })

      this.tabItems.forEach(({ toggle }) => {
        toggle(this.target)
      })
    }
  },

  render (h) {
    return h('div', {
      'class': this.classes,
      directives: [{
        name: 'resize',
        value: this.onResize
      }]
    }, this.$slots.default)
  }
}


import Ripple from '../directives/ripple'

export default {
  directives: {
    Ripple
  },

  props: {
    activeClass: String,
    append: Boolean,
    disabled: Boolean,
    exact: Boolean,
    exactActiveClass: String,
    href: [String, Object],
    to: [String, Object],
    nuxt: Boolean,
    replace: Boolean,
    ripple: Boolean,
    tag: String,
    target: String
  },

  methods: {
    click () {},
    generateRouteLink () {
      let exact = this.exact
      let tag

      const data = {
        attrs: { disabled: this.disabled },
        class: this.classes,
        props: {},
        directives: [{
          name: 'ripple',
          value: this.ripple || false
        }],
        on: {
          ...(this.$listeners || {}),
          click: this.click
        }
      }

      if (typeof this.exact === 'undefined') {
        exact = this.to === '/' ||
          (this.to === Object(this.to) && this.to.path === '/')
      }

      if (this.to) {
        // Add a special activeClass hook
        // for component level styles
        let activeClass = this.activeClass
        let exactActiveClass = this.exactActiveClass || activeClass

        if (this.proxyClass) {
          activeClass += ' ' + this.proxyClass
          exactActiveClass += ' ' + this.proxyClass
        }

        tag = this.nuxt ? 'nuxt-link' : 'router-link'
        data.props.to = this.to
        data.props.exact = exact
        data.props.activeClass = activeClass
        data.props.exactActiveClass = exactActiveClass
        data.props.append = this.append
        data.props.replace = this.replace
      } else {
        tag = this.href && 'a' || this.tag || 'a'

        if (tag === 'a') {
          if (this.href) data.attrs.href = this.href
          if (this.target) data.attrs.target = this.target
        }
      }

      return { tag, data }
    }
  }
}


export default {
  data () {
    return {
      errorBucket: [],
      hasFocused: false,
      hasInput: false,
      shouldValidate: false,
      valid: false
    }
  },

  props: {
    error: {
      type: Boolean
    },
    errorMessages: {
      type: [String, Array],
      default: () => []
    },
    rules: {
      type: Array,
      default: () => []
    },
    validateOnBlur: Boolean
  },

  computed: {
    validations () {
      if (!Array.isArray(this.errorMessages)) {
        return [this.errorMessages]
      } else if (this.errorMessages.length > 0) {
        return this.errorMessages
      } else if (this.shouldValidate) {
        return this.errorBucket
      } else {
        return []
      }
    },
    hasError () {
      return this.validations.length > 0 ||
        this.errorMessages.length > 0 ||
        this.error
    }
  },

  watch: {
    rules: {
      handler (newVal, oldVal) {
        // TODO: This handler seems to trigger when input changes, even though
        // rules array stays the same? Solved it like this for now
        if (newVal.length === oldVal.length) return

        this.validate()
      },
      deep: true
    },
    inputValue (val) {
      // If it's the first time we're setting input,
      // mark it with hasInput
      if (!!val && !this.hasInput) this.hasInput = true

      if (this.hasInput && !this.validateOnBlur) this.shouldValidate = true
    },
    isFocused (val) {
      // If we're not focused, and it's the first time
      // we're defocusing, set shouldValidate to true
      if (!val && !this.hasFocused) {
        this.hasFocused = true
        this.shouldValidate = true

        this.$emit('update:error', this.errorBucket.length > 0)
      }
    },
    hasError (val) {
      if (this.shouldValidate) {
        this.$emit('update:error', val)
      }
    },
    error (val) {
      this.shouldValidate = !!val
    }
  },

  mounted () {
    this.shouldValidate = !!this.error
    this.validate()
  },

  methods: {
    reset () {
      // TODO: Do this another way!
      // This is so that we can reset all types of inputs
      this.$emit('input', this.isMultiple ? [] : null)
      this.$emit('change', null)

      this.$nextTick(() => {
        this.shouldValidate = false
        this.hasFocused = false
        this.validate()
      })
    },
    validate (force = false, value = this.inputValue) {
      if (force) this.shouldValidate = true

      this.errorBucket = []

      this.rules.forEach(rule => {
        const valid = typeof rule === 'function' ? rule(value) : rule

        if (valid !== true && !['string', 'boolean'].includes(typeof valid)) {
          throw new TypeError(`Rules should return a string or boolean, received '${typeof valid}' instead`)
        }

        if (valid !== true) {
          this.errorBucket.push(valid)
        }
      })

      this.valid = this.errorBucket.length === 0

      return this.valid
    }
  }
}


keep keep keep keep keep replace keep keep keep

mixins: [
Applicationable,
ButtonGroup,
Colorable
],

export default {
  props: {
    app: Boolean
  },

  watch: {
    app () {
      this.updateApplication()
    }
  },

  created () {
    this.updateApplication()
  }
}


@import '../bootstrap'
@import '../theme'

small-dialog($material)
  &__content
    background: $material.cards

  &__actions
    background: $material.cards

  a
    color: rgba($material.fg-color, $material.primary-text-percent)

theme(small-dialog, "small-dialog")

.small-dialog
  display: block
  height: 100%

  &__content
    padding: 0 24px

  &__actions
    text-align: right

  a
    display: flex
    align-items: center

    height: 100%
    text-decoration: none

    > *
      width: 100%

  .menu__activator
    height: 100%


.blockquote
  padding: $spacers.three.y 0 $spacers.three.y $spacers.four.x
  font-size: 18px
  font-weight: 300


import VForm from './VForm'

/* istanbul ignore next */
VForm.install = function install (Vue) {
  Vue.component(VForm.name, VForm)
}

export default VForm


require('../../stylus/components/_dividers.styl')

import Themeable from '../../mixins/themeable'

export default {
  name: 'v-divider',

  functional: true,

  mixins: [Themeable],

  props: {
    inset: Boolean
  },

  render (h, { props, data, children }) {
    data.staticClass = (`divider ${data.staticClass || ''}`).trim()

    if (props.inset) data.staticClass += ' divider--inset'
    if (props.light) data.staticClass += ' theme--light'
    if (props.dark) data.staticClass += ' theme--dark'

    return h('hr', data)
  }
}


import VSlider from './VSlider'

/* istanbul ignore next */
VSlider.install = function install (Vue) {
  Vue.component(VSlider.name, VSlider)
}

export default VSlider


function style (el, value) {
  [
    'transform',
    'webkitTransform'
  ].forEach(i => {
    el.style[i] = value
  })
}

const RippleDataAttribute = 'data-ripple'

const ripple = {
  /**
   * @param {Event} e
   * @param {Element} el
   * @param {{ class?: string, center?: boolean }} [value={}]
   */
  show: (e, el, { value = {} }) => {
    if (el.getAttribute(RippleDataAttribute) !== 'true') {
      return
    }

    const container = document.createElement('span')
    const animation = document.createElement('span')

    container.appendChild(animation)
    container.className = 'ripple__container'

    if (value.class) {
      container.className += ` ${value.class}`
    }

    const size = el.clientWidth > el.clientHeight
      ? el.clientWidth
      : el.clientHeight
    animation.className = 'ripple__animation'
    animation.style.width = `${size * (value.center ? 1 : 2)}px`
    animation.style.height = animation.style.width

    el.appendChild(container)
    const computed = window.getComputedStyle(el)
    if (computed.position !== 'absolute' && computed.position !== 'fixed') el.style.position = 'relative'

    const offset = el.getBoundingClientRect()
    const x = value.center ? '50%' : `${e.clientX - offset.left}px`
    const y = value.center ? '50%' : `${e.clientY - offset.top}px`

    animation.classList.add('ripple__animation--enter')
    animation.classList.add('ripple__animation--visible')
    style(animation, `translate(-50%, -50%) translate(${x}, ${y}) scale3d(0.01,0.01,0.01)`)
    animation.dataset.activated = Date.now()

    setTimeout(() => {
      animation.classList.remove('ripple__animation--enter')
      style(animation, `translate(-50%, -50%) translate(${x}, ${y})  scale3d(0.99,0.99,0.99)`)
    }, 0)
  },

  hide: (el) => {
    if (el.getAttribute(RippleDataAttribute) !== 'true') {
      return
    }

    const ripples = el.getElementsByClassName('ripple__animation')

    if (ripples.length === 0) return
    const animation = ripples[ripples.length - 1]
    const diff = Date.now() - Number(animation.dataset.activated)
    let delay = 400 - diff

    delay = delay < 0 ? 0 : delay

    setTimeout(() => {
      animation.classList.remove('ripple__animation--visible')

      setTimeout(() => {
        // Need to figure out a new way to do this
        try {
          if (ripples.length < 1) el.style.position = null
          animation.parentNode && el.removeChild(animation.parentNode)
        } catch (e) {}
      }, 300)
    }, delay)
  }
}

function isRippleEnabled (binding) {
  return typeof binding.value === 'undefined' || !!binding.value
}

function directive (el, binding) {
  el.setAttribute(RippleDataAttribute, isRippleEnabled(binding))

  if ('ontouchstart' in window) {
    el.addEventListener('touchend', () => ripple.hide(el), false)
    el.addEventListener('touchcancel', () => ripple.hide(el), false)
  }

  el.addEventListener('mousedown', e => ripple.show(e, el, binding), false)
  el.addEventListener('mouseup', () => ripple.hide(el), false)
  el.addEventListener('mouseleave', () => ripple.hide(el), false)
  // Anchor tags can be dragged, causes other hides to fail - #1537
  el.addEventListener('dragstart', () => ripple.hide(el), false)
}

function unbind (el, binding) {
  el.removeEventListener('touchstart', e => ripple.show(e, el, binding), false)
  el.removeEventListener('mousedown', e => ripple.show(e, el, binding), false)
  el.removeEventListener('touchend', () => ripple.hide(el), false)
  el.removeEventListener('touchcancel', () => ripple.hide(el), false)
  el.removeEventListener('mouseup', () => ripple.hide(el), false)
  el.removeEventListener('mouseleave', () => ripple.hide(el), false)
  el.removeEventListener('dragstart', () => ripple.hide(el), false)
}

function update (el, binding) {
  if (binding.value === binding.oldValue) {
    return
  }

  el.setAttribute(RippleDataAttribute, isRippleEnabled(binding))
}

export default {
  name: 'ripple',
  bind: directive,
  unbind: unbind,
  update: update
}


keep keep keep keep add keep keep keep keep


import Bootable from '../../mixins/bootable'

import {
  VTabTransition,
  VTabReverseTransition
} from '../transitions'

import Touch from '../../directives/touch'

export default {
  name: 'v-tabs-content',

  mixins: [Bootable],

  inject: ['registerContent', 'unregisterContent'],

  components: {
    VTabTransition,
    VTabReverseTransition
  },

  directives: {
    Touch
  },

  data () {
    return {
      isActive: false,
      reverse: false
    }
  },

  props: {
    id: {
      type: String,
      required: true
    },
    transition: {
      type: [Boolean, String],
      default: 'tab-transition'
    },
    reverseTransition: {
      type: [Boolean, String],
      default: 'tab-reverse-transition'
    }
  },

  computed: {
    computedTransition () {
      return this.reverse ? this.reverseTransition : this.transition
    }
  },

  methods: {
    toggle (target, reverse, showTransition) {
      this.$el.style.transition = !showTransition ? 'none' : null
      this.reverse = reverse
      this.isActive = this.id === target
    }
  },

  mounted () {
    this.registerContent(this.id, this.toggle)
  },

  beforeDestroy () {
    this.unregisterContent(this.id)
  },

  render (h) {
    const data = {
      staticClass: 'tabs__content',
      directives: [{
        name: 'show',
        value: this.isActive
      }],
      on: this.$listeners
    }

    if (this.id) data.domProps = { id: this.id }

    const div = h('div', data, this.showLazyContent(this.$slots.default))

    if (!this.computedTransition) return div

    return h('transition', {
      props: { name: this.computedTransition }
    }, [div])
  }
}


import VSpeedDial from './VSpeedDial'

/* istanbul ignore next */
VSpeedDial.install = function install (Vue) {
  Vue.component(VSpeedDial.name, VSpeedDial)
}

export default VSpeedDial


@require './settings/*'
@require './generic/*'
@require './elements/*'
@require './tools/*'
@require './trumps/*'
@require './components/_tooltips.styl'


app () {
this.updateApplication()
}
},
created () {
this.updateApplication()
}
}

require('../../stylus/components/_bottom-navs.styl')

import ButtonGroup from '../../mixins/button-group'
import Colorable from '../../mixins/colorable'

export default {
  name: 'v-bottom-nav',

  mixins: [ButtonGroup, Colorable],

  data: () => ({
    defaultColor: 'primary'
  }),

  props: {
    absolute: Boolean,
    active: [Number, String],
    shift: Boolean,
    value: { required: false }
  },

  watch: {
    active () {
      this.update()
    }
  },

  computed: {
    classes () {
      return {
        'bottom-nav': true,
        'bottom-nav--absolute': this.absolute,
        'bottom-nav--shift': this.shift,
        'bottom-nav--active': this.value
      }
    }
  },

  methods: {
    isSelected (i) {
      const item = this.getValue(i)
      return this.active === item
    },
    updateValue (i) {
      const item = this.getValue(i)
      this.$emit('update:active', item)
    }
  },

  render (h) {
    return h('div', {
      class: this.addBackgroundColorClassChecks(this.classes)
    }, this.$slots.default)
  }
}


export default {
  name: 'v-card-media',

  props: {
    contain: Boolean,
    height: {
      type: [Number, String],
      default: 'auto'
    },
    src: {
      type: String
    }
  },

  render (h) {
    const data = {
      'class': 'card__media',
      style: {
        height: !isNaN(this.height) ? `${this.height}px` : this.height
      },
      on: this.$listeners
    }

    const children = []

    if (this.src) {
      children.push(h('div', {
        'class': 'card__media__background',
        style: {
          background: `url(${this.src}) center center / ${this.contain ? 'contain' : 'cover'} no-repeat`
        }
      }))
    }

    children.push(h('div', {
      'class': 'card__media__content'
    }, this.$slots.default))

    return h('div', data, children)
  }
}


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

exports[`VRadioGroup.vue should render role on radio group 1`] = `

`;


const THEME_DEFAULTS = {
  primary: '#1976D2',
  secondary: '#424242',
  accent: '#82B1FF',
  error: '#FF5252',
  info: '#2196F3',
  success: '#4CAF50',
  warning: '#FFC107'
}

export default function theme (theme) {
  theme = theme || {}

  return Object.assign({}, THEME_DEFAULTS, theme)
}


keep keep keep replace keep keep keep keep

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

exports[`VBtn.js should render a <button> tag when using to prop 1`] = `

<button class="btn btn--router">
</button>

`;

exports[`VBtn.js should render an <a> tag when using href prop 1`] = `

`;

exports[`VBtn.js should render component and match snapshot 1`] = `

<button type="button"
        class="btn"
>

</button>

`;

exports[`VBtn.js should render component with color prop and match snapshot 1`] = `

<button type="button"
        class="btn green darken-1"
>

</button>

`;

exports[`VBtn.js should render component with color prop and match snapshot 2`] = `

<button type="button"
        class="btn btn--flat green--text text--darken-1"
>

</button>

`;

exports[`VBtn.js should render component with loader and match snapshot 1`] = `

<button type="button"
        class="btn btn--loader"
>

      <svg xmlns="http://www.w3.org/2000/svg"
           viewbox="25 25 50 50"
      >
        <circle fill="transparent"
                cx="50"
                cy="50"
                r="20"
                stroke-width="4"
                stroke-dasharray="125.664"
                stroke-dashoffset="125.66370614359172px"
                class="progress-circular__overlay"
        >
        </circle>
      </svg>

</button>

`;

exports[`VBtn.js should render component with loader slot and match snapshot 1`] = `

<button type="button"
        class="btn btn--loader"
>

      loader

</button>

`;

exports[`VBtn.js should render specified tag when using tag prop 1`] = `

`;


import VDivider from '@components/VDivider'
import { test } from '@util/testing'

test('VDivider.js', ({ mount, compileToFunctions, functionalContext }) => {
  it('should render component and match snapshot', () => {
    const wrapper = mount(VDivider, functionalContext())

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

  it('should render an inset component', () => {
    const wrapper = mount(VDivider, functionalContext({
      props: {
        inset: true
      }
    }))

    expect(wrapper.hasClass('divider--inset')).toBe(true)
  })

  it('should render a light component', () => {
    const wrapper = mount(VDivider, functionalContext({
      props: {
        light: true
      }
    }))

    expect(wrapper.hasClass('theme--light')).toBe(true)
  })

  it('should render a dark component', () => {
    const wrapper = mount(VDivider, functionalContext({
      props: {
        dark: true
      }
    }))

    expect(wrapper.hasClass('theme--dark')).toBe(true)
  })
})


export default {
  props: {
    appendIcon: {
      type: String,
      default: 'arrow_drop_down'
    },
    appendIconCb: Function,
    attach: Boolean,
    auto: Boolean,
    autocomplete: Boolean,
    browserAutocomplete: {
      type: String,
      default: 'on'
    },
    cacheItems: Boolean,
    chips: Boolean,
    clearable: Boolean,
    combobox: Boolean,
    contentClass: String,
    deletableChips: Boolean,
    dense: Boolean,
    editable: Boolean,
    hideSelected: Boolean,
    items: {
      type: Array,
      default: () => []
    },
    itemAvatar: {
      type: String,
      default: 'avatar'
    },
    itemDisabled: {
      type: String,
      default: 'disabled'
    },
    itemText: {
      type: String,
      default: 'text'
    },
    itemValue: {
      type: String,
      default: 'value'
    },
    maxHeight: {
      type: [Number, String],
      default: 300
    },
    minWidth: {
      type: [Boolean, Number, String],
      default: false
    },
    multiple: Boolean,
    multiLine: Boolean,
    openOnClear: Boolean,
    overflow: Boolean,
    returnObject: Boolean,
    searchInput: {
      default: null
    },
    segmented: Boolean,
    singleLine: Boolean,
    tags: Boolean,
    valueComparator: {
      type: Function,
      default: (a, b) => {
        if (a !== Object(a)) return a === b
        const aProps = Object.keys(a)
        const bProps = Object.keys(b)
        return aProps.length === bProps.length && aProps.every(propName => (a[propName] === b[propName]))
      }
    }
  }
}


import {
  createSimpleFunctional
} from '../../util/helpers'
import VContent from './VContent'
import VContainer from './VContainer'
import VFlex from './VFlex'
import VLayout from './VLayout'

const VSpacer = createSimpleFunctional('spacer')

export {
  VContainer,
  VContent,
  VFlex,
  VLayout,
  VSpacer
}

const VGrid = {}

/* istanbul ignore next */
VGrid.install = function install (Vue) {
  Vue.component(VContent.name, VContent)
  Vue.component(VContainer.name, VContainer)
  Vue.component(VFlex.name, VFlex)
  Vue.component(VLayout.name, VLayout)
  Vue.component(VSpacer.name, VSpacer)
}

export default VGrid