Showing preview only (672K chars total). Download the full file or copy to clipboard to get everything.
Repository: Qvant-lab/qui
Branch: master
Commit: 3ead95280f7e
Files: 322
Total size: 590.1 KB
Directory structure:
gitextract_kt2b5v6x/
├── .eslintrc
├── .github/
│ └── workflows/
│ ├── codeql-analysis.yml
│ └── deploy.yml
├── .gitignore
├── .lintstagedrc
├── .npmignore
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── .storybook/
│ ├── locales/
│ │ ├── en.js
│ │ ├── index.js
│ │ └── ru.js
│ ├── main.js
│ ├── manager.js
│ ├── preview.js
│ └── theme.js
├── .stylelintignore
├── .stylelintrc
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── babel.config.js
├── jest.config.js
├── package.json
├── scripts/
│ └── badges.js
├── src/
│ ├── components.scss
│ ├── fonts/
│ │ └── index.scss
│ ├── icons/
│ │ └── index.scss
│ ├── main.scss
│ ├── normalize.scss
│ ├── onDemand.js
│ ├── qComponents/
│ │ ├── QBreadcrumbs/
│ │ │ ├── QBreadcrumbs.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QBreadcrumbs.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QBreadcrumbs.vue
│ │ │ └── q-breadcrumbs.scss
│ │ ├── QButton/
│ │ │ ├── QButton.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QButton.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QButton.vue
│ │ │ └── q-button.scss
│ │ ├── QCascader/
│ │ │ ├── QCascader.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QCascader.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCascader.vue
│ │ │ ├── QCascaderMenu.vue
│ │ │ ├── QCascaderPanel.vue
│ │ │ ├── q-cascader-menu.scss
│ │ │ └── q-cascader.scss
│ │ ├── QCheckbox/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCheckbox.vue
│ │ │ └── q-checkbox.scss
│ │ ├── QCheckboxGroup/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCheckboxGroup.vue
│ │ │ └── q-checkbox-group.scss
│ │ ├── QCol/
│ │ │ ├── QCol.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QCol.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCol.vue
│ │ │ └── q-col.scss
│ │ ├── QCollapse/
│ │ │ ├── QCollapse.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QCollapse.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCollapse.vue
│ │ │ └── q-collapse.scss
│ │ ├── QCollapseItem/
│ │ │ ├── QCollapseItem.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QCollapseItem.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCollapseItem.vue
│ │ │ └── q-collapse-item.scss
│ │ ├── QColorPicker/
│ │ │ ├── QColorAlphaSlider.test.js
│ │ │ ├── QColorHueSlider.test.js
│ │ │ ├── QColorPicker.test.js
│ │ │ ├── QColorSvpanel.test.js
│ │ │ ├── QPickerDropdown.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ ├── QColorAlphaSlider.test.js.snap
│ │ │ │ ├── QColorHueSlider.test.js.snap
│ │ │ │ ├── QColorPicker.test.js.snap
│ │ │ │ ├── QColorSvpanel.test.js.snap
│ │ │ │ └── QPickerDropdown.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QColorAlphaSlider.vue
│ │ │ ├── QColorHueSlider.vue
│ │ │ ├── QColorPicker.vue
│ │ │ ├── QColorSvpanel.vue
│ │ │ ├── QPickerDropdown.vue
│ │ │ ├── draggable.js
│ │ │ ├── q-color-alpha-slider.scss
│ │ │ ├── q-color-hue-slider.scss
│ │ │ ├── q-color-picker.scss
│ │ │ ├── q-color-svpanel.scss
│ │ │ └── q-picker-dropdown.scss
│ │ ├── QContextMenu/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QContextMenu.vue
│ │ │ └── q-context-menu.scss
│ │ ├── QDatePicker/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QDatePicker.vue
│ │ │ ├── basic/
│ │ │ │ ├── date-table.vue
│ │ │ │ ├── month-table.vue
│ │ │ │ └── year-table.vue
│ │ │ ├── helpers.js
│ │ │ ├── panel/
│ │ │ │ ├── date-range.vue
│ │ │ │ ├── date.vue
│ │ │ │ ├── focus-mixin.js
│ │ │ │ ├── focus-time-mixin.js
│ │ │ │ ├── month-range.vue
│ │ │ │ ├── range-mixin.js
│ │ │ │ └── year-range.vue
│ │ │ ├── q-date-picker.scss
│ │ │ └── styles/
│ │ │ ├── date-table.scss
│ │ │ ├── month-table.scss
│ │ │ ├── picker-panel.scss
│ │ │ ├── picker.scss
│ │ │ └── year-table.scss
│ │ ├── QDialog/
│ │ │ ├── QDialog.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QDialog.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QDialog.js
│ │ │ ├── QDialog.vue
│ │ │ └── q-dialog.scss
│ │ ├── QDrawer/
│ │ │ ├── QDrawer.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QDrawer.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QDrawer.vue
│ │ │ └── q-drawer.scss
│ │ ├── QForm/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QForm.vue
│ │ │ └── q-form.scss
│ │ ├── QFormItem/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QFormItem.vue
│ │ │ └── q-form-item.scss
│ │ ├── QInput/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QInput.vue
│ │ │ └── q-input.scss
│ │ ├── QInputNumber/
│ │ │ ├── QInputNumber.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QInputNumber.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QInputNumber.vue
│ │ │ └── q-input-number.scss
│ │ ├── QMessageBox/
│ │ │ ├── QMessageBox.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QMessageBox.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QMessageBox.js
│ │ │ ├── QMessageBox.vue
│ │ │ └── q-message-box.scss
│ │ ├── QNotification/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QNotification.js
│ │ │ ├── QNotification.vue
│ │ │ └── q-notification.scss
│ │ ├── QOption/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QOption.vue
│ │ │ └── q-option.scss
│ │ ├── QPagination/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QPagination.vue
│ │ │ └── q-pagination.scss
│ │ ├── QPopover/
│ │ │ ├── QPopover.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QPopover.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QPopover.vue
│ │ │ └── q-popover.scss
│ │ ├── QRadio/
│ │ │ ├── QRadio.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QRadio.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QRadio.vue
│ │ │ └── q-radio.scss
│ │ ├── QRadioGroup/
│ │ │ ├── QRadioGroup.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QRadioGroup.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QRadioGroup.vue
│ │ │ └── q-radio-group.scss
│ │ ├── QRow/
│ │ │ ├── QRow.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QRow.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QRow.vue
│ │ │ └── q-row.scss
│ │ ├── QScrollbar/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QBar.vue
│ │ │ ├── QScrollbar.vue
│ │ │ ├── q-scrollbar.scss
│ │ │ └── util.js
│ │ ├── QSelect/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QSelect.vue
│ │ │ ├── QSelectDropdown.vue
│ │ │ ├── QSelectTags.vue
│ │ │ ├── q-select-dropdown.scss
│ │ │ ├── q-select-tags.scss
│ │ │ └── q-select.scss
│ │ ├── QSlider/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QSlider.vue
│ │ │ ├── components/
│ │ │ │ ├── QSliderBar/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── QSliderButton/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── QSliderCaptions/
│ │ │ │ │ └── index.vue
│ │ │ │ └── QSliderSteps/
│ │ │ │ └── index.vue
│ │ │ ├── q-slider.scss
│ │ │ └── styles/
│ │ │ ├── q-slider-bar.scss
│ │ │ ├── q-slider-button.scss
│ │ │ ├── q-slider-captions.scss
│ │ │ ├── q-slider-steps.scss
│ │ │ └── q-slider.scss
│ │ ├── QTabPane/
│ │ │ ├── QTabPane.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QTabPane.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTabPane.vue
│ │ │ └── q-tab-pane.scss
│ │ ├── QTable/
│ │ │ ├── QTable.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QTable.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTable.vue
│ │ │ ├── components/
│ │ │ │ ├── DragElements/
│ │ │ │ │ └── index.vue
│ │ │ │ └── QTableRow/
│ │ │ │ └── index.vue
│ │ │ ├── hocs/
│ │ │ │ └── withQTableRow/
│ │ │ │ └── index.js
│ │ │ └── q-table.scss
│ │ ├── QTabs/
│ │ │ ├── QTabs.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QTabs.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTabs.vue
│ │ │ └── q-tabs.scss
│ │ ├── QTag/
│ │ │ ├── QTag.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QTag.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTag.vue
│ │ │ └── q-tag.scss
│ │ ├── QTextarea/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTextarea.vue
│ │ │ ├── calcTextareaHeight.js
│ │ │ └── q-textarea.scss
│ │ ├── QTimePicker/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTimePicker.vue
│ │ │ ├── components/
│ │ │ │ ├── panel.vue
│ │ │ │ └── time-panel.scss
│ │ │ └── q-time-picker.scss
│ │ ├── QUpload/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QUpload.vue
│ │ │ ├── QUploadDropZone.vue
│ │ │ ├── QUploadFileMultiple.vue
│ │ │ ├── QUploadFileSingle.vue
│ │ │ ├── q-upload-drop-zone.scss
│ │ │ ├── q-upload-file-multiple.scss
│ │ │ ├── q-upload-file-single.scss
│ │ │ └── q-upload.scss
│ │ ├── constants/
│ │ │ ├── locales/
│ │ │ │ ├── en.js
│ │ │ │ ├── index.js
│ │ │ │ └── ru.js
│ │ │ └── popperPlacements.js
│ │ ├── helpers/
│ │ │ ├── collapse-transition.js
│ │ │ ├── dateHelpers.js
│ │ │ └── index.js
│ │ ├── index.js
│ │ └── mixins/
│ │ ├── emitter.js
│ │ ├── inputs.js
│ │ └── pickers.js
│ ├── transition.scss
│ └── vars.scss
├── stories/
│ ├── components/
│ │ ├── Layout/
│ │ │ ├── Layout.stories.js
│ │ │ ├── QCol.stories.js
│ │ │ ├── QRow.stories.js
│ │ │ └── layout.scss
│ │ ├── QBreadcrumbs.stories.js
│ │ ├── QButton.stories.mdx
│ │ ├── QCascader.stories.js
│ │ ├── QCheckbox.stories.js
│ │ ├── QCheckboxGroup.stories.js
│ │ ├── QCollapse.stories.js
│ │ ├── QColorPicker.stories.js
│ │ ├── QContextMenu.stories.js
│ │ ├── QDatePicker/
│ │ │ ├── DateRange.js
│ │ │ ├── DateTime.js
│ │ │ ├── DateTimeRange.js
│ │ │ ├── Default.js
│ │ │ ├── Month.js
│ │ │ ├── MonthRange.js
│ │ │ ├── QDatePicker.stories.js
│ │ │ ├── Year.js
│ │ │ └── YearRange.js
│ │ ├── QDialog/
│ │ │ ├── DialogFormTest.vue
│ │ │ └── QDialog.stories.js
│ │ ├── QDrawer.stories.js
│ │ ├── QForm.stories.js
│ │ ├── QInput.stories.js
│ │ ├── QInputNumber.stories.js
│ │ ├── QMessageBox/
│ │ │ ├── MessageBoxFormTest.vue
│ │ │ └── QMessageBox.stories.js
│ │ ├── QNotification.stories.js
│ │ ├── QPagination.stories.js
│ │ ├── QPopover.stories.js
│ │ ├── QRadio.stories.js
│ │ ├── QRadioGroup.stories.js
│ │ ├── QScrollbar/
│ │ │ ├── QScrollbar.stories.js
│ │ │ └── q-scrollbar.scss
│ │ ├── QSelect/
│ │ │ ├── Default.js
│ │ │ ├── Multiple.js
│ │ │ └── QSelect.stories.js
│ │ ├── QSlider/
│ │ │ ├── Breakpoints.js
│ │ │ ├── Captions.js
│ │ │ ├── Default.js
│ │ │ ├── Disabled.js
│ │ │ ├── QSlider.stories.js
│ │ │ ├── Range.js
│ │ │ ├── Vertical.js
│ │ │ └── WithoutTooltip.js
│ │ ├── QTabPane.stories.js
│ │ ├── QTable/
│ │ │ ├── CustomRows.js
│ │ │ ├── CustomWidth.js
│ │ │ ├── Default.js
│ │ │ ├── Draggable.js
│ │ │ ├── Groups.js
│ │ │ ├── QTable.stories.js
│ │ │ ├── Selectable.js
│ │ │ ├── StickyColumn.js
│ │ │ └── Total.js
│ │ ├── QTabs.stories.js
│ │ ├── QTag.stories.js
│ │ ├── QTextarea.stories.js
│ │ ├── QTimePicker.stories.js
│ │ └── QUpload/
│ │ ├── Default.js
│ │ ├── Multiple.js
│ │ └── QUpload.stories.js
│ ├── core/
│ │ ├── colors.js
│ │ ├── colors.stories.mdx
│ │ └── icons.stories.mdx
│ └── intro.stories.mdx
└── tests/
└── unit/
└── setup.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc
================================================
{
"root": false,
"env": {
"browser": true
},
"parserOptions": {
"parser": "babel-eslint",
"sourceType": "module"
},
"extends": [
"eslint:recommended",
"airbnb-base",
"plugin:vue/recommended",
"prettier",
"prettier/vue"
],
"rules": {
"func-names": 0,
"no-unused-expressions": [
"error",
{
"allowShortCircuit": true,
"allowTernary": false,
"allowTaggedTemplates": false
}
],
"prefer-destructuring": [
"error",
{
"array": false
}
],
"import/extensions": "off",
"import/no-unresolved": "off",
"vue/component-name-in-template-casing": ["error", "kebab-case"],
"vue/max-attributes-per-line": "warn",
"vue/no-v-html": 0,
"vue/html-closing-bracket-newline": "warn",
"vue/html-indent": "warn",
"no-console": 0,
"no-debugger": 0
},
"overrides": [
{
"files": ["**/*.test.js"],
"env": { "jest": true },
"globals": {
"mount": "readonly",
"shallowMount": "readonly"
}
}
]
}
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: 'CodeQL'
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '21 8 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: ['javascript']
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
================================================
FILE: .github/workflows/deploy.yml
================================================
name: github pages
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: '12.x'
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: yarn --frozen-lockfile
- run: yarn build-storybook
- run: yarn deploy-storybook -- --ci
env:
GH_TOKEN: Qvant-lab:${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules
coverage
/dist
.out
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: .lintstagedrc
================================================
{
"*.js": ["prettier --write", "git add"],
"*.scss": ["stylelint --fix", "prettier --write", "git add"],
"*.vue": [
"stylelint --fix",
"prettier --parser vue --write",
"eslint --fix --no-ignore",
"git add"
],
"{*.json,.*rc}": ["prettier --parser json --write", "git add"],
"*.{yaml,yml}": ["prettier --parser yaml --write", "git add"]
}
================================================
FILE: .npmignore
================================================
yarn-error.log
/.storybook
/.github
/.readme-assets
================================================
FILE: .nvmrc
================================================
12.22.6
================================================
FILE: .prettierignore
================================================
.nvmrc
/dist
================================================
FILE: .prettierrc
================================================
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}
================================================
FILE: .storybook/locales/en.js
================================================
export default {
qBreadcrumbsStories: {
routeA: 'Route A',
routeB: 'Route B',
routeC: 'Route C',
routeD: 'Route D'
}
};
================================================
FILE: .storybook/locales/index.js
================================================
import en from './en';
import ru from './ru';
import {
en as qMessagesEn,
ru as qMessagesRu
} from '../../src/qComponents/constants/locales';
export default {
en: {
...en,
...qMessagesEn
},
ru: {
...ru,
...qMessagesRu
}
};
================================================
FILE: .storybook/locales/ru.js
================================================
export default {
qBreadcrumbsStories: {
routeA: 'Роут А',
routeB: 'Очень длинный маршрут Б',
routeC: 'Роут С',
routeD: 'Роут D'
}
};
================================================
FILE: .storybook/main.js
================================================
module.exports = {
stories: ['../stories/**/**/*.stories.@(ts|js|mdx)'],
logLevel: 'debug',
addons: [
'@storybook/addon-docs',
'@storybook/addon-controls',
'@storybook/addon-storysource',
'@storybook/preset-scss',
'@storybook/addon-toolbars'
]
};
================================================
FILE: .storybook/manager.js
================================================
import addons from '@storybook/addons';
import theme from './theme';
addons.setConfig({
theme
});
================================================
FILE: .storybook/preview.js
================================================
import Vue from 'vue';
import Qui from '../src/qComponents';
import VueI18n from 'vue-i18n';
import messages from './locales';
Vue.use(VueI18n);
Vue.use(Qui, {
localization: {
locale: 'en'
}
});
export const parameters = {
layout: 'centered',
controls: { expanded: true },
docs: {
inlineStories: true
}
};
export const globalTypes = {
locale: {
name: 'Locale',
description: 'Internationalization locale',
defaultValue: 'en',
toolbar: {
icon: 'globe',
items: [
{ value: 'en', right: '🇺🇸', title: 'English' },
{ value: 'ru', right: '🇷🇺', title: 'Русский' }
]
}
}
};
const i18n = new VueI18n({
locale: 'en',
messages,
silentTranslationWarn: true,
silentFallbackWarn: true,
fallbackRoot: true
});
export const decorators = [
(args, { globals: { locale } }) => ({
i18n,
beforeCreate: function() {
this.$Q.locale = locale;
i18n.locale = locale;
this.$root._i18n = i18n;
},
template: '<story />'
})
];
================================================
FILE: .storybook/theme.js
================================================
import { create } from '@storybook/theming/create';
import logo from '../.readme-assets/qui-logo.svg';
export default create({
name: 'Theme',
base: 'light',
brandTitle: 'QUI',
brandUrl: 'https://qvant-lab.github.io/qui/',
brandImage: logo
});
================================================
FILE: .stylelintignore
================================================
/src/normalize.scss
================================================
FILE: .stylelintrc
================================================
{
"extends": ["stylelint-config-prettier"],
"plugins": ["stylelint-order"],
"rules": {
"color-hex-length": "short",
"color-named": "never",
"font-weight-notation": "numeric",
"function-url-quotes": ["always", { "except": ["empty"] }],
"length-zero-no-unit": true,
"rule-empty-line-before": [
"always-multi-line",
{ "ignore": ["after-comment", "first-nested"] }
],
"selector-pseudo-element-colon-notation": "double",
"order/properties-order": [
"content",
"position",
"top",
"right",
"bottom",
"left",
"z-index",
"display",
"flex",
"flex-grow",
"flex-shrink",
"flex-basis",
"flex-flow",
"flex-direction",
"flex-wrap",
"justify-content",
"align-content",
"align-items",
"order",
"align-self",
"float",
"clear",
"box-sizing",
"width",
"min-width",
"max-width",
"height",
"min-height",
"max-height",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"overflow",
"overflow-x",
"overflow-y",
"list-style",
"list-style-position",
"list-style-type",
"list-style-image",
"border-collapse",
"border-spacing",
"table-layout",
"empty-cells",
"caption-side",
"font",
"font-style",
"font-variant",
"font-weight",
"font-size",
"line-height",
"font-family",
"vertical-align",
"text-align",
"direction",
"color",
"text-transform",
"text-decoration",
"font-size-adjust",
"font-stretch",
"font-effect",
"font-emphasize",
"font-emphasize-position",
"font-emphasize-style",
"font-smooth",
"text-align-last",
"letter-spacing",
"word-spacing",
"white-space",
"text-emphasis",
"text-emphasis-color",
"text-emphasis-style",
"text-emphasis-position",
"text-indent",
"text-justify",
"-ms-writing-mode",
"text-outline",
"text-wrap",
"text-overflow",
"text-overflow-ellipsis",
"text-overflow-mode",
"text-orientation",
"word-wrap",
"word-break",
"tab-size",
"hyphens",
"unicode-bidi",
"columns",
"column-count",
"column-fill",
"column-gap",
"column-rule",
"column-rule-color",
"column-rule-style",
"column-rule-width",
"column-span",
"column-width",
"text-shadow",
"page-break-after",
"page-break-before",
"page-break-inside",
"background",
"background-color",
"background-image",
"linear-gradient",
"background-repeat",
"background-position",
"background-position-x",
"background-position-y",
"background-size",
"background-clip",
"background-origin",
"background-attachment",
"box-decoration-break",
"background-blend-mode",
"border",
"border-width",
"border-style",
"border-color",
"border-top",
"border-top-width",
"border-top-style",
"border-top-color",
"border-right",
"border-right-width",
"border-right-style",
"border-right-color",
"border-bottom",
"border-bottom-width",
"border-bottom-style",
"border-bottom-color",
"border-left",
"border-left-width",
"border-left-style",
"border-left-color",
"border-radius",
"border-top-left-radius",
"border-top-right-radius",
"border-bottom-right-radius",
"border-bottom-left-radius",
"border-image",
"border-image-source",
"border-image-slice",
"border-image-width",
"border-image-outset",
"border-image-repeat",
"outline",
"outline-width",
"outline-style",
"outline-color",
"outline-offset",
"box-shadow",
"transform",
"transform-origin",
"backface-visibility",
"perspective",
"perspective-origin",
"transform-style",
"visibility",
"cursor",
"opacity",
"filter",
"transition",
"transition-delay",
"transition-timing-function",
"transition-duration",
"transition-property",
"animation",
"animation-name",
"animation-duration",
"animation-play-state",
"animation-timing-function",
"animation-delay",
"animation-iteration-count",
"animation-direction",
"quotes",
"counter-reset",
"counter-increment",
"resize",
"user-select",
"nav-index",
"nav-up",
"nav-right",
"nav-down",
"nav-left",
"pointer-events",
"will-change",
"clip",
"clip-path",
"zoom"
]
}
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at . All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Qvant
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<p align="center">
<img src="/.readme-assets/qui-logo.svg?raw=true" />
</p>
<p align="center" class="unchanged rich-diff-level-one">
<!--BADGES-->
<span class="badge-shields"><a href="https://qvant-lab.github.io/qui" title="storybook"><img src="https://img.shields.io/badge/storybook-yes-green.svg" alt="storybook" /></a></span> <span class="badge-shields"><img src="https://img.shields.io/badge/responsive-yes-green.svg" /></span> <span class="badge-npmversion"><a href="https://npmjs.org/package/@qvant/qui" title="View this project on NPM"><img src="https://img.shields.io/npm/v/@qvant/qui.svg" alt="NPM version" /></a></span> <span class="badge-npmdownloads"><a href="https://npmjs.org/package/@qvant/qui" title="View this project on NPM"><img src="https://img.shields.io/npm/dm/@qvant/qui.svg" alt="NPM downloads" /></a></span></span><!--/BADGES--></p>
# A Vue.js Neumorphism Design System for Web
Responsive, user-friendly and lightweight library helping us build great products for our customers. This library for Vue 2.x
‼️ Currently we are working on [Qui Max](https://github.com/Qvant-lab/qui-max) for Vue 3.x, it is more complex and modern version of Qui - we recommend use it instead of this package. Qui remains for Vue 2.x, but we can't claim it will be supported well.
[Storybook (live demo)](https://qvant-lab.github.io/qui/)
What is it?
- 🔩 30+ Vue components
- 📦 icons pack
- 🏳️🌈 colors & grid
- 🥷 neumorphism styles
- 📚 storybook sandbox
Some examples below:






## Install
CDN:
```html
<!-- import CSS -->
<link rel="stylesheet" href="https://unpkg.com/@qvant/qui/dist/qui.css" />
<!-- import JavaScript -->
<script src="https://unpkg.com/@qvant/qui/dist/qui.umd.min.js"></script>
```
Npm | Yarn:
```bash
npm install @qvant/qui -S
yarn add @qvant/qui
```
You can import Qui entirely, or just import what you need. Let's start with fully import.
## Quick setup
In main.js:
```js
import Vue from 'vue';
import Qui from '@qvant/qui';
import '@qvant/qui/dist/qui.css';
// Setup all components
Vue.use(Qui);
// that's it! All components will be imported with styles
```
in YourComponent.vue: (Example)
```vue
<template>
<q-input v-model="value" />
</template>
<script>
export default {
data() {
return {
value: ''
};
},
mounted() {
// the modals have shortcuts in your components:
this.$notify({ ... }) // calls QNotification
this.$message({ ... }) // calls QMessageBox
this.$dialog({ ... }) // calls QDialog
}
};
</script>
```
...or configure quick setup
In main.js:
```js
import Vue from 'vue';
import Qui from '@qvant/qui';
import '@qvant/qui/dist/qui.css';
Vue.use(Qui, {
localization: {
locale: 'en', // Russian language by default, you can set `en` for English
customI18nMessages: {
// rewrite default texts, see the source: src/qComponents/constants/locales
en: {
QDatepicker: {
placeholder: 'Pick your birthday!'
}
}
},
zIndexCounter: 3000, // zIndexCounter is being used by some components, (e.g QPopover, QSelect, QDialog ...etc), 2000 by default
prefix: 'yo' // you can change component's prefix, e.g. must be used <yo-input /> instead of <q-input />
}
});
```
in YourComponent.vue: (Example)
```vue
<template>
<!-- placeholder is changed on 'Pick your birthday!' -->
<yo-datepicker v-model="value" type="date" />
</template>
<script>
export default {
data() {
return {
value: null
};
}
};
</script>
```
Now you have implemented Vue and Qui to your project, and it's time to write your code.
Please refer to each component's [Stories](https://qvant-lab.github.io/qui/) to learn how to use them.
## Not quick setup
If you have a module bundler (e.g webpack), you can import components separately and take care about your bundle size
In main.js:
```js
// import the main plugin from another place (it ensures Qui will be installed without any components, but instance will set required properties and directives)
import Qui from '@qvant/qui/src/onDemand';
// import the component you want
import QButton from '@qvant/qui/src/qComponents/QButton';
// ...or in async way
Vue.component('q-button', () =>
import(/* webpackChunkName: "qui" */ '@qvant/qui/src/qComponents/QButton')
);
// init
Vue.use(Qui);
Vue.use(QButton);
```
In main.scss:
```scss
// need to set the path for files with statics
$--base-path: '~@qvant/qui/src';
// set main styles
@import '~@qvant/qui/src/main.scss';
// notice that you must use `fonts` and `icons` styles for some of components:
@import '~@qvant/qui/src/fonts/index.scss';
@import '~@qvant/qui/src/icons/index.scss';
```
import all styles:
```scss
@import '~@qvant/qui/src/components.scss';
```
...or components separately:
```scss
@import '~@qvant/qui/src/qComponents/QBreadcrumbs/src/q-breadcrumbs.scss';
@import '~@qvant/qui/src/qComponents/QButton/src/q-button.scss';
// ...etc
```
## Optional
- if you want use modals inside your components as property of 'this':
```js
import { QMessageBox, QDialog, QNotification } from '@qvant/qui';
// or import separately
import QMessageBox from '@qvant/qui/src/qComponents/QMessageBox';
import QDialog from '@qvant/qui/src/qComponents/QDialog';
import QNotification from '@qvant/qui/src/qComponents/QNotification';
Vue.prototype.$message = QMessageBox;
Vue.prototype.$dialog = QDialog;
Vue.prototype.$notify = options =>
QNotification({
duration: 3000, // - ms
...options
});
```
- if you use VueI18n, you need to merge messages:
```js
import VueI18n from 'vue-i18n';
import { en, ru } from '@qvant/qui/src/qComponents/constants/locales';
Vue.use(VueI18n);
const messages = {
en: {
message: {
hello: 'hello world'
},
...en
},
ru: {
message: {
hello: 'привет, мир'
},
...ru
}
};
const i18n = new VueI18n({
locale: 'en',
messages
});
new Vue({
i18n
}).$mount('#your-app');
```
## Supported languages
- Russian ✅
- English ✅
- Also you can use any language by setting texts for components via 'customI18nMessages' property in the Qui instance. See the example above.
## Browser Support
Modern browsers are recomended
- safari: >11
- chrome: >=61
- firefox: >=58
- opera: >=62
- edge: >=16
- yandex: >=18
- ie: ? (we don't know :) and will not support it)
## Development
Clone repository and run storybook
```bash
yarn storybook
npm run storybook
```
## LICENSE
MIT
================================================
FILE: babel.config.js
================================================
module.exports = {
presets: ['@vue/cli-plugin-babel/preset']
};
================================================
FILE: jest.config.js
================================================
module.exports = {
moduleFileExtensions: ['js', 'vue', 'json'],
transform: {
'^.+\\.vue$': 'vue-jest',
'^.+\\.js$': 'babel-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub'
},
transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\](?!lodash-es/).+\\.js$'],
setupFiles: ['<rootDir>/tests/unit/setup.js'],
testMatch: ['<rootDir>/src/**/*.test.(ts|tsx|js)'],
testURL: 'http://localhost/',
coverageReporters: ['text-summary', 'html', 'lcov', 'clover'],
coverageDirectory: '<rootDir>/tests/unit/coverage',
coverageThreshold: {
global: {
branches: 7,
functions: 14,
lines: 17
}
}
};
================================================
FILE: package.json
================================================
{
"name": "@qvant/qui",
"version": "1.4.5",
"private": false,
"description": "A Vue.js Design system for Web.",
"author": {
"name": "Qvant Frontend team"
},
"scripts": {
"build": "npx vue-cli-service build --target lib --name qui --entry ./src/qComponents/index.js",
"build-storybook": "build-storybook -c .storybook -o .out",
"create-badges": "node scripts/badges.js",
"deploy-storybook": "storybook-to-ghpages",
"eslint": "eslint --fix --no-ignore .storybook/**/**/*.{vue,js}",
"prettier": "prettier --write '**/*.{js,vue,scss,json,yml}'",
"storybook": "start-storybook -s ./public -p 6006",
"stylelint:fix": "stylelint --fix '**/*.{scss,vue}'",
"test:unit": "jest"
},
"main": "./dist/qui.common.js",
"dependencies": {
"@popperjs/core": "^2.4.4",
"async-validator": "^3.4.0",
"color": "^3.1.2",
"date-fns": "^2.15.0",
"focus-visible": "^5.2.0",
"jest-transform-stub": "^2.0.0",
"lodash-es": "^4.17.15",
"resize-observer-polyfill": "^1.5.0",
"v-click-outside": "^3.1.2",
"vue": "^2.6.11",
"vue-i18n": "^8.22.2"
},
"devDependencies": {
"@babel/core": "^7.10.4",
"@storybook/addon-controls": "^6.1.10",
"@storybook/addon-docs": "^6.1.10",
"@storybook/addon-storysource": "^6.1.10",
"@storybook/addon-toolbars": "^6.1.11",
"@storybook/preset-scss": "^1.0.3",
"@storybook/storybook-deployer": "^2.8.7",
"@storybook/theming": "^6.1.10",
"@storybook/vue": "^6.1.10",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/test-utils": "^1.1.1",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"babel-loader": "^8.1.0",
"babel-preset-vue": "^2.0.2",
"badges": "^4.24.0",
"css-loader": "^4.3.0",
"eslint": "^7.9.0",
"eslint-config-airbnb-base": "^14.1.0",
"eslint-config-prettier": "^6.7.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-vue": "^6.0.1",
"husky": "^4.2.5",
"jest": "^26.6.3",
"lint-staged": "^10.1.7",
"node-sass": "^4.13.0",
"sass-loader": "^10.0.2",
"style-loader": "^1.2.1",
"stylelint": "^13.7.1",
"stylelint-config-prettier": "^8.0.1",
"stylelint-order": "^4.0.0",
"vue-jest": "^3.0.7",
"vue-loader": "^15.9.3",
"vue-template-compiler": "^2.6.11"
},
"_id": "@qvant/qui@1.4.5",
"bugs": {
"url": "https://github.com/Qvant-lab/qui/issues"
},
"contributors": [
{
"name": "Viktor Zheleztsov",
"url": "https://github.com/ViZhe"
},
{
"name": "Maksim Novikov",
"url": "https://github.com/Esvalirion"
},
{
"name": "Shamil Alisultanov",
"url": "https://github.com/shamilfrontend"
}
],
"demo": "https://qvant-lab.github.io/qui/",
"homepage": "https://github.com/Qvant-lab/qui#readme",
"husky": {
"hooks": {
"pre-commit": "lint-staged && yarn test:unit --coverage"
}
},
"keywords": [
"design-system",
"components",
"library",
"ui",
"vue",
"sass",
"neumorphism"
],
"license": "MIT",
"maintainers": [
{
"name": "Tim Bochkarev",
"url": "https://github.com/Tim152"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/Qvant-lab/qui.git"
}
}
================================================
FILE: scripts/badges.js
================================================
// edit `list` & run `node scripts/badges.js` for creating new badges
const { renderBadges } = require('badges');
const fs = require('fs');
// Listing of badges to output
const list = [
[
'shields',
{
left: 'storybook',
right: 'yes',
color: 'green',
alt: 'storybook',
url: 'https://qvant-lab.github.io/qui',
title: 'storybook'
}
],
[
'shields',
{
left: 'responsive',
right: 'yes',
color: 'green',
title: 'responsive'
}
],
'npmversion',
'npmdownloads',
'daviddm',
'daviddmdev'
];
// Configuration for the badges
const config = {
npmPackageName: '@qvant/qui',
saucelabsUsername: 'Qui',
githubSlug: 'Qvant-lab/qui',
nodeicoQueryString: { downloads: true, compact: true, height: 2 },
homepage: 'https://qvant-lab.github.io/qui/'
};
// Options for rendering the badges
const options = {
// Filter Category
// When set to a string, will only render badges from the list that of the specified category
// Values can be 'development', 'testing', 'funding', or 'social'
// E.g. to render only funding badges, set to 'funding'
filterCategory: false,
// Filter Scripts
// When true, do not render any badges from the list that are scripts
filterScripts: false
};
// Render the badges to a string
const badges = renderBadges(list, config, options);
fs.readFile('README.md', 'utf8', function(err, data) {
if (err) {
return console.log(err);
}
const result = data.replace(
/<!--BADGES-->((.|\n)*)<!--\/BADGES-->/g,
`<!--BADGES-->${badges}<!--/BADGES-->`
);
fs.writeFile('README.md', result, 'utf8', function(error) {
if (error) return console.log(error);
});
});
================================================
FILE: src/components.scss
================================================
@import '../qComponents/QBreadcrumbs/src/q-breadcrumbs.scss';
@import '../qComponents/QButton/src/q-button.scss';
@import '../qComponents/QCascader/src/q-cascader.scss';
@import '../qComponents/QCheckbox/src/q-checkbox.scss';
@import '../qComponents/QCheckboxGroup/src/q-checkbox-group.scss';
@import '../qComponents/QCol/src/q-col.scss';
@import '../qComponents/QCollapseItem/src/q-collapse-item.scss';
@import '../qComponents/QColorPicker/src/q-color-picker.scss';
@import '../qComponents/QContextMenu/src/q-context-menu.scss'; //
@import '../qComponents/QDatePicker/src/q-date-picker.scss';
@import '../qComponents/QDialog/src/q-dialog.scss';
@import '../qComponents/QDrawer/src/q-drawer.scss';
@import '../qComponents/QFormItem/src/q-form-item.scss';
@import '../qComponents/QInput/src/q-input.scss';
@import '../qComponents/QInputNumber/src/q-input-number.scss';
@import '../qComponents/QMessageBox/src/q-message-box.scss';
@import '../qComponents/QNotification/src/q-notification.scss';
@import '../qComponents/QOption/src/q-option.scss';
@import '../qComponents/QPagination/src/q-pagination.scss';
@import '../qComponents/QPopover/src/q-popover.scss';
@import '../qComponents/QRadio/src/q-radio.scss';
@import '../qComponents/QRadioGroup/src/q-radio-group.scss';
@import '../qComponents/QRow/src/q-row.scss';
@import '../qComponents/QScrollbar/src/q-scrollbar.scss';
@import '../qComponents/QSelect/src/q-select.scss';
@import '../qComponents/QSlider/src/q-slider.scss';
@import '../qComponents/QTable/src/q-table.scss';
@import '../qComponents/QTabPane/src/q-tab-pane.scss';
@import '../qComponents/QTabs/src/q-tabs.scss';
@import '../qComponents/QTag/src/q-tag.scss';
@import '../qComponents/QTextarea/src/q-textarea.scss';
@import '../qComponents/QTimePicker/src/q-time-picker.scss';
@import '../qComponents/QUpload/src/q-upload.scss';
================================================
FILE: src/fonts/index.scss
================================================
$--base-path: '..' !default;
@font-face {
font-display: swap;
font-style: normal;
font-weight: 800;
font-family: 'Gilroy';
src: local('Gilroy ExtraBold'), local('Gilroy-ExtraBold'),
url('#{$--base-path}/fonts/Gilroy-ExtraBold.woff?29042020') format('woff');
}
@font-face {
font-display: swap;
font-style: normal;
font-weight: 600;
font-family: 'Gilroy';
src: local('Gilroy Bold'), local('Gilroy-Bold'),
url('#{$--base-path}/fonts/Gilroy-Bold.woff?29042020') format('woff');
}
@font-face {
font-display: swap;
font-style: normal;
font-weight: 500;
font-family: 'Gilroy';
src: local('Gilroy Medium'), local('Gilroy-Medium'),
url('#{$--base-path}/fonts/Gilroy-Medium.woff?29042020') format('woff');
}
@font-face {
font-display: swap;
font-style: normal;
font-weight: 400;
font-family: 'Gilroy';
src: local('Gilroy Regular'), local('Gilroy-Regular'),
url('#{$--base-path}/fonts/Gilroy-Regular.woff?29042020') format('woff');
}
================================================
FILE: src/icons/index.scss
================================================
$--base-path: '..' !default;
@font-face {
font-style: normal;
font-weight: 400;
font-family: 'qicon';
src: url('#{$--base-path}/icons/qicon.woff?07092021') format('woff');
font-display: block;
}
[class^='q-icon-'],
[class*=' q-icon-'] {
display: inline-block;
font-style: normal;
font-variant: normal;
font-weight: 400;
line-height: 1;
// use !important to prevent issues with browser extensions that change fonts
// stylelint-disable-next-line font-family-no-missing-generic-family-keyword
font-family: 'qicon' !important;
vertical-align: baseline;
text-transform: none;
letter-spacing: 0;
speak: none;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.q-icon-y-4::before {
content: '\e900';
}
.q-icon-y-3::before {
content: '\e901';
}
.q-icon-y-2::before {
content: '\e902';
}
.q-icon-y-1::before {
content: '\e903';
}
.q-icon-withdraw-fill::before {
content: '\e904';
}
.q-icon-wifi::before {
content: '\e905';
}
.q-icon-wallet::before {
content: '\e906';
}
.q-icon-wallet-stroke::before {
content: '\e907';
}
.q-icon-view-list::before {
content: '\e908';
}
.q-icon-triangle-up::before {
content: '\e909';
}
.q-icon-triangle-right::before {
content: '\e90a';
}
.q-icon-triangle-left::before {
content: '\e90b';
}
.q-icon-triangle-down::before {
content: '\e90c';
}
.q-icon-trash-bin::before {
content: '\e90d';
}
.q-icon-trash-bin-stroke::before {
content: '\e90e';
}
.q-icon-target::before {
content: '\e90f';
}
.q-icon-stop-2::before {
content: '\e910';
}
.q-icon-stop-1::before {
content: '\e911';
}
.q-icon-star::before {
content: '\e912';
}
.q-icon-star-fill::before {
content: '\e913';
}
.q-icon-settings-vertical::before {
content: '\e914';
}
.q-icon-settings-horizontal::before {
content: '\e915';
}
.q-icon-search::before {
content: '\e916';
}
.q-icon-save::before {
content: '\e917';
}
.q-icon-rubles::before {
content: '\e918';
}
.q-icon-rubles-circle::before {
content: '\e919';
}
.q-icon-router::before {
content: '\e91a';
}
.q-icon-router-arrow-down::before {
content: '\e91b';
}
.q-icon-reverse::before {
content: '\e91c';
}
.q-icon-question::before {
content: '\e91d';
}
.q-icon-question-mark::before {
content: '\e91e';
}
.q-icon-proceed-2::before {
content: '\e91f';
}
.q-icon-proceed-1::before {
content: '\e920';
}
.q-icon-plus::before {
content: '\e921';
}
.q-icon-play::before {
content: '\e922';
}
.q-icon-piggy-bank-fill::before {
content: '\e923';
}
.q-icon-pic::before {
content: '\e924';
}
.q-icon-phone-settings-stroke::before {
content: '\e925';
}
.q-icon-percent::before {
content: '\e926';
}
.q-icon-pencil-square-stroke::before {
content: '\e927';
}
.q-icon-pencil-list::before {
content: '\e928';
}
.q-icon-pause::before {
content: '\e929';
}
.q-icon-multiply-fill::before {
content: '\e92a';
}
.q-icon-minus::before {
content: '\e92b';
}
.q-icon-menu::before {
content: '\e92c';
}
.q-icon-menu-2-fill::before {
content: '\e92d';
}
.q-icon-logout::before {
content: '\e92e';
}
.q-icon-login::before {
content: '\e92f';
}
.q-icon-lock::before {
content: '\e930';
}
.q-icon-lock-fill::before {
content: '\e931';
}
.q-icon-key::before {
content: '\e932';
}
.q-icon-info::before {
content: '\e933';
}
.q-icon-info-stroke::before {
content: '\e934';
}
.q-icon-info-fill::before {
content: '\e935';
}
.q-icon-house-stroke::before {
content: '\e936';
}
.q-icon-graph-gisto::before {
content: '\e937';
}
.q-icon-finish-fill::before {
content: '\e938';
}
.q-icon-filter-stroke::before {
content: '\e939';
}
.q-icon-filter-fill::before {
content: '\e93a';
}
.q-icon-file::before {
content: '\e93b';
}
.q-icon-eye::before {
content: '\e93c';
}
.q-icon-eye-fill::before {
content: '\e93d';
}
.q-icon-eye-close::before {
content: '\e93e';
}
.q-icon-envelope-edit::before {
content: '\e93f';
}
.q-icon-earth::before {
content: '\e940';
}
.q-icon-drag-linear::before {
content: '\e941';
}
.q-icon-drag-vertical-fill::before {
content: '\e942';
}
.q-icon-double-triangle-right::before {
content: '\e943';
}
.q-icon-double-triangle-left::before {
content: '\e944';
}
.q-icon-dots-3-horizontal::before {
content: '\e945';
}
.q-icon-diagram-round::before {
content: '\e946';
}
.q-icon-diagram-round-stroke::before {
content: '\e947';
}
.q-icon-database-arrow-down::before {
content: '\e948';
}
.q-icon-credit-card::before {
content: '\e949';
}
.q-icon-comment::before {
content: '\e94a';
}
.q-icon-cog-stroke::before {
content: '\e94b';
}
.q-icon-cloud-upload::before {
content: '\e94c';
}
.q-icon-cog-fill::before {
content: '\e94d';
}
.q-icon-close::before {
content: '\e94e';
}
.q-icon-clock-stroke::before {
content: '\e94f';
}
.q-icon-checkbox-square-multiply-non::before {
content: '\e950';
}
.q-icon-check::before {
content: '\e951';
}
.q-icon-change-list::before {
content: '\e952';
}
.q-icon-chain::before {
content: '\e953';
}
.q-icon-cart::before {
content: '\e954';
}
.q-icon-calendar::before {
content: '\e955';
}
.q-icon-calendar-refresh::before {
content: '\e956';
}
.q-icon-calendar-clock::before {
content: '\e957';
}
.q-icon-bell::before {
content: '\e958';
}
.q-icon-bell-ring::before {
content: '\e959';
}
.q-icon-attention-mark::before {
content: '\e95a';
}
.q-icon-attach-fill::before {
content: '\e95b';
}
.q-icon-arrow-up::before {
content: '\e95c';
}
.q-icon-arrow-right::before {
content: '\e95d';
}
.q-icon-arrow-left::before {
content: '\e95e';
}
.q-icon-arrow-down::before {
content: '\e95f';
}
.q-icon-alert-stroke::before {
content: '\e960';
}
.q-icon-archive-arrow-down::before {
content: '\e961';
}
.q-icon-alert-fill::before {
content: '\e962';
}
.q-icon-account::before {
content: '\e963';
}
.q-icon-account-settings::before {
content: '\e964';
}
.q-icon-account-group::before {
content: '\e965';
}
.q-icon-account-group-web::before {
content: '\e966';
}
.q-icon-account-couple-fill::before {
content: '\e967';
}
.q-icon-account-check-fill::before {
content: '\e968';
}
================================================
FILE: src/main.scss
================================================
@import './normalize.scss';
@import './vars.scss';
@import './transition.scss';
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body,
#app {
height: 100%;
}
body {
margin: 0;
font-weight: var(--font-weight-base);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
font-family: 'Gilroy', sans-serif;
color: rgba(var(--color-rgb-gray), 0.64);
letter-spacing: var(--letter-spacing-base);
background-color: var(--color-tertiary-gray-lighter);
}
svg {
vertical-align: middle;
}
button {
cursor: pointer;
&:focus {
outline: none;
}
}
a {
color: var(--color-primary-blue);
text-decoration: none;
&:focus {
outline: none;
}
}
/*
:focus-visible polyfill.
This will hide the focus indicator if the element receives focus via the mouse,
but it will still show up on keyboard focus.
*/
.js-focus-visible :focus:not(.focus-visible) {
outline: none;
}
================================================
FILE: src/normalize.scss
================================================
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
margin: 0.67em 0;
font-size: 2em;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-size: 1em; /* 2 */
font-family: monospace, monospace; /* 1 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
border-bottom: none; /* 1 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-size: 1em; /* 2 */
font-family: monospace, monospace; /* 1 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
margin: 0; /* 2 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
font-family: inherit; /* 1 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
padding: 0;
border-style: none;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
display: table; /* 1 */
box-sizing: border-box; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
color: inherit; /* 2 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type='checkbox'],
[type='radio'] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
================================================
FILE: src/onDemand.js
================================================
/* eslint-disable global-require */
/* eslint-disable no-param-reassign */
import vClickOutside from 'v-click-outside';
import 'focus-visible';
import { version } from '../package.json';
import { installI18n } from './qComponents/constants/locales';
const install = (
Vue,
{
localization: { locale = 'ru', customI18nMessages = {} } = {},
zIndexCounter = 2000
} = {}
) => {
Vue.prototype.$Q = {};
// define plugins
Object.defineProperties(Vue.prototype.$Q, {
zIndex: {
get() {
zIndexCounter += 1;
return zIndexCounter;
}
},
locale: {
value: locale
}
});
Vue.use(vClickOutside);
installI18n({ locale, customI18nMessages });
};
const Qui = {
version,
install
};
export default Qui;
================================================
FILE: src/qComponents/QBreadcrumbs/QBreadcrumbs.test.js
================================================
import Component from './src/QBreadcrumbs';
describe('QBreadcrumbs', () => {
let instance;
let options;
beforeEach(() => {
options = {
mocks: {
$route: { matched: [{ meta: 'one' }] }
}
};
instance = shallowMount(Component, options);
});
it('should match snapshot', () => {
expect(instance.element).toMatchSnapshot();
});
describe('computed', () => {
it('lastCrumb should return custom last crumb if it has been passed', () => {
const expected = 'one';
instance.setProps({
last: expected
});
expect(instance.vm.lastCrumb).toEqual(expected);
});
});
describe('pushTo', () => {
it(`should return the router's 'name' if it has been passed`, () => {
const route = { name: 'R_MAIN', path: '/main' };
const expected = { name: route.name };
expect(instance.vm.pushTo(route)).toEqual(expected);
});
it(`should return the router's 'path' if the 'name' hasn't been passed`, () => {
const route = { path: '/main' };
const expected = route.path;
expect(instance.vm.pushTo(route)).toEqual(expected);
});
});
});
================================================
FILE: src/qComponents/QBreadcrumbs/__snapshots__/QBreadcrumbs.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QBreadcrumbs should match snapshot 1`] = `
<div
class="q-breadcrumbs"
>
<div
class="q-breadcrumbs__crumb q-breadcrumbs__crumb_last"
>
</div>
</div>
`;
================================================
FILE: src/qComponents/QBreadcrumbs/index.js
================================================
import QBreadcrumbs from './src/QBreadcrumbs.vue';
/* istanbul ignore next */
QBreadcrumbs.install = function(Vue) {
Vue.component(QBreadcrumbs.name, QBreadcrumbs);
};
export default QBreadcrumbs;
================================================
FILE: src/qComponents/QBreadcrumbs/src/QBreadcrumbs.vue
================================================
<template>
<div class="q-breadcrumbs">
<template v-for="crumb in breadcrumbs">
<router-link
:key="crumb.name || crumb.path"
:to="pushTo(crumb)"
active-class="q-breadcrumbs__crumb_active"
exact-active-class="q-breadcrumbs__crumb_exact-active"
class="q-breadcrumbs__crumb"
>
{{ crumb.meta.breadcrumb }}
</router-link>
<span
:key="`${crumb.name || crumb.path}divider`"
class="q-breadcrumbs__divider q-icon-arrow-right"
/>
</template>
<div class="q-breadcrumbs__crumb q-breadcrumbs__crumb_last">
{{ lastCrumb }}
</div>
</div>
</template>
<script>
export default {
name: 'QBreadcrumbs',
componentName: 'QBreadcrumbs',
/**
* if you have vue router, QBreadcrumbs will get crumbs from this.$route.matched
*/
props: {
/**
* custom last crumb
*/
last: {
type: String,
default: null
},
/**
* Array of Objects, object must contain required fields: `path` - uses as route path, `name` - route name, `meta` - must contain `breadcrumb` - visible title
*/
customRoutes: {
type: Array,
default: null
}
},
computed: {
crumbs() {
const routes = this.customRoutes ?? this.$route.matched;
return routes.filter(route => route.meta?.breadcrumb);
},
breadcrumbs() {
const breadcrumbs = [...this.crumbs];
breadcrumbs.pop();
return breadcrumbs;
},
lastCrumb() {
if (this.last) return this.last;
return this.crumbs[this.crumbs.length - 1]?.meta.breadcrumb ?? '';
}
},
methods: {
pushTo({ name, path }) {
return name ? { name } : path;
}
}
};
</script>
================================================
FILE: src/qComponents/QBreadcrumbs/src/q-breadcrumbs.scss
================================================
.q-breadcrumbs {
display: inline-grid;
grid-auto-flow: column;
align-items: baseline;
max-width: 100%;
&__divider {
position: relative;
top: 5px;
margin-right: 8px;
margin-left: 8px;
font-size: 24px;
color: rgba(var(--color-rgb-gray), 0.32);
}
&__crumb {
overflow: hidden;
font-weight: 800;
font-size: 24px;
line-height: 31px;
color: var(--color-primary-blue);
white-space: nowrap;
text-overflow: ellipsis;
&:focus {
text-decoration: underline;
}
&_last:first-child {
color: var(--color-primary-black);
}
&_exact-active:not(:first-child),
&_last:not(:first-child) {
font-weight: var(--font-weight-bold);
font-size: 16px;
line-height: var(--line-height-base);
color: var(--color-primary-black);
cursor: default;
}
}
}
================================================
FILE: src/qComponents/QButton/QButton.test.js
================================================
import Component from './src/QButton';
describe('QButton', () => {
it('should match snapshot', () => {
const { element } = shallowMount(Component);
expect(element).toMatchSnapshot();
});
it(`should have class 'q-button_type_icon' if type is icon`, () => {
const instance = shallowMount(Component);
const expected = 'q-button_type_icon';
instance.setProps({
type: 'icon'
});
expect(instance.vm.classes).toContain(expected);
});
it(`should have class 'q-button_theme_link' if theme is link`, () => {
const instance = shallowMount(Component);
const expected = 'q-button_theme_link';
instance.setProps({
theme: 'link'
});
expect(instance.vm.classes).toContain(expected);
});
describe('handleClick', () => {
it('should emit click', () => {
const instance = shallowMount(Component);
instance.vm.handleClick();
expect(instance.emitted().click).toBeTruthy();
});
});
});
================================================
FILE: src/qComponents/QButton/__snapshots__/QButton.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QButton should match snapshot 1`] = `
<button
class="q-button q-button_theme_primary q-button_type_default q-button_size_medium"
type="button"
>
<!---->
<!---->
<!---->
</button>
`;
================================================
FILE: src/qComponents/QButton/index.js
================================================
import QButton from './src/QButton.vue';
/* istanbul ignore next */
QButton.install = function(Vue) {
Vue.component(QButton.name, QButton);
};
export default QButton;
================================================
FILE: src/qComponents/QButton/src/QButton.vue
================================================
<template>
<button
class="q-button"
:disabled="disabled || loading"
:autofocus="autofocus"
:type="nativeType"
:class="classes"
@click="handleClick"
>
<span
v-if="loading"
class="q-icon-reverse"
/>
<span
v-if="icon && !loading"
:class="icon"
/>
<span
v-if="$slots.default"
class="q-button__inner"
>
<slot />
</span>
</button>
</template>
<script>
export default {
name: 'QButton',
componentName: 'QButton',
inject: {
qForm: {
default: null
}
},
props: {
type: {
type: String,
default: 'default',
validator: value => ['default', 'icon'].includes(value)
},
theme: {
type: String,
default: 'primary',
validator: value => ['primary', 'secondary', 'link'].includes(value)
},
size: {
type: String,
default: 'medium',
validator: value => ['small', 'medium'].includes(value)
},
/**
* any q-icon
*/
icon: {
type: String,
default: ''
},
/**
* as native button type
*/
nativeType: {
type: String,
default: 'button'
},
/**
* whether to show loader inside button
*/
loading: {
type: Boolean,
default: false
},
/**
* whether the button is disabled
*/
disabled: {
type: Boolean,
default: false
},
/**
* as native button autofocus
*/
autofocus: {
type: Boolean,
default: false
},
/**
* change button's shape to circle (use with type icon)
*/
circle: {
type: Boolean,
default: false
},
/**
* sets button width to 100%
*/
fullWidth: {
type: Boolean,
default: false
}
},
computed: {
classes() {
const classes = Object.entries({
theme: this.theme,
type: this.type,
size: this.size
})
.filter(([, value]) => Boolean(value))
.map(([key, value]) => `q-button_${key}_${value}`);
classes.push({
'q-button_disabled': this.disabled || (this.qForm?.disabled ?? false),
'q-button_loading': this.loading,
'q-button_circle': this.circle,
'q-button_full-width': this.fullWidth
});
return classes;
}
},
methods: {
handleClick(evt) {
this.$emit('click', evt);
}
}
};
</script>
================================================
FILE: src/qComponents/QButton/src/q-button.scss
================================================
.q-button {
display: inline-block;
box-sizing: border-box;
max-height: 40px;
padding: 12px 40px;
font-weight: var(--font-weight-bold);
font-size: 12px;
line-height: var(--line-height-button);
vertical-align: middle;
text-align: center;
color: var(--color-tertiary-white);
text-transform: uppercase;
letter-spacing: var(--letter-spacing-base);
white-space: nowrap;
background-color: var(--color-primary);
background-image: var(--gradient-primary);
border: none;
border-radius: var(--border-radius-base);
outline: none;
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4),
4px 4px 8px rgba(var(--color-rgb-blue), 0.4),
-4px -4px 12px var(--color-tertiary-white);
cursor: pointer;
transition: background-color 0.1s;
appearance: none;
& + & {
margin-left: 16px;
}
&:focus {
outline: none;
}
&:hover {
background-color: var(--color-primary);
background-image: none;
box-shadow: -1px -1px 4px rgba(var(--color-rgb-white), 0.25),
1px 1px 4px rgba(var(--color-rgb-blue), 0.4),
4px 4px 8px rgba(var(--color-rgb-blue), 0.4),
-4px -4px 8px rgba(var(--color-rgb-white), 0.8);
}
&:active {
background-image: none;
box-shadow: var(--box-shadow-pressed);
}
&:visited {
background-image: var(--gradient-primary);
box-shadow: var(--box-shadow-primary);
}
&::-moz-focus-inner {
border: 0;
}
&_type {
&_icon {
display: inline-flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
padding: 0;
font-size: 26px;
}
}
&_theme {
&_primary {
&.focus-visible {
background-color: var(--color-primary-darker);
background-image: none;
}
}
&_secondary {
color: var(--color-primary-blue);
background-color: var(--color-tertiary-gray);
background-image: none;
box-shadow: var(--box-shadow-primary);
&:hover {
color: var(--color-tertiary-white);
background-color: var(--color-primary);
box-shadow: var(--box-shadow-hover);
}
&:active {
outline: none;
box-shadow: var(--box-shadow-pressed);
}
&.focus-visible {
color: var(--color-tertiary-white);
background-color: var(--color-primary-darker);
}
}
&_link {
padding: 0;
font-weight: var(--font-weight-base);
font-size: var(--font-size-base);
color: var(--color-primary-blue);
text-transform: initial;
background-color: transparent;
background-image: none;
box-shadow: none;
&.focus-visible {
text-decoration: underline;
}
&:hover {
color: var(--color-primary-black);
background-color: transparent;
box-shadow: none;
}
&:active {
outline: none;
box-shadow: none;
}
}
}
&_size {
&_small {
&.q-button_type_icon {
width: 24px;
height: 24px;
padding: 0;
font-size: 16px;
line-height: 1;
}
&.q-button_theme_link {
font-size: 12px;
}
}
}
&_full-width {
width: 100%;
}
&_disabled {
--color: rgba(var(--color-rgb-gray), 0.64);
--background-color: var(--color-tertiary-gray);
--box-shadow: 1px 1px 3px rgba(var(--color-rgb-blue), 0.4),
-1px -1px 3px rgba(var(--color-rgb-white), 0.25);
color: var(--color);
background-color: var(--background-color);
background-image: none;
box-shadow: var(--box-shadow);
cursor: not-allowed;
&:hover,
&:active {
color: var(--color);
background-color: var(--background-color);
box-shadow: var(--box-shadow);
}
&.q-button_theme_link {
--box-shadow: none;
--background-color: transparent;
}
}
&_loading {
position: relative;
pointer-events: none;
.q-button__inner {
margin-left: 0;
visibility: hidden;
}
.q-icon-reverse {
--icon-size: 24px;
position: absolute;
top: calc(50% - var(--icon-size) / 2);
left: calc(50% - var(--icon-size) / 2);
font-size: var(--icon-size);
transform-origin: calc(var(--icon-size) / 2);
animation: rotating 2s linear infinite;
}
&.q-button_size_small {
.q-icon-reverse {
--icon-size: 16px;
}
}
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: inherit;
pointer-events: none;
}
}
&_circle {
border-radius: 50%;
}
}
@keyframes rotating {
0% {
transform: rotateZ(0deg);
}
100% {
transform: rotateZ(360deg);
}
}
================================================
FILE: src/qComponents/QCascader/QCascader.test.js
================================================
import Component from './src/QCascader';
const module = require('../helpers');
module.randId = jest.fn();
describe('QCascader', () => {
let instance;
let options;
beforeEach(() => {
options = {
mocks: { $t: () => {} },
propsData: {
value: 'resource',
placeholder: 'placeholder',
options: [
{
value: 'guide',
label: 'Guide',
children: [
{
value: 'child',
label: 'Child',
children: [{ value: 'next child', label: 'Next child' }]
}
]
},
{ value: 'resource', label: 'Resource' }
]
}
};
instance = mount(Component, options);
});
it('should match snapshot', () => {
const spy = prefix => `${prefix}000`;
module.randId.mockImplementation(spy);
instance = mount(Component, options);
expect(instance.element).toMatchSnapshot();
});
it('data should match snapshot', () => {
expect(Component.data()).toMatchSnapshot();
});
describe('computed', () => {
describe('model', () => {
it('should set inputValue if has been changed', () => {
const expected = 'guide';
instance.vm.model = expected;
expect(instance.vm.inputValue).toEqual(expected);
});
});
describe('isClearBtnVisible', () => {
it('should return false if showClose is false', async () => {
await instance.setData({ showClose: false });
expect(instance.vm.isClearBtnVisible).toBeFalsy();
});
it('should return true if showClose is true', async () => {
await instance.setData({ showClose: true });
expect(instance.vm.isClearBtnVisible).toBeTruthy();
});
});
});
describe('watch', () => {
describe('value', () => {
it('should set checkedValues if value is array', async () => {
const expected = ['guide', 'child'];
await instance.setProps({ value: expected });
expect(instance.vm.checkedValues).toEqual(expected);
});
});
});
});
================================================
FILE: src/qComponents/QCascader/__snapshots__/QCascader.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QCascader data should match snapshot 1`] = `
Object {
"areTagsHovered": false,
"checkedNodes": Array [],
"checkedValues": Array [],
"focus": false,
"id": "q-cascader-000",
"inputInitialHeight": 0,
"inputValue": null,
"popper": null,
"presentText": null,
"showClose": false,
}
`;
exports[`QCascader should match snapshot 1`] = `
<div
class="q-cascader"
>
<div
class="q-input q-input_suffix"
>
<!---->
<input
aria-controls="q-cascader-000"
aria-haspopup="true"
aria-label=""
autocomplete="off"
class="q-input__inner"
placeholder="placeholder"
readonly="readonly"
role="button"
tabindex=""
type="text"
/>
<span
class="q-input__suffix"
>
<span
class="q-input__suffix-inner"
>
<span
class="q-input__icon q-icon-triangle-down"
/>
<!---->
<!---->
</span>
</span>
</div>
<!---->
<div
class="q-cascader-panel"
>
<div
aria-labelledby="q-cascader-000"
class="q-cascader-menu"
role="menu"
>
<div
class="q-cascader-menu__wrap"
>
<div
class="q-scrollbar q-scrollbar_has-horizontal-bar q-scrollbar_has-vertical-bar"
>
<div
class="q-scrollbar__wrap q-cascader-menu__scrollbar q-scrollbar__wrap_hidden-default"
>
<div
class="q-scrollbar__view"
>
<div
class="q-cascader-node"
id="q-cascader-000-node-0-0"
role="menuitem"
tabindex="-1"
>
<!---->
<span
class="q-cascader-node__label"
>
Guide
</span>
<span
class="q-cascader-node__postfix q-icon-triangle-right"
/>
</div>
<div
class="q-cascader-node"
id="q-cascader-000-node-0-1"
role="menuitem"
tabindex="-1"
>
<!---->
<span
class="q-cascader-node__label"
>
Resource
</span>
<!---->
</div>
</div>
</div>
<div
class="q-scrollbar__bar q-scrollbar__bar_horizontal"
>
<div
class="q-scrollbar__thumb"
style="width: 0px; transform: translateX(0%); webkit-transform: translateX(0%);"
/>
</div>
<div
class="q-scrollbar__bar q-scrollbar__bar_vertical"
>
<div
class="q-scrollbar__thumb"
style="height: 0px; transform: translateY(0%); webkit-transform: translateY(0%);"
/>
</div>
</div>
</div>
</div>
</div>
</div>
`;
================================================
FILE: src/qComponents/QCascader/index.js
================================================
import QCascader from './src/QCascader';
/* istanbul ignore next */
QCascader.install = function(Vue) {
Vue.component(QCascader.name, QCascader);
};
export default QCascader;
================================================
FILE: src/qComponents/QCascader/src/QCascader.vue
================================================
<template>
<div
ref="reference"
v-click-outside="hidePopper"
:class="['q-cascader', { 'q-cascader_disabled': isDisabled }]"
>
<q-input
ref="input"
v-model="model"
readonly
:placeholder="calcPlaceholder"
:disabled="isDisabled"
:validate-event="false"
aria-haspopup="true"
role="button"
:aria-controls="id"
:aria-expanded="popper"
:class="{
'q-input_focus': Boolean(popper),
'q-input_hover': areTagsHovered
}"
@mouseenter.native="handleMouseEnter"
@mouseleave.native="showClose = false"
@focus="handleInputFocus"
@click="togglePopper"
>
<template slot="suffix">
<span
v-if="isClearBtnVisible"
key="clear"
class="q-input__icon q-icon-close"
@click.stop="handleClear"
/>
<span
v-else
key="arrow-down"
:class="[
'q-input__icon',
'q-icon-triangle-down',
Boolean(popper) && 'q-icon-triangle-down_reverse'
]"
/>
</template>
</q-input>
<div
v-if="multiple"
class="q-cascader__tags"
@click="togglePopper"
@mouseenter="handleTagsHover"
@mouseleave="handleTagsLeave"
>
<template v-if="collapseTags">
<q-tag
v-for="(tag, index) in checkedNodes.slice(0, 1)"
:key="index"
closable
@close="deleteTag(tag)"
>
<span
v-if="allLevelsShown"
class="q-cascader__tag-text"
>{{
tag.fullPathLabel
}}</span>
<span
v-else
class="q-cascader__tag-text"
>{{ tag.label }}</span>
</q-tag>
<q-tag v-if="checkedNodes.length > 1">
<span
class="q-cascader__tag-text"
>+{{ checkedNodes.length - 1 }}</span>
</q-tag>
</template>
<template v-else>
<q-tag
v-for="(tag, index) in checkedNodes"
:key="index"
closable
@close="deleteTag(tag)"
>
<span
v-if="allLevelsShown"
class="q-cascader__tag-text"
>{{
tag.fullPathLabel
}}</span>
<span
v-else
class="q-cascader__tag-text"
>{{ tag.label }}</span>
</q-tag>
</template>
</div>
<q-cascader-panel
ref="panel"
v-model="checkedValues"
:options="options"
/>
</div>
</template>
<script>
import { createPopper } from '@popperjs/core';
import Emitter from '../../mixins/emitter';
import { addResizeListener, removeResizeListener, randId } from '../../helpers';
import QCascaderPanel from './QCascaderPanel';
const DEFAULT_INPUT_HEIGHT = 40;
const findFullPath = (branches, find) => {
let level = null;
for (let i = 0; i < branches.length; i += 1) {
if (branches[i].value === find) {
level = [branches[i].label];
break;
}
if (branches[i].children) {
const nextLevel = findFullPath(branches[i].children, find);
if (nextLevel !== null) {
level = [branches[i].label, nextLevel].flat(Infinity);
}
}
}
return level;
};
const getCheckedNodes = (options, checkedValues, separator) => {
return options.reduce((acc, firstLevelOption) => {
const getChecked = option => {
if (checkedValues.includes(option.value)) {
const transformedOption = {
...option,
fullPathLabel: findFullPath(options, option.value).join(separator)
};
acc.push(transformedOption);
}
if (option.children) {
option.children.forEach(getChecked);
}
};
getChecked(firstLevelOption);
return acc;
}, []);
};
export default {
name: 'QCascader',
componentName: 'QCascader',
components: {
QCascaderPanel
},
mixins: [Emitter],
inject: {
qForm: {
default: null
},
qFormItem: {
default: null
}
},
props: {
/**
* Array for multiple value, String for single
*/
value: { type: [String, Array], default: null },
/**
* array of objects with required fields, example:
* ```{
value: 'guide',
label: 'Guide',
children: [{ ... }]
}```
*/
options: { type: Array, default: null },
/**
* as native placeholder
*/
placeholder: {
type: String,
default: null
},
/**
* whether QCascader is disabled
*/
disabled: {
type: Boolean,
default: false
},
/**
* whether QCascader is clearable
*/
clearable: {
type: Boolean,
default: true
},
/**
* pick several values
*/
multiple: {
type: Boolean,
default: false
},
/**
* check each value as independent
*/
checkStrictly: {
type: Boolean,
default: false
},
/**
* separator in tags
*/
separator: {
type: String,
default: ' / '
},
/**
* whether all path to value in tags is shown
*/
allLevelsShown: {
type: Boolean,
default: true
},
/**
* hide tags in counter
*/
collapseTags: { type: Boolean, default: true }
},
provide() {
return {
cascader: this
};
},
data() {
return {
checkedValues: [],
inputValue: null,
presentText: null,
checkedNodes: [],
inputInitialHeight: 0,
popper: null,
showClose: false,
areTagsHovered: false,
focus: false,
id: randId('q-cascader-')
};
},
computed: {
model: {
get() {
return this.presentText;
},
set(value) {
this.inputValue = value;
}
},
calcPlaceholder() {
return this.checkedNodes.length
? ''
: this.placeholder ?? this.$t('QCascader.placeholder');
},
isDisabled() {
return this.disabled || (this.qForm?.disabled ?? false);
},
isClearBtnVisible() {
return Boolean(this.value?.length) && this.showClose;
},
panel() {
return this.$refs.panel;
}
},
watch: {
value: {
immediate: true,
handler(value) {
if (Array.isArray(value) || (this.multiple && value === null)) {
this.checkedValues = value;
} else {
this.inputValue = value;
}
}
},
checkedValues(val) {
this.calcTags(val);
},
inputValue: {
immediate: true,
handler(val) {
if (val) {
const fullPath = findFullPath(this.options, val);
if (this.allLevelsShown) {
this.presentText = fullPath.join(this.separator);
} else {
this.presentText = fullPath[fullPath.length - 1];
}
} else {
this.presentText = null;
if (this.$refs.panel) {
this.$refs.panel.activePath = [];
this.$refs.panel.menus = [this.options];
}
}
}
},
checkedNodes() {
if (this.multiple) {
this.$nextTick(this.updateStyle);
}
}
},
mounted() {
const { input } = this.$refs;
this.inputInitialHeight = input?.$el?.offsetHeight ?? DEFAULT_INPUT_HEIGHT;
addResizeListener(this.$el, this.updateStyle);
document.addEventListener('keyup', this.handleKeyUp, true);
},
beforeDestroy() {
removeResizeListener(this.$el, this.updateStyle);
document.removeEventListener('keyup', this.handleKeyUp, true);
},
methods: {
handleKeyUp(e) {
if (!this.focus) return;
if (e.target.classList.contains('q-input__inner') && e.key === 'Enter') {
this.togglePopper();
}
switch (e.key) {
case 'Escape': {
this.$refs.input.blur();
this.hidePopper();
break;
}
case 'Backspace': {
this.deleteTag();
break;
}
case 'Tab': {
if (!this.$refs.reference.contains(document.activeElement)) {
this.hidePopper();
this.focus = false;
}
break;
}
case 'ArrowRight':
case 'ArrowUp':
case 'ArrowLeft':
case 'ArrowDown': {
this.$refs.panel.navigateFocus(e);
break;
}
default:
break;
}
},
handleTagsHover() {
this.areTagsHovered = true;
this.showClose = true;
},
handleTagsLeave() {
this.areTagsHovered = false;
this.showClose = false;
},
handleMouseEnter() {
if (this.disabled) return;
if (this.clearable && this.value) {
this.showClose = true;
}
},
deleteTag({ value } = {}) {
if (!this.checkedValues) return;
const result = new Set(this.checkedValues);
if (value) {
result.delete(value);
} else {
result.delete(this.checkedValues[this.checkedValues.length - 1]);
}
const payload = Array.from(result);
this.emit(payload.length ? payload : null);
},
emit(val) {
this.$emit('change', val);
this.qFormItem?.validateField('change');
},
handleClear() {
this.emit(null);
this.showClose = false;
},
calcTags(checkedValues) {
if (!checkedValues) {
this.checkedNodes = [];
return;
}
this.checkedNodes = getCheckedNodes(
this.options,
this.checkedValues,
this.separator
);
},
togglePopper() {
if (this.popper) {
this.hidePopper();
} else {
this.showPopper();
}
},
showPopper() {
if (this.isDisabled) return;
this.$refs.panel?.$el.setAttribute('data-show', '');
this.popper = createPopper(this.$refs.reference, this.$refs.panel?.$el, {
placement: 'bottom-start',
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8]
}
}
]
});
this.$refs.panel.$el.style.zIndex = this.$Q?.zIndex ?? 2000;
},
hidePopper() {
if (this.popper) {
this.$refs.panel?.$el.removeAttribute('data-show');
this.popper.destroy();
this.popper = null;
}
},
handleInputFocus(e) {
this.$emit('focus', e);
this.focus = true;
},
updateStyle() {
const { $el, inputInitialHeight } = this;
if (!$el) return;
const inputInner = $el.querySelector('.q-input__inner');
if (!inputInner) return;
const tags = $el.querySelector('.q-cascader__tags');
if (tags) {
const { offsetHeight } = tags;
const height = `${Math.max(offsetHeight + 6, inputInitialHeight)}px`;
inputInner.style.height = height;
}
this.popper?.update();
}
}
};
</script>
================================================
FILE: src/qComponents/QCascader/src/QCascaderMenu.vue
================================================
<template>
<div class="q-cascader-menu">
<div :class="classes">
<q-scrollbar wrap-class="q-cascader-menu__scrollbar">
<div
v-if="isEmpty"
class="q-cascader-menu__empty-text"
>
{{ noDataText }}
</div>
<div
v-for="(node, key) in nodes"
v-else
:id="`${cascader.id}-node-${index}-${key}`"
:key="key"
:class="getNodeClass(node)"
role="menuitem"
tabindex="-1"
@click="e => expandMenuItem(e, node)"
@keyup.enter="e => handleEnterKeyUp(e, node, index, key)"
@keyup.right="e => expandMenuItem(e, node)"
>
<q-checkbox
v-if="cascader.multiple"
:ref="`QCheckbox${index}${key}`"
input-tab-index="-1"
:value="getChecked(node)"
:indeterminate="getIndeterminate(node)"
@change="isChecked => handleValueChange(node, isChecked)"
/>
<span class="q-cascader-node__label">{{ node.label }}</span>
<span
v-if="node.children"
:class="postfixClasses"
/>
</div>
</q-scrollbar>
</div>
</div>
</template>
<script>
const getCheckedChildren = (option, checkedValues) => {
const checkedArray = [];
const goToTheLast = childOption => {
if (childOption.children) {
childOption.children.forEach(goToTheLast);
} else {
checkedArray.push(checkedValues.includes(childOption.value));
}
};
goToTheLast(option);
return checkedArray;
};
export default {
name: 'QCascaderMenu',
componentName: 'QCascaderMenu',
inject: ['panel', 'cascader'],
props: {
nodes: {
type: Array,
required: true
},
index: { type: Number, default: 0 }
},
computed: {
postfixClasses() {
return {
'q-cascader-node__postfix': true,
'q-icon-lock': this.disabled,
'q-icon-triangle-right': !this.disabled
};
},
classes() {
const classes = ['q-cascader-menu__wrap'];
const currentNodesCount = this.panel.menus[this.index]?.length;
const nextNodesCount = this.panel.menus[this.index + 1]?.length;
const prevNodesCount = this.panel.menus[this.index - 1]?.length;
if (this.panel.menus?.length > 1) {
if (this.index === 0 || nextNodesCount >= currentNodesCount) {
classes.push('q-cascader-menu__wrap_no-right-borders');
}
if (this.index > 0 && prevNodesCount >= currentNodesCount) {
classes.push('q-cascader-menu__wrap_no-left-bottom-border');
}
if (prevNodesCount) {
classes.push('q-cascader-menu__wrap_no-left-top-border');
}
if (nextNodesCount) {
classes.push('q-cascader-menu__wrap_no-right-top-border');
}
if (currentNodesCount > nextNodesCount) {
classes.push('q-cascader-menu__wrap_with-right-borders');
}
}
return classes;
},
isEmpty() {
return !this.nodes.length;
},
noDataText() {
return this.panel?.noDataText ?? this.$t('QCascader.noDataText');
}
},
methods: {
handleEnterKeyUp(e, node, index, key) {
if (this.cascader.multiple) {
const QCheckboxInstance = this.$refs[`QCheckbox${index}${key}`]?.[0];
if (QCheckboxInstance) {
QCheckboxInstance.nativeClick();
}
return;
}
this.expandMenuItem(e, node);
},
getIndeterminate(node) {
if (
!this.cascader.checkedValues ||
this.cascader.checkedValues.includes(node.value) ||
this.cascader.checkStrictly
)
return false;
const checkedArray = getCheckedChildren(
node,
this.cascader.checkedValues
);
if (checkedArray.every(Boolean)) return false;
if (checkedArray.some(Boolean)) return true;
return false;
},
checkStrictly(value) {
const result = new Set(this.cascader.checkedValues);
if (result.has(value)) {
result.delete(value);
} else {
result.add(value);
}
this.cascader.emit(Array.from(result));
},
check(value, isChecked) {
const result = new Set(this.cascader.checkedValues);
if (result.has(value)) {
result.delete(value);
} else {
const findAndCheckChildren = ({ option = {}, checkMode = false }) => {
// try to find checked value and run check mode to all children if success
if (checkMode || option.value === value) {
if (option.children) {
// it's not the last level, go to next
option.children.forEach(childOption => {
findAndCheckChildren({ option: childOption, checkMode: true });
});
// it is the last level, check if value have already added
} else if (this.cascader.checkStrictly) {
if (result.has(option.value)) {
result.delete(option.value);
} else {
result.add(option.value);
}
// if no stricltly, check all or delete all
} else if (isChecked) {
result.add(option.value);
} else {
result.delete(option.value);
}
} else if (option.children) {
// just go to the next level
option.children.forEach(childOption => {
findAndCheckChildren({ option: childOption });
});
}
};
this.cascader.options.forEach(option => {
// go throw the children to the last level
findAndCheckChildren({ option });
});
}
this.cascader.emit(Array.from(result));
},
handleValueChange({ value }, isChecked) {
if (this.cascader.checkStrictly) {
this.checkStrictly(value);
} else {
this.check(value, isChecked);
}
},
getChecked(node) {
if (!this.cascader.checkedValues) return false;
if (this.cascader.checkStrictly) {
if (Array.isArray(this.cascader.checkedValues)) {
return Boolean(
this.cascader.checkedValues.find(value => value === node.value)
);
}
return false;
}
const checkedArray = getCheckedChildren(
node,
this.cascader.checkedValues
);
return checkedArray.every(Boolean);
},
getNodeClass(node) {
const isActive = this.panel.activePath.find(
value => value === node.label
);
return {
'q-cascader-node': true,
'q-cascader-node_active': isActive
};
},
expandMenuItem(e, value) {
if (e.key === 'ArrowRight' && !value.children) return;
if (!value.children && !this.cascader.multiple) {
this.cascader.emit(value.value);
}
this.$emit('expand', this.index, value);
}
}
};
</script>
================================================
FILE: src/qComponents/QCascader/src/QCascaderPanel.vue
================================================
<template>
<div class="q-cascader-panel">
<q-cascader-menu
v-for="(menu, index) in menus"
:key="index"
role="menu"
:aria-labelledby="cascader.id"
:index="index"
:nodes="menu"
@expand="handleExpand"
/>
</div>
</template>
<script>
import QCascaderMenu from './QCascaderMenu';
export default {
name: 'QCascaderPanel',
componentName: 'QCascaderPanel',
components: {
QCascaderMenu
},
props: {
/**
* see QCascader value
*/
value: { type: [Array, Object], default: null },
/**
* see QCascader options
*/
options: { type: Array, default: null },
/**
* noDataText
*/
noDataText: {
type: String,
default: null
}
},
inject: ['cascader'],
provide() {
return {
panel: this
};
},
data() {
return {
activePath: [],
menus: []
};
},
watch: {
options: {
immediate: true,
handler() {
this.menus = [this.options];
}
}
},
methods: {
navigateFocus(e) {
if (
['ArrowDown', 'ArrowUp'].includes(e.key) &&
e.target instanceof HTMLInputElement
) {
const firstNode = this.$el.querySelector(
`#${this.cascader.id}-node-0-0`
);
if (firstNode) {
firstNode.focus();
}
}
if (!e.target.classList.contains('q-cascader-node')) return;
const nodeText = e.target.innerText;
let nodeIndex;
let currentNodePosition;
this.menus.forEach((menu, menuIndex) => {
nodeIndex = menu.findIndex(node => node.label === nodeText);
if (nodeIndex > -1) {
currentNodePosition = [menuIndex, nodeIndex];
}
});
let nextNodePosition;
switch (e.key) {
case 'ArrowRight':
nextNodePosition = [
currentNodePosition[0] + 1,
currentNodePosition[1]
];
break;
case 'ArrowLeft':
nextNodePosition = [
currentNodePosition[0] === 0 ? 0 : currentNodePosition[0] - 1,
currentNodePosition[1]
];
break;
case 'ArrowUp':
nextNodePosition = [
currentNodePosition[0],
currentNodePosition[1] === 0 ? 0 : currentNodePosition[1] - 1
];
break;
case 'ArrowDown':
nextNodePosition = [
currentNodePosition[0],
currentNodePosition[1] + 1
];
break;
default:
break;
}
const node = this.$el.querySelector(
`#${this.cascader.id}-node-${nextNodePosition[0]}-${nextNodePosition[1]}`
);
if (node) {
node.focus();
}
},
handleExpand(index, activeValue) {
if (this.cascader.multiple && !activeValue.children) return;
let nextLevel;
// current level has already opened
if (this.menus[index]) {
if (this.activePath.length) {
this.activePath = this.activePath.slice(0, index);
}
this.activePath.push(activeValue.label);
this.menus = this.menus.slice(0, index + 1);
nextLevel = activeValue.children;
}
if (nextLevel) {
this.menus.push(nextLevel);
}
}
}
};
</script>
================================================
FILE: src/qComponents/QCascader/src/q-cascader-menu.scss
================================================
.q-cascader-menu {
min-width: 200px;
height: auto;
max-height: 304px;
margin-left: 1px;
&__wrap {
overflow: hidden;
color: var(--color-primary-blue);
background-color: var(--color-tertiary-gray-light);
border-radius: var(--border-radius-base);
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
-1px 1px 3px rgba(var(--color-rgb-blue), 0.4),
4px 4px 8px rgba(var(--color-rgb-blue), 0.4),
-4px -4px 12px var(--color-tertiary-white);
&_no-right-borders {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&_no-left-bottom-border {
border-bottom-left-radius: 0;
}
&_no-left-top-border {
border-top-left-radius: 0;
}
&_no-right-top-border {
border-top-right-radius: 0;
}
&_with-right-borders {
border-bottom-right-radius: var(--border-radius-base);
}
}
&__scrollbar {
max-height: 300px;
}
&__empty-text {
margin: 9px 0;
text-align: center;
color: var(--color-controls-black);
}
&:last-child {
border-right: none;
.q-cascader-node {
padding-right: 20px;
}
}
&__list {
min-height: 100%;
margin: 0;
padding: 0;
list-style: none;
}
&__hover-zone {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.q-cascader-node {
position: relative;
z-index: 1;
display: flex;
align-items: center;
height: 40px;
margin-top: 1px;
padding: 4px 8px;
background-color: var(--color-tertiary-gray-light);
outline: none;
box-shadow: var(--box-shadow-pressed);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
&:not(.q-cascader-node_disabled) {
cursor: pointer;
&:hover {
position: relative;
z-index: 1;
color: var(--color-primary-black);
background: var(--color-tertiary-gray);
}
}
&:focus,
&_active {
position: relative;
color: var(--color-primary-black);
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4);
.q-cascader-node__postfix::before {
color: var(--color-primary-blue);
}
}
&_active {
background-color: var(--color-tertiary-gray-ultra-light);
}
&:focus {
background-color: var(--color-tertiary-gray);
}
&_disabled {
color: var(--color-opacity-gray-dark);
cursor: not-allowed;
.q-cascader-node__postfix {
&::before {
font-size: 24px;
}
}
}
&__prefix {
display: none;
}
&__postfix {
position: absolute;
right: 10px;
&::before {
font-size: 22px;
color: rgba(var(--color-rgb-gray), 0.64);
}
}
&__label {
flex: 1;
padding: 0 25px 0 10px;
overflow: hidden;
text-align: left;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
================================================
FILE: src/qComponents/QCascader/src/q-cascader.scss
================================================
.q-cascader {
position: relative;
display: inline-block;
width: 100%;
font-size: var(--font-size-base);
&:not(.q-cascader_disabled):hover {
.q-input__inner {
cursor: pointer;
}
}
.q-input {
cursor: pointer;
&__inner {
text-overflow: ellipsis;
}
&__icon {
&::before {
font-size: 24px;
}
&:hover {
color: var(--color-primary-blue);
background-color: unset;
}
}
.q-icon-triangle-down {
transition: transform 0.3s;
&_reverse {
transform: rotateZ(180deg);
}
}
&_hover {
.q-input__inner {
background-color: var(--field-background-color-hover);
}
}
&_focus {
.q-input__inner {
background-color: var(--color-tertiary-gray-ultra-light);
box-shadow: var(--box-shadow-focus);
}
}
}
&__dropdown {
position: relative;
z-index: 100;
}
&__tags {
position: absolute;
top: 50%;
right: 30px;
left: 8px;
display: flex;
flex-wrap: wrap;
box-sizing: border-box;
line-height: normal;
text-align: left;
transform: translateY(-50%);
.q-tag {
max-width: calc(100% - 15px);
margin: 2px 0 2px 8px;
line-height: var(--line-height-base);
color: var(--color-opacity-gray-dark);
text-overflow: ellipsis;
background: var(--color-tertiary-gray-darker);
border: none;
border-radius: 2px;
.q-cascader__tag-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.q-icon-close {
position: relative;
top: 0;
flex: none;
border-radius: unset;
&:hover {
color: var(--color-primary-blue);
background-color: unset;
}
&::before {
font-size: 16px;
}
}
}
}
&__search-input {
flex: 1;
box-sizing: border-box;
min-width: 60px;
height: 24px;
margin: 2px 0 2px 15px;
padding: 0;
border: none;
outline: none;
}
}
.q-cascader-panel {
display: none;
&[data-show] {
display: flex;
border-radius: var(--border-radius-base);
}
}
@import './q-cascader-menu.scss';
================================================
FILE: src/qComponents/QCheckbox/index.js
================================================
import QCheckbox from './src/QCheckbox.vue';
/* istanbul ignore next */
QCheckbox.install = function(Vue) {
Vue.component(QCheckbox.name, QCheckbox);
};
export default QCheckbox;
================================================
FILE: src/qComponents/QCheckbox/src/QCheckbox.vue
================================================
<template>
<component
:is="rootTag"
class="q-checkbox"
:class="{
'q-checkbox_disabled': isDisabled,
'q-checkbox_checked': isChecked
}"
>
<span
class="q-checkbox__input"
:class="{
'q-checkbox__input_disabled': isDisabled,
'q-checkbox__input_checked': isChecked,
'q-checkbox__input_focus': focus
}"
:tabindex="indeterminate ? 0 : false"
:role="indeterminate ? 'checkbox' : false"
:aria-checked="indeterminate ? 'mixed' : false"
>
<span class="q-checkbox__inner">
<span
class="q-checkbox__inner-icon"
:class="{
'q-icon-minus': indeterminate,
'q-icon-check': isChecked
}"
/>
</span>
<input
ref="checkboxInput"
v-model="model"
class="q-checkbox__original"
type="checkbox"
:aria-hidden="indeterminate ? 'true' : 'false'"
:disabled="isDisabled"
:value="label"
:name="name"
:tabindex="inputTabIndex"
@change="handleChange"
@focus="focus = true"
@blur="focus = false"
/>
</span>
<span
v-if="$slots.default || label"
class="q-checkbox__label"
>
<slot>{{ label }}</slot>
</span>
</component>
</template>
<script>
import { isBoolean } from 'lodash-es';
import Emitter from '../../mixins/emitter';
export default {
name: 'QCheckbox',
componentName: 'QCheckbox',
mixins: [Emitter],
inject: {
qForm: {
default: null
},
qFormItem: {
default: null
}
},
props: {
/**
* Array for group, Boolean for single
*/
value: {
type: [Array, Boolean],
default: null
},
/**
* Checkbox label
*/
label: { type: String, default: '' },
/**
* wheteher Checkbox is indeterminate
*/
indeterminate: { type: Boolean, default: false },
/**
* wheteher Checkbox is disabled
*/
disabled: { type: Boolean, default: false },
/**
* wheteher Checkbox is checked
*/
checked: { type: Boolean, default: false },
/**
* as native name
*/
name: { type: String, default: '' },
rootTag: { type: String, default: 'label' },
/**
* as native tabIndex
*/
inputTabIndex: {
type: [Number, String],
default: null
}
},
data() {
return {
selfModel: false,
focus: false,
isLimitExceeded: false,
isGroup: false
};
},
computed: {
model: {
get() {
const result = this.value !== undefined ? this.value : this.selfModel;
return this.isGroup ? this.store : result;
},
set(val) {
if (this.isGroup) {
this.isLimitExceeded = false;
if (
val.length < this.checkboxGroup.min ||
val.length > this.checkboxGroup.max
) {
this.isLimitExceeded = true;
}
if (!this.isLimitExceeded)
this.dispatch('QCheckboxGroup', 'input', [val]);
} else {
this.$emit('input', val);
this.selfModel = val;
}
}
},
isChecked() {
if (isBoolean(this.model)) return this.model;
if (Array.isArray(this.model)) return this.model.includes(this.label);
return false;
},
store() {
return this.checkboxGroup ? this.checkboxGroup.value : this.value;
},
/* used to make the isDisabled judgment under max/min props */
isLimitDisabled() {
const { max, min } = this.checkboxGroup;
return (
(Boolean(max || min) && this.model.length >= max && !this.isChecked) ||
(this.model.length <= min && this.isChecked)
);
},
isDisabled() {
return this.isGroup
? this.checkboxGroup.disabled ||
this.disabled ||
(this.qForm?.disabled ?? false) ||
this.isLimitDisabled
: this.disabled || (this.qForm?.disabled ?? false);
}
},
watch: {
value() {
this.qFormItem?.validateField('change');
}
},
created() {
if (this.checked) this.addToStore();
},
mounted() {
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'QCheckboxGroup') {
parent = parent.$parent;
this.isGroup = false;
} else {
this.checkboxGroup = parent;
this.isGroup = true;
break;
}
}
},
methods: {
/**
* @public
*/
nativeClick() {
this.$refs.checkboxInput.click();
},
addToStore() {
if (Array.isArray(this.model) && this.model.includes(this.label)) {
this.model.push(this.label);
return;
}
this.model = false;
},
handleChange(event) {
if (this.isLimitExceeded) return;
const value = event.target.checked;
this.$emit('change', value, event);
if (this.isGroup) {
this.$nextTick(() => {
this.dispatch('QCheckboxGroup', 'change', [this.checkboxGroup.value]);
});
}
}
}
};
</script>
================================================
FILE: src/qComponents/QCheckbox/src/q-checkbox.scss
================================================
.q-checkbox {
--checkbox-color-base: var(--color-primary-black);
--checkbox-color-disabled: rgba(var(--color-rgb-gray), 0.64);
--checkbox-background-color-base: var(--color-tertiary-gray-ultra-light);
--checkbox-background-color-hover: var(--color-tertiary-gray);
--checkbox-background-color-focus: var(--color-tertiary-gray-ultra-light);
--checkbox-background-color-checked: var(--color-tertiary-gray-ultra-light);
--checkbox-background-color-disabled: var(--color-tertiary-gray);
--checkbox-mark-color-base: var(--color-primary-blue);
--checkbox-mark-color-disabled: rgba(var(--color-rgb-gray), 0.64);
--checkbox-box-shadow-base: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4),
inset -1px -1px 1px rgba(var(--color-rgb-white), 0.7),
inset 1px 1px 2px rgba(var(--color-rgb-blue), 0.2);
--checkbox-box-shadow-focus: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4),
4px 4px 10px rgba(var(--color-rgb-blue), 0.4),
-4px -4px 10px rgba(var(--color-rgb-white), 0.25),
inset -1px -1px 1px rgba(var(--color-rgb-white), 0.7),
inset 1px 1px 2px rgba(var(--color-rgb-blue), 0.2);
position: relative;
display: inline-flex;
font-weight: var(--font-weight-base);
font-size: var(--font-size-base);
line-height: 1;
vertical-align: middle;
color: var(--checkbox-color-base);
white-space: nowrap;
outline: none;
cursor: pointer;
user-select: none;
&__label {
margin-top: 3px;
padding-left: 16px;
font-size: var(--font-size-base);
line-height: 18px;
white-space: normal;
word-break: break-word;
.q-checkbox_disabled & {
color: var(--checkbox-color-disabled);
cursor: not-allowed;
}
}
&__input {
position: relative;
line-height: 0;
white-space: nowrap;
border: none;
outline: none;
cursor: pointer;
}
&__inner {
position: relative;
z-index: 1;
display: inline-block;
box-sizing: border-box;
width: 24px;
height: 24px;
overflow: hidden;
background-color: var(--checkbox-background-color-base);
border: none;
border-radius: var(--border-radius-base);
box-shadow: var(--checkbox-box-shadow-base);
&-icon {
position: absolute;
top: 4px;
left: 4px;
box-sizing: content-box;
width: 16px;
font-size: 16px;
text-align: center;
color: var(--checkbox-mark-color-base);
}
.q-checkbox_checked &,
.q-checkbox__input_indeterminate & {
background-color: var(--checkbox-background-color-checked);
&::after {
opacity: 1;
}
}
.q-checkbox__input_focus & {
background-color: var(--checkbox-background-color-focus);
box-shadow: var(--checkbox-box-shadow-focus);
}
&,
.q-checkbox__input_focus &,
.q-checkbox_checked &,
.q-checkbox__input_indeterminate & {
&:hover {
background-color: var(--checkbox-background-color-hover);
}
}
.q-checkbox_disabled & {
background-color: var(--checkbox-background-color-disabled);
cursor: not-allowed;
&-icon {
color: var(--checkbox-mark-color-disabled);
cursor: not-allowed;
}
}
}
&__original {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
margin: 0;
outline: none;
opacity: 0;
}
}
================================================
FILE: src/qComponents/QCheckboxGroup/index.js
================================================
import QCheckboxGroup from './src/QCheckboxGroup.vue';
/* istanbul ignore next */
QCheckboxGroup.install = function(Vue) {
Vue.component(QCheckboxGroup.name, QCheckboxGroup);
};
export default QCheckboxGroup;
================================================
FILE: src/qComponents/QCheckboxGroup/src/QCheckboxGroup.vue
================================================
<template>
<component
:is="tag"
:class="['q-checkbox-group', `q-checkbox-group_${direction}`]"
role="group"
aria-label="checkbox-group"
>
<slot />
</component>
</template>
<script>
import Emitter from '../../mixins/emitter';
export default {
name: 'QCheckboxGroup',
componentName: 'QCheckboxGroup',
mixins: [Emitter],
inject: {
qFormItem: {
default: null
}
},
props: {
value: {
type: Array,
default: () => []
},
/**
* disable all inner QCheckbox'es
*/
disabled: { type: Boolean, default: false },
/**
* minimum checked values
*/
min: { type: Number, default: 0 },
/**
* maximum checked values
*/
max: { type: Number, default: Infinity },
/**
* root tag
*/
tag: { type: String, default: 'div' },
/**
* vertical renders to column, horizontal to row
*/
direction: {
type: String,
default: 'vertical',
validator: value => ['vertical', 'horizontal'].includes(value)
}
},
watch: {
value() {
this.qFormItem?.validateField('change');
}
}
};
</script>
================================================
FILE: src/qComponents/QCheckboxGroup/src/q-checkbox-group.scss
================================================
.q-checkbox-group {
display: flex;
align-items: flex-start;
font-size: 0;
line-height: 1;
&_vertical {
flex-direction: column;
.q-checkbox:not(:last-child) {
margin-bottom: 16px;
}
}
&_horizontal {
.q-checkbox:not(:last-child) {
margin-right: 32px;
}
}
}
================================================
FILE: src/qComponents/QCol/QCol.test.js
================================================
import Component from './src/QCol';
describe('QCol', () => {
it('should match snapshot', async () => {
const { element } = shallowMount(Component);
expect(element).toMatchSnapshot();
});
});
================================================
FILE: src/qComponents/QCol/__snapshots__/QCol.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QCol should match snapshot 1`] = `
<div
class="q-col"
/>
`;
================================================
FILE: src/qComponents/QCol/index.js
================================================
import QCol from './src/QCol';
QCol.install = Vue => {
Vue.component(QCol.name, QCol);
};
export default QCol;
================================================
FILE: src/qComponents/QCol/src/QCol.vue
================================================
<template>
<component
:is="tag"
class="q-col"
:class="classes"
>
<slot />
</component>
</template>
<script>
export default {
name: 'QCol',
componentName: 'QCol',
props: {
/**
* custom element tag
*/
tag: {
type: String,
default: 'div'
},
/**
* number of column the grid cols.
* `auto || [1-12]`
*/
cols: {
type: [String, Number],
default: null,
validator: value => value === 'auto' || (value > 0 && value <= 12)
},
/**
* number of spacing on the left side of the grid.
* `[0-11]`
*/
offset: {
type: [String, Number],
default: null,
validator: value => value >= 0 && value <= 11
}
},
computed: {
classes() {
return {
[`q-col_size_${this.cols}`]: Boolean(this.cols),
[`q-col_offset_${this.offset}`]: Number.isInteger(
Number(this.offset ?? 'hide')
)
};
}
}
};
</script>
================================================
FILE: src/qComponents/QCol/src/q-col.scss
================================================
.q-col {
position: relative;
flex: 1 0 0;
width: 100%;
max-width: 100%;
padding-right: calc(var(--layout-gutter) / 2);
padding-left: calc(var(--layout-gutter) / 2);
&_size_auto {
flex: 0 0 auto;
width: auto;
max-width: 100%;
}
@for $i from 1 through 12 {
&_size_#{$i} {
flex: 0 0 percentage($i / 12);
max-width: percentage($i / 12);
}
}
@for $i from 0 through 11 {
&_offset_#{$i} {
margin-left: percentage($i / 12);
}
}
}
================================================
FILE: src/qComponents/QCollapse/QCollapse.test.js
================================================
import Component from './src/QCollapse';
describe('QCollapse', () => {
it('should match snapshot', () => {
const { element } = mount(Component);
expect(element).toMatchSnapshot();
});
it('data should match snapshot', () => {
expect(Component.data()).toMatchSnapshot();
});
});
================================================
FILE: src/qComponents/QCollapse/__snapshots__/QCollapse.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QCollapse data should match snapshot 1`] = `
Object {
"activeNames": Array [],
"uniqueId": [Function],
}
`;
exports[`QCollapse should match snapshot 1`] = `
<div
class="q-collapse"
/>
`;
================================================
FILE: src/qComponents/QCollapse/index.js
================================================
import QCollapse from './src/QCollapse.vue';
/* istanbul ignore next */
QCollapse.install = function(Vue) {
Vue.component(QCollapse.name, QCollapse);
};
export default QCollapse;
================================================
FILE: src/qComponents/QCollapse/src/QCollapse.vue
================================================
<template>
<div class="q-collapse">
<slot />
</div>
</template>
<script>
import { uniqueId } from 'lodash-es';
export default {
name: 'QCollapse',
componentName: 'QCollapse',
provide() {
return {
qCollapse: this
};
},
model: {
prop: 'value',
event: 'change'
},
props: {
/**
* shrink all panels, except the last selected
*/
accordion: {
type: Boolean,
default: false
},
value: {
type: [Array, String, Number],
default: () => []
}
},
data() {
return {
activeNames: [],
uniqueId
};
},
watch: {
value: {
immediate: true,
handler(value) {
this.activeNames = [].concat(value);
}
}
},
methods: {
updateValue(name) {
if (this.accordion) {
this.activeNames = this.activeNames === name ? [] : [name];
} else {
const index = this.activeNames.indexOf(name);
if (index > -1) {
this.activeNames.splice(index, 1);
} else {
this.activeNames.push(name);
}
}
this.$emit('change', this.activeNames);
}
}
};
</script>
================================================
FILE: src/qComponents/QCollapse/src/q-collapse.scss
================================================
================================================
FILE: src/qComponents/QCollapseItem/QCollapseItem.test.js
================================================
import Component from './src/QCollapseItem';
describe('QCollapseItem', () => {
it('should match snapshot', () => {
const { element } = mount(Component);
expect(element).toMatchSnapshot();
});
it('data should match snapshot', () => {
expect(Component.data()).toMatchSnapshot();
});
});
================================================
FILE: src/qComponents/QCollapseItem/__snapshots__/QCollapseItem.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QCollapseItem data should match snapshot 1`] = `
Object {
"activeNames": Array [],
}
`;
exports[`QCollapseItem should match snapshot 1`] = `
<div
class="q-collapse-item"
>
<button
class="q-collapse-item__header"
>
<div
class="q-collapse-item__title"
>
</div>
<div
class="q-collapse-item__icon q-icon-plus"
/>
</button>
<transition-stub>
<div
class="q-collapse-item__body"
style="display: none;"
>
<div
class="q-collapse-item__content"
/>
</div>
</transition-stub>
</div>
`;
================================================
FILE: src/qComponents/QCollapseItem/index.js
================================================
import QCollapseItem from './src/QCollapseItem.vue';
/* istanbul ignore next */
QCollapseItem.install = function(Vue) {
Vue.component(QCollapseItem.name, QCollapseItem);
};
export default QCollapseItem;
================================================
FILE: src/qComponents/QCollapseItem/src/QCollapseItem.vue
================================================
<template>
<div
class="q-collapse-item"
:class="{
'q-collapse-item_active': isActive
}"
>
<button
class="q-collapse-item__header"
@click="handleTabClick"
>
<slot name="title">
<div class="q-collapse-item__title">{{ title }}</div>
</slot>
<div
class="q-collapse-item__icon"
:class="icon"
/>
</button>
<q-collapse-transition>
<div
v-show="isActive"
class="q-collapse-item__body"
>
<div class="q-collapse-item__content">
<slot />
</div>
</div>
</q-collapse-transition>
</div>
</template>
<script>
import QCollapseTransition from '../../helpers/collapse-transition';
export default {
name: 'QCollapseItem',
componentName: 'QCollapseItem',
components: { QCollapseTransition },
inject: {
qCollapse: {
default: null
}
},
props: {
title: {
type: String,
default: ''
},
name: {
type: [String, Number],
default: null
}
},
data() {
return {
activeNames: []
};
},
computed: {
preparedName() {
return this.name ?? this.qCollapse?.uniqueId('default-collapse-name-');
},
isActive() {
return this.qCollapse?.activeNames?.includes(this.preparedName) ?? false;
},
icon() {
return this.isActive ? 'q-icon-minus' : 'q-icon-plus';
}
},
methods: {
handleTabClick() {
this.qCollapse?.updateValue(this.preparedName);
}
}
};
</script>
================================================
FILE: src/qComponents/QCollapseItem/src/q-collapse-item.scss
================================================
.q-collapse-item {
&:not(:last-child) {
margin-bottom: 16px;
}
&__header {
display: flex;
align-items: center;
width: 100%;
text-align: left;
background-color: var(--color-tertiary-gray-light);
border: none;
border-radius: var(--border-radius-base);
outline: none;
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4);
cursor: pointer;
transition: background-color 0.1s;
.q-collapse-item_active & {
background-color: var(--color-tertiary-gray-ultra-light);
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0;
}
&:focus,
&:hover {
background-color: var(--color-tertiary-gray);
}
}
&__title {
flex: 1;
padding: 12px 0 12px 24px;
font-weight: 600;
font-size: 16px;
line-height: 20px;
color: rgba(var(--color-rgb-gray), 0.64);
.q-collapse-item_active > .q-collapse-item__header:not(:hover) &,
.q-collapse-item_active > .q-collapse-item__header:not(:focus) & {
color: var(--color-primary-black);
}
}
&__icon {
flex: 0 64px;
padding: 20px 16px 20px 24px;
font-size: 24px;
color: var(--color-primary-blue);
}
&__body {
margin-top: 1px;
background-color: var(--color-tertiary-gray-light);
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4);
}
&__content {
padding: 24px;
}
}
================================================
FILE: src/qComponents/QColorPicker/QColorAlphaSlider.test.js
================================================
import Component from './src/QColorAlphaSlider';
describe('QColorAlphaSlider', () => {
it('should match snapshot', async () => {
const instance = shallowMount(Component, {
propsData: {
color: 'rgba(255,155,155,.5)',
alpha: 55
}
});
expect(instance.element).toMatchSnapshot();
});
it('data should match snapshot', () => {
expect(Component.data()).toMatchSnapshot();
});
});
================================================
FILE: src/qComponents/QColorPicker/QColorHueSlider.test.js
================================================
import Component from './src/QColorHueSlider';
describe('QColorHueSlider', () => {
it('should match snapshot', async () => {
const instance = shallowMount(Component, {
propsData: {
hue: 155
}
});
expect(instance.element).toMatchSnapshot();
});
it('data should match snapshot', () => {
expect(Component.data()).toMatchSnapshot();
});
});
================================================
FILE: src/qComponents/QColorPicker/QColorPicker.test.js
================================================
import Component from './src/QColorPicker';
describe('QColorPicker', () => {
it('should match snapshot', async () => {
const instance = shallowMount(Component);
expect(instance.element).toMatchSnapshot();
});
it('data should match snapshot', () => {
expect(Component.data()).toMatchSnapshot();
});
});
================================================
FILE: src/qComponents/QColorPicker/QColorSvpanel.test.js
================================================
import Component from './src/QColorSvpanel';
describe('QColorSvpanel', () => {
it('should match snapshot', async () => {
const instance = shallowMount(Component, {
propsData: {
hue: 100,
saturation: 50,
value: 25
}
});
expect(instance.element).toMatchSnapshot();
});
it('data should match snapshot', () => {
expect(Component.data()).toMatchSnapshot();
});
});
================================================
FILE: src/qComponents/QColorPicker/QPickerDropdown.test.js
================================================
import Component from './src/QPickerDropdown';
describe('QPickerDropdown', () => {
it('should match snapshot', async () => {
const instance = shallowMount(Component);
expect(instance.element).toMatchSnapshot();
});
it('data should match snapshot', () => {
expect(Component.data()).toMatchSnapshot();
});
});
================================================
FILE: src/qComponents/QColorPicker/__snapshots__/QColorAlphaSlider.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QColorAlphaSlider data should match snapshot 1`] = `
Object {
"thumbLeft": 0,
}
`;
exports[`QColorAlphaSlider should match snapshot 1`] = `
<div
class="q-color-alpha-slider"
>
<div
class="q-color-alpha-slider__bar"
/>
<div
class="q-color-alpha-slider__thumb"
style="left: 0px;"
/>
</div>
`;
================================================
FILE: src/qComponents/QColorPicker/__snapshots__/QColorHueSlider.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QColorHueSlider data should match snapshot 1`] = `
Object {
"thumbTop": 0,
}
`;
exports[`QColorHueSlider should match snapshot 1`] = `
<div
class="q-color-hue-slider"
>
<div
class="q-color-hue-slider__bar"
/>
<div
class="q-color-hue-slider__thumb"
style="top: 0px;"
/>
</div>
`;
================================================
FILE: src/qComponents/QColorPicker/__snapshots__/QColorPicker.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QColorPicker data should match snapshot 1`] = `
Object {
"isClickIgnored": false,
"isPickerShown": false,
"popperJS": null,
"zIndex": null,
}
`;
exports[`QColorPicker should match snapshot 1`] = `
<div
class="q-color-picker"
>
<div
class="q-color-picker-trigger"
>
<button
class="q-color-picker-trigger__default"
>
<div
class="q-color-picker-trigger__color"
/>
<span
class="q-color-picker-trigger__icon q-icon-triangle-down"
/>
</button>
</div>
<q-picker-dropdown-stub
colorformat="hex"
isclearbtnshown="true"
/>
</div>
`;
================================================
FILE: src/qComponents/QColorPicker/__snapshots__/QColorSvpanel.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QColorSvpanel data should match snapshot 1`] = `
Object {
"cursorLeft": 0,
"cursorTop": 0,
}
`;
exports[`QColorSvpanel should match snapshot 1`] = `
<div
class="q-color-svpanel"
style="background-color: rgb(0, 0, 0);"
>
<div
class="q-color-svpanel__cursor"
style="top: 0px; left: 0px;"
/>
</div>
`;
================================================
FILE: src/qComponents/QColorPicker/__snapshots__/QPickerDropdown.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QPickerDropdown data should match snapshot 1`] = `
Object {
"alpha": 100,
"elementToFocusAfterClosing": null,
"hue": 0,
"saturation": 100,
"tempColor": "",
"value": 100,
}
`;
exports[`QPickerDropdown should match snapshot 1`] = `
<transition-stub
name="fade"
>
<div
class="q-picker-dropdown"
style="display: none;"
tabindex="-1"
>
<div
class="q-picker-dropdown__base"
>
<q-color-svpanel-stub
hue="0"
saturation="0"
value="0"
/>
<q-color-hue-slider-stub
hue="0"
/>
</div>
<!---->
<div
class="q-picker-dropdown__footer"
>
<div
class="q-picker-dropdown__input"
>
<q-input-stub
autocomplete="off"
label=""
suffixicon=""
tabindex=""
type="text"
value="#000000"
/>
</div>
<!---->
<q-button-stub
icon=""
nativetype="button"
size="medium"
theme="primary"
type="default"
>
Применить
</q-button-stub>
</div>
</div>
</transition-stub>
`;
================================================
FILE: src/qComponents/QColorPicker/index.js
================================================
import QColorPicker from './src/QColorPicker';
QColorPicker.install = Vue => {
Vue.component(QColorPicker.name, QColorPicker);
};
export default QColorPicker;
================================================
FILE: src/qComponents/QColorPicker/src/QColorAlphaSlider.vue
================================================
<template>
<div class="q-color-alpha-slider">
<div
ref="bar"
class="q-color-alpha-slider__bar"
:style="barStyles"
@click="handleBarClick"
/>
<div
ref="thumb"
class="q-color-alpha-slider__thumb"
:style="thumbStyles"
/>
</div>
</template>
<script>
import draggable from './draggable';
export default {
name: 'QColorAlphaSlider',
componentName: 'QColorAlphaSlider',
props: {
color: {
type: String,
required: true
},
alpha: {
type: Number,
required: true
}
},
data() {
return {
thumbLeft: 0
};
},
computed: {
barStyles() {
return {
backgroundImage: `linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, ${this.color})`
};
},
thumbStyles() {
return {
left: `${this.thumbLeft}px`
};
}
},
watch: {
alpha() {
this.update();
}
},
mounted() {
const { bar, thumb } = this.$refs;
const dragConfig = {
drag: this.handleDrag,
end: this.handleDrag
};
draggable(bar, dragConfig);
draggable(thumb, dragConfig);
this.update();
},
methods: {
handleDrag(event) {
const rect = this.$el.getBoundingClientRect();
const { thumb } = this.$refs;
let left = event.clientX - rect.left;
left = Math.max(thumb.offsetWidth / 2, left);
left = Math.min(left, rect.width - thumb.offsetWidth / 2);
const alpha = Math.round(
((left - thumb.offsetWidth / 2) / (rect.width - thumb.offsetWidth)) *
100
);
this.$emit('update:alpha', alpha);
},
handleBarClick(event) {
if (event.target !== this.$refs.thumb) {
this.handleDrag(event);
}
},
getThumbLeft() {
const el = this.$el;
if (!el) return 0;
return Math.round(
(this.alpha * (el.offsetWidth - this.$refs.thumb.offsetWidth * 1.5)) /
100
);
},
update() {
this.thumbLeft = this.getThumbLeft();
}
}
};
</script>
================================================
FILE: src/qComponents/QColorPicker/src/QColorHueSlider.vue
================================================
<template>
<div class="q-color-hue-slider">
<div
ref="bar"
class="q-color-hue-slider__bar"
@click="handleBarClick"
/>
<div
ref="thumb"
class="q-color-hue-slider__thumb"
:style="thumbStyles"
/>
</div>
</template>
<script>
import draggable from './draggable';
export default {
name: 'QColorHueSlider',
componentName: 'QColorHueSlider',
props: {
hue: {
type: Number,
required: true
}
},
data() {
return {
thumbTop: 0
};
},
computed: {
thumbStyles() {
return {
top: `${this.thumbTop}px`
};
}
},
watch: {
hue() {
this.update();
}
},
mounted() {
const { bar, thumb } = this.$refs;
const dragConfig = {
drag: this.handleDrag,
end: this.handleDrag
};
draggable(bar, dragConfig);
draggable(thumb, dragConfig);
this.update();
},
methods: {
handleDrag(event) {
const rect = this.$el.getBoundingClientRect();
const { thumb } = this.$refs;
let top = event.clientY - rect.top;
top = Math.min(top, rect.height - thumb.offsetHeight / 2);
top = Math.max(thumb.offsetHeight / 2, top);
const hue = Math.round(
((top - thumb.offsetHeight / 2) / (rect.height - thumb.offsetHeight)) *
360
);
this.$emit('update:hue', hue);
},
handleBarClick(event) {
if (event.target !== this.$refs.thumb) {
this.handleDrag(event);
}
},
getThumbTop() {
const el = this.$el;
if (!el) return 0;
return Math.round(
(this.hue * (el.offsetHeight - this.$refs.thumb.offsetHeight * 1.5)) /
360
);
},
update() {
this.thumbTop = this.getThumbTop();
}
}
};
</script>
================================================
FILE: src/qComponents/QColorPicker/src/QColorPicker.vue
================================================
<template>
<div class="q-color-picker">
<div
ref="trigger"
class="q-color-picker-trigger"
:class="{
'q-color-picker-trigger_is-disabled': isDisabled,
'q-color-picker-trigger_is-opened': isPickerShown
}"
@click.prevent="handleTriggerClick"
>
<!-- @slot _Optional_. HTML element that triggers dropdown -->
<slot
v-if="$slots.trigger"
name="trigger"
/>
<button
v-else
:disabled="isDisabled"
class="q-color-picker-trigger__default"
>
<div
class="q-color-picker-trigger__color"
:style="{
backgroundColor: value
}"
/>
<span :class="iconClasses" />
</button>
</div>
<q-picker-dropdown
ref="dropdown"
:is-shown="isPickerShown"
:is-clear-btn-shown="clearable"
:color="value"
:color-format="colorFormat"
:alpha-shown="alphaShown"
:style="{ zIndex }"
@click.stop
@close="handleClose"
@clear="handleClear"
@pick="handlePick"
/>
</div>
</template>
<script>
import { createPopper } from '@popperjs/core';
import Color from 'color';
import Emitter from '../../mixins/emitter';
import PLACEMENTS from '../../constants/popperPlacements';
import QPickerDropdown from './QPickerDropdown';
export default {
name: 'QColorPicker',
componentName: 'QColorPicker',
components: {
QPickerDropdown
},
mixins: [Emitter],
model: {
prop: 'value',
event: 'change'
},
inject: {
qForm: {
default: null
},
qFormItem: {
default: null
}
},
props: {
/**
* binding value
*/
value: {
type: String,
default: null
},
/**
* whether to disable the ColorPicker
*/
disabled: {
type: Boolean,
default: false
},
/**
* whether to show clear button
*/
clearable: {
type: Boolean,
default: true
},
/**
* whether to display the alpha slider
*/
alphaShown: {
type: Boolean,
default: false
},
/**
* output color format
*/
colorFormat: {
type: String,
default: 'hex',
validator: value => ['hex', 'rgb'].includes(value)
},
placement: {
type: String,
default: 'right-start',
validator: value => PLACEMENTS.includes(value)
},
/**
* whether to append the popper menu to body. If the positioning of the popper is wrong, you can try to set this prop to false
*/
appendToBody: {
type: Boolean,
default: true
},
popperOptions: {
type: Object,
default: () => ({})
}
},
data() {
return {
zIndex: null,
isClickIgnored: false,
isPickerShown: false,
popperJS: null
};
},
computed: {
isDisabled() {
return this.disabled || (this.qForm?.disabled ?? false);
},
isColorDark() {
if (!this.value) return false;
return Color(this.value).isDark();
},
iconClasses() {
return {
'q-color-picker-trigger__icon': true,
'q-color-picker-trigger__icon_light': this.isColorDark,
'q-icon-triangle-down': !this.isDisabled,
'q-icon-lock': this.isDisabled
};
},
options() {
return {
placement: this.placement,
computeStyle: {
boundariesElement: 'body',
gpuAcceleration: false
},
modifiers: [
{
name: 'offset',
options: {
offset: [0, 16]
}
}
],
...this.popperOptions
};
}
},
watch: {
isPickerShown(value) {
if (this.isDisabled || !value) return;
this.zIndex = this.$Q?.zIndex ?? 2000;
this.createPopper();
}
},
beforeDestroy() {
const dropdown = this.$refs?.dropdown?.$el;
if (dropdown?.parentNode === document.body) {
document.body.removeChild(dropdown);
}
},
methods: {
handleClose() {
if (this.isClickIgnored) {
this.isClickIgnored = false;
return;
}
this.isPickerShown = false;
},
handleTriggerClick() {
if (this.isDisabled) return;
this.isClickIgnored = true;
this.isPickerShown = !this.isPickerShown;
},
handleClear() {
this.$emit('change', null);
if (this.value !== null) {
this.qFormItem?.validateField('change');
}
this.isPickerShown = false;
},
handlePick(value) {
this.$emit('change', value);
if (this.value !== value) {
this.qFormItem?.validateField('change');
}
this.isPickerShown = false;
},
createPopper() {
if (this.popperJS?.destroy) {
this.popperJS.destroy();
this.popperJS = null;
}
if (this.appendToBody) document.body.appendChild(this.$refs.dropdown.$el);
this.popperJS = createPopper(
this.$refs.trigger,
this.$refs.dropdown.$el,
this.options
);
}
}
};
</script>
================================================
FILE: src/qComponents/QColorPicker/src/QColorSvpanel.vue
================================================
<template>
<div
class="q-color-svpanel"
:style="rootStyles"
>
<div
class="q-color-svpanel__cursor"
:style="cursorStyles"
/>
</div>
</template>
<script>
import draggable from './draggable';
export default {
name: 'QColorSvpanel',
componentName: 'QColorSvpanel',
props: {
hue: {
type: Number,
required: true
},
saturation: {
type: Number,
required: true
},
value: {
type: Number,
required: true
}
},
data() {
return {
cursorTop: 0,
cursorLeft: 0
};
},
computed: {
rootStyles() {
return {
backgroundColor: `hsl(${this.hue}, 100%, 50%)`
};
},
cursorStyles() {
return {
top: `${this.cursorTop}px`,
left: `${this.cursorLeft}px`
};
}
},
watch: {
saturation() {
this.update();
},
value() {
this.update();
}
},
mounted() {
draggable(this.$el, {
drag: this.handleDrag,
end: this.handleDrag
});
this.update();
},
methods: {
update() {
const { clientWidth: width, clientHeight: height } = this.$el;
this.cursorLeft = (this.saturation * width) / 100;
this.cursorTop = ((100 - this.value) * height) / 100;
},
handleDrag(event) {
const rect = this.$el.getBoundingClientRect();
let left = event.clientX - rect.left;
let top = event.clientY - rect.top;
left = Math.min(Math.max(0, left), rect.width);
top = Math.min(Math.max(0, top), rect.height);
this.cursorLeft = left;
this.cursorTop = top;
this.$emit('update:saturation', (left / rect.width) * 100);
this.$emit('update:value', 100 - (top / rect.height) * 100);
}
}
};
</script>
================================================
FILE: src/qComponents/QColorPicker/src/QPickerDropdown.vue
================================================
<template>
<transition name="fade">
<div
v-show="isShown"
ref="dropdown"
v-click-outside="closeDropdown"
class="q-picker-dropdown"
tabindex="-1"
@keyup.esc="closeDropdown"
>
<div class="q-picker-dropdown__base">
<q-color-svpanel
ref="sv"
:hue="hue"
:saturation.sync="saturation"
:value.sync="value"
/>
<q-color-hue-slider
ref="hue"
:hue.sync="hue"
/>
</div>
<q-color-alpha-slider
v-if="alphaShown"
ref="alpha"
:color="rgbString"
:alpha.sync="alpha"
/>
<div class="q-picker-dropdown__footer">
<div class="q-picker-dropdown__input">
<q-input
v-model="tempColor"
:validate-event="false"
@keyup.native.enter="updateHSVA(tempColor)"
@blur="updateHSVA(tempColor)"
/>
</div>
<q-button
v-if="isClearBtnShown"
theme="link"
@click="handleClearBtnClick"
>
{{ $t('QColorPicker.clear') }}
</q-button>
<q-button @click="handleConfirmBtnClick">
{{ $t('QColorPicker.confirm') }}
</q-button>
</div>
</div>
</transition>
</template>
<script>
import Color from 'color';
import QButton from '../../QButton';
import QColorSvpanel from './QColorSvpanel';
import QColorAlphaSlider from './QColorAlphaSlider';
import QColorHueSlider from './QColorHueSlider';
export default {
name: 'QPickerDropdown',
componentName: 'QPickerDropdown',
components: {
QButton,
QColorSvpanel,
QColorHueSlider,
QColorAlphaSlider
},
props: {
isShown: {
type: Boolean,
default: false
},
isClearBtnShown: {
type: Boolean,
default: false
},
color: {
type: String,
default: null
},
colorFormat: {
type: String,
default: 'hex',
validator: value => ['hex', 'rgb'].includes(value)
},
alphaShown: {
type: Boolean,
default: false
}
},
data() {
return {
elementToFocusAfterClosing: null,
tempColor: '',
hue: 0,
saturation: 100,
value: 100,
alpha: 100
};
},
computed: {
colorModel() {
return Color({
h: this.hue,
s: this.saturation,
v: this.value
});
},
rgbString() {
return this.colorModel.rgb().string();
},
colorString() {
if (this.alphaShown || this.colorFormat === 'rgb') {
return this.colorModel
.alpha(this.alpha / 100)
.rgb()
.string();
}
return this.colorModel.hex();
}
},
watch: {
async isShown(value) {
if (!value) {
document.removeEventListener('focus', this.handleDocumentFocus, true);
await this.$nextTick();
this.elementToFocusAfterClosing?.focus();
return;
}
document.addEventListener('focus', this.handleDocumentFocus, true);
this.updateHSVA(this.color);
this.tempColor = this.colorString;
this.elementToFocusAfterClosing = document.activeElement;
await this.$nextTick();
this.$refs.dropdown.focus();
const { sv, hue, alpha } = this.$refs;
sv?.update();
hue?.update();
alpha?.update();
},
color: {
immediate: true,
handler(value) {
this.updateHSVA(value);
}
},
colorString: {
immediate: true,
handler(value) {
this.tempColor = value;
}
}
},
methods: {
closeDropdown() {
this.$emit('close');
},
handleDocumentFocus(event) {
if (!this.$refs.dropdown.contains(event.target)) {
this.$refs.dropdown.focus();
}
},
updateHSVA(value) {
try {
const { valpha, color } = Color(value).hsv();
this.hue = color[0];
this.saturation = color[1];
this.value = color[2];
this.alpha = valpha * 100;
} catch (error) {
// do nothing
}
},
handleClearBtnClick() {
this.$emit('clear');
},
handleConfirmBtnClick() {
this.$emit('pick', this.colorString);
}
}
};
</script>
================================================
FILE: src/qComponents/QColorPicker/src/draggable.js
================================================
let isDragging = false;
export default function(element, options) {
const moveFn = event => {
if (options.drag) options.drag(event);
};
const upFn = event => {
document.removeEventListener('mousemove', moveFn);
document.removeEventListener('mouseup', upFn);
document.onselectstart = null;
document.ondragstart = null;
isDragging = false;
if (options.end) options.end(event);
};
element.addEventListener('mousedown', event => {
if (isDragging) return;
document.onselectstart = () => false;
document.ondragstart = () => false;
document.addEventListener('mousemove', moveFn);
document.addEventListener('mouseup', upFn);
isDragging = true;
if (options.start) options.start(event);
});
}
================================================
FILE: src/qComponents/QColorPicker/src/q-color-alpha-slider.scss
================================================
.q-color-alpha-slider {
position: relative;
width: 464px;
height: 16px;
margin-top: 8px;
overflow: hidden;
background-image: linear-gradient(
45deg,
var(--color-tertiary-gray-ultra-dark) 25%,
transparent 25%
),
linear-gradient(
-45deg,
var(--color-tertiary-gray-ultra-dark) 25%,
transparent 25%
),
linear-gradient(
45deg,
transparent 75%,
var(--color-tertiary-gray-ultra-dark) 75%
),
linear-gradient(
-45deg,
transparent 75%,
var(--color-tertiary-gray-ultra-dark) 75%
);
background-position: 8px 16px, -8px 8px, 16px 8px, -16px 0;
background-size: 16px 16px;
border-radius: var(--border-radius-base);
box-shadow: inset 2px 2px 4px rgba(var(--color-rgb-gray), 0.32);
cursor: pointer;
&__bar {
width: 100%;
height: 100%;
}
&__thumb {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 8px;
height: 12px;
margin: 2px;
background-color: var(--color-tertiary-white);
border-radius: 2px;
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4);
}
}
================================================
FILE: src/qComponents/QColorPicker/src/q-color-hue-slider.scss
================================================
.q-color-hue-slider {
position: relative;
width: 16px;
height: 208px;
margin-left: 16px;
cursor: pointer;
&__bar {
width: 100%;
height: 100%;
background-image: linear-gradient(
180deg,
#f00 0,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00
);
border-radius: var(--border-radius-base);
box-shadow: inset 2px 2px 4px rgba(var(--color-rgb-gray), 0.32);
}
&__thumb {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 12px;
height: 8px;
margin: 2px;
background-color: var(--color-tertiary-white);
border-radius: 2px;
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4);
}
}
================================================
FILE: src/qComponents/QColorPicker/src/q-color-picker.scss
================================================
@import './q-picker-dropdown';
@import './q-color-svpanel';
@import './q-color-hue-slider';
@import './q-color-alpha-slider';
.q-color-picker {
&-trigger {
display: inline-block;
cursor: pointer;
&__default {
position: relative;
display: block;
width: 40px;
height: 40px;
padding: 0;
font-size: 24px;
border: none;
border-radius: var(--border-radius-base);
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4),
4px 4px 8px rgba(var(--color-rgb-blue), 0.4),
-4px -4px 12px var(--color-tertiary-white);
cursor: inherit;
&:hover {
box-shadow: -1px -1px 4px rgba(var(--color-rgb-white), 0.25),
1px 1px 4px rgba(var(--color-rgb-blue), 0.4),
4px 4px 8px rgba(var(--color-rgb-blue), 0.4),
-4px -4px 8px rgba(var(--color-rgb-white), 0.8);
}
&:focus {
outline: none;
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4);
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
height: 100%;
background-image: linear-gradient(
45deg,
var(--color-tertiary-gray-ultra-dark) 25%,
transparent 25%
),
linear-gradient(
-45deg,
var(--color-tertiary-gray-ultra-dark) 25%,
transparent 25%
),
linear-gradient(
45deg,
transparent 75%,
var(--color-tertiary-gray-ultra-dark) 75%
),
linear-gradient(
-45deg,
transparent 75%,
var(--color-tertiary-gray-ultra-dark) 75%
);
background-position: 8px 16px, -8px 8px, 16px 8px, -16px 0;
background-size: 16px 16px;
border-radius: var(--border-radius-base);
}
.q-color-picker-trigger_is-disabled & {
background-color: var(--color-tertiary-gray);
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4);
cursor: not-allowed;
&::before {
display: none;
}
}
}
&__color {
position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
height: 100%;
overflow: hidden;
background-color: var(--color-tertiary-gray-light);
border-radius: var(--border-radius-base);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity ease 0.25s;
}
.q-color-picker-trigger__default:hover & {
&::before {
background-color: rgba(var(--color-rgb-gray), 0.1);
opacity: 1;
}
.q-color-picker-trigger_is-disabled & {
&::before {
opacity: 0;
}
}
}
.q-color-picker-trigger__default:focus & {
&::before {
background-color: rgba(var(--color-rgb-white), 0.1);
opacity: 1;
}
}
}
&__icon {
position: relative;
z-index: 1;
line-height: 40px;
color: var(--color-primary-black);
transition: transform 0.25s ease, color 0.25s ease;
.q-color-picker-trigger_is-opened & {
transform: rotate(-180deg);
}
.q-color-picker-trigger_is-disabled & {
color: rgba(var(--color-rgb-gray), 0.64);
}
&_light {
color: var(--color-tertiary-white);
.q-color-picker-trigger_is-disabled & {
color: rgba(var(--color-rgb-white), 0.64);
}
}
}
}
}
================================================
FILE: src/qComponents/QColorPicker/src/q-color-svpanel.scss
================================================
.q-color-svpanel {
position: relative;
width: 464px;
height: 208px;
cursor: pointer;
&::before,
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&::before {
background-image: linear-gradient(
90deg,
var(--color-tertiary-white),
transparent
);
}
&::after {
background-image: linear-gradient(0, #000, transparent);
}
&__cursor {
position: absolute;
z-index: 1;
width: 8px;
height: 8px;
background-color: var(--color-tertiary-gray-lighter);
border-radius: 50%;
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4),
4px 4px 8px rgba(var(--color-rgb-blue), 0.4),
-4px -4px 12px var(--color-tertiary-white);
transform: translate(-4px, -4px);
cursor: grab;
}
}
================================================
FILE: src/qComponents/QColorPicker/src/q-picker-dropdown.scss
================================================
.q-picker-dropdown {
padding: 16px;
background-color: var(--color-tertiary-gray-ultra-light);
border-radius: var(--border-radius-base);
outline: none;
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4);
&__base {
display: flex;
}
&__footer {
display: flex;
margin-top: 16px;
}
&__input {
width: 223px;
margin-right: auto;
}
}
================================================
FILE: src/qComponents/QContextMenu/index.js
================================================
import QContextMenu from './src/QContextMenu.vue';
/* istanbul ignore next */
QContextMenu.install = function(Vue) {
Vue.component(QContextMenu.name, QContextMenu);
};
export default QContextMenu;
================================================
FILE: src/qComponents/QContextMenu/src/QContextMenu.vue
================================================
<template>
<div class="q-context-wrapper">
<div
ref="reference"
class="q-context-trigger"
@click="handleTriggerClick"
>
<slot v-if="$slots.default" />
<button
v-else
class="q-context-trigger__button q-icon-dots-3-horizontal"
/>
</div>
<div
v-show="isContextMenuShown"
ref="qContextMenu"
class="q-context-menu"
>
<button
v-for="(item, index) in menuItems"
:key="index"
tabindex="-1"
class="q-context-menu__item"
:class="{ 'q-context-menu__item_with-icon': item.icon }"
@click.prevent="handleItemClick(item.action)"
>
<span
v-if="item.icon"
class="q-context-menu__icon"
:class="item.icon"
/>
{{ item.name }}
</button>
</div>
</div>
</template>
<script>
import { createPopper } from '@popperjs/core';
export default {
name: 'QContextMenu',
componentName: 'QContextMenu',
props: {
position: {
type: String,
default: 'left',
validator: value => ['left', 'right'].includes(value)
},
menuItems: {
type: Array,
required: true
},
appendToBody: {
type: Boolean,
default: true
}
},
data() {
return {
isContextMenuShown: false,
menuItemElements: null
};
},
computed: {
placement() {
return this.position === 'right' ? 'bottom-start' : 'bottom-end';
}
},
beforeDestroy() {
document.removeEventListener('click', this.handleDocumentClick);
const { qContextMenu } = this.$refs;
if (qContextMenu?.parentNode === document.body)
document.body.removeChild(qContextMenu);
},
methods: {
handleDocumentClick({ target }) {
if (
this.$refs.reference?.contains(target) ||
this.$refs.qContextMenu.contains(target)
) {
return;
}
this.closePopper();
},
createPopper() {
document.addEventListener('click', this.handleDocumentClick);
document.addEventListener('keyup', this.handleKeyUp);
this.isContextMenuShown = true;
if (this.appendToBody) document.body.appendChild(this.$refs.qContextMenu);
const options = {
placement: this.placement,
computeStyle: {
boundariesElement: 'body',
gpuAcceleration: false
},
modifiers: [
{
name: 'offset',
options: {
offset: [0, -40]
}
}
]
};
this.popperJS = createPopper(
this.$refs.reference,
this.$refs.qContextMenu,
options
);
this.$refs.qContextMenu.style.zIndex = this.$Q?.zIndex ?? 2000;
this.menuItemElements = this.$refs.qContextMenu.querySelectorAll(
'.q-context-menu__item'
);
},
handleTriggerClick() {
if (this.isContextMenuShown) {
this.closePopper();
return;
}
this.createPopper();
},
handleItemClick(actionName) {
this.$emit('action', actionName, this.entity);
this.closePopper();
},
handleKeyUp(e) {
if (!this.isContextMenuShown) return;
if (e.key === 'Escape') this.closePopper();
if (!['ArrowUp', 'ArrowDown'].includes(e.key)) return;
if (document.activeElement.classList.contains('q-context-menu__item')) {
let currentNodeIndex;
let nextNodeIndex;
Array.from(this.menuItemElements).some((element, index) => {
const isItActiveElement = document.activeElement === element;
if (isItActiveElement) currentNodeIndex = index;
return isItActiveElement;
});
switch (e.key) {
case 'ArrowUp': {
nextNodeIndex = currentNodeIndex - 1;
break;
}
case 'ArrowDown': {
nextNodeIndex = currentNodeIndex + 1;
break;
}
default:
break;
}
this.menuItemElements[nextNodeIndex]?.focus();
} else {
this.menuItemElements[0]?.focus();
}
},
closePopper() {
document.removeEventListener('click', this.handleDocumentClick);
document.removeEventListener('keyup', this.handleKeyUp);
if (this.appendToBody) document.body.removeChild(this.$refs.qContextMenu);
this.isContextMenuShown = false;
this.menuItemElements = null;
}
}
};
</script>
================================================
FILE: src/qComponents/QContextMenu/src/q-context-menu.scss
================================================
.q-context {
&-menu {
min-width: 150px;
overflow: hidden;
background-color: var(--color-tertiary-gray-light);
border-radius: var(--border-radius-base);
box-shadow: var(--box-shadow-primary);
&__icon {
margin-right: 16px;
font-size: 24px;
}
&__item {
position: relative;
display: flex;
width: 100%;
padding: 12px 24px;
font-weight: var(--font-weight-base);
font-size: var(--font-size-base);
line-height: 24px;
color: var(--color-primary-blue);
text-decoration: none;
white-space: nowrap;
background-color: var(--color-tertiary-gray-light);
border: none;
cursor: pointer;
&_with-icon {
padding-right: 50px;
padding-left: 24px;
}
&:not(:first-child) {
margin-top: 1px;
}
&:hover,
&:focus {
position: relative;
z-index: 1;
color: var(--color-primary-black);
background-color: var(--color-tertiary-gray);
outline: none;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-shadow: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),
1px 1px 3px rgba(var(--color-rgb-blue), 0.4);
}
}
}
&-trigger {
display: inline-block;
cursor: pointer;
&__button {
display: block;
width: 40px;
height: 40px;
padding: 0;
font-size: 24px;
color: var(--color-primary-blue);
background-color: transparent;
border: none;
outline: none;
&:hover,
&[data-focus-visible-added] {
color: var(--color-primary-black);
background-color: rgba(var(--color-rgb-gray), 0.16);
border-radius: 100%;
}
}
}
}
================================================
FILE: src/qComponents/QDatePicker/index.js
================================================
import QDatePicker from './src/QDatePicker.vue';
/* istanbul ignore next */
QDatePicker.install = function install(Vue) {
Vue.component(QDatePicker.name, QDatePicker);
};
export default QDatePicker;
================================================
FILE: src/qComponents/QDatePicker/src/QDatePicker.vue
================================================
<template>
<div v-click-outside="handleClose">
<q-input
v-if="!isRanged"
ref="reference"
:class="['q-date-editor', { 'q-input_focus': pickerVisible }]"
:readonly="!editable"
:disabled="pickerDisabled"
:name="name"
:placeholder="placeholder || $t('QDatePicker.placeholder')"
:value="displayValue"
@focus="handleFocus"
@keyup.native="handleKeyUp"
@input="handleInput"
@change="handleChange"
@mouseenter.native="handleMouseEnter"
@mouseleave.native="showClose = false"
>
<span
slot="suffix"
class="q-input__icon"
:class="iconClass"
@click="handleIconClick"
/>
</q-input>
<div
v-else
ref="reference"
:class="rangeClasses"
tabindex="0"
@click="handleRangeClick"
@mouseenter="handleMouseEnter"
@mouseleave="showClose = false"
@keyup="handleKeyUp"
@focus="handleFocus"
>
<input
autocomplete="off"
class="q-range-input"
:placeholder="startPlaceholder || $t('QDatePicker.startPlaceholder')"
:value="displayValue && displayValue[0]"
:disabled="pickerDisabled"
:name="name && name[0]"
readonly
tabindex="-1"
/>
<slot name="range-separator">
<span class="q-range-separator">{{ rangeSeparator }}</span>
</slot>
<input
autocomplete="off"
:placeholder="endPlaceholder || $t('QDatePicker.endPlaceholder')"
:value="displayValue && displayValue[1]"
:disabled="pickerDisabled"
:name="name && name[1]"
class="q-range-input"
readonly
tabindex="-1"
/>
<span
:class="iconClass"
class="q-input__icon"
@click="handleIconClick"
/>
</div>
<component
:is="panelComponent"
ref="panel"
:class="{ 'q-picker-panel_shown': Boolean(popper) }"
:visible="Boolean(popper)"
:type="type"
:shortcuts="shortcuts"
:disabled-values="disabledValues"
:first-day-of-week="calcFirstDayOfWeek"
:value="transformedValue"
:show-time="timepicker"
@pick="handlePickClick"
>
<slot
v-if="$slots.sidebar"
:slot="$slots.sidebar"
name="sidebar"
/>
</component>
</div>
</template>
<script>
import { createPopper } from '@popperjs/core';
import { isNil, isString } from 'lodash-es';
import {
formatISO,
isDate,
isValid,
parse,
startOfMonth,
startOfWeek,
startOfYear,
parseISO
} from 'date-fns';
import { formatLocalDate } from './helpers';
import Emitter from '../../mixins/emitter';
import Pickers from '../../mixins/pickers';
import DatePanel from './panel/date';
import DateRangePanel from './panel/date-range';
import MonthRangePanel from './panel/month-range';
import YearRangePanel from './panel/year-range';
const validator = function(val) {
// either: String, Array of String, null / undefined
return (
val === null ||
val === undefined ||
isString(val) ||
(Array.isArray(val) && val.length === 2 && val.every(isString))
);
};
const dateValidator = function(val) {
return (
null ||
isDate(val) ||
isDate(parseISO(val)) ||
(Array.isArray(val) &&
val.length === 2 &&
(val.every(isDate) || val.every(isDate(parseISO))))
);
};
const convertDate = value => {
if (value && !isDate(value)) {
return parseISO(value);
}
return value;
};
export default {
name: 'QDatePicker',
componentName: 'QDatePicker',
mixins: [Emitter, Pickers],
inject: {
qForm: {
default: null
},
qFormItem: {
default: null
}
},
model: {
prop: 'value',
event: 'change'
},
props: {
/**
* one of sugested types
*/
type: {
type: String,
default: 'date',
validator: value =>
[
'date',
'datetime',
'week',
'month',
'year',
'daterange',
'datetimerange',
'monthrange',
'yearrange'
].includes(value)
},
/**
* any format from date-fns https://date-fns.org/v2.16.1/docs/format
*/
format: { type: String, default: 'dd MMMM yyyy' },
/**
* two options of returned value: 'date' - type Date format, 'iso' - ISO string format
*/
outputFormat: {
type: String,
default: 'date',
validator: val => ['date', 'iso'].includes(val)
},
placeholder: { type: String, default: null },
/**
* only for ranged types
*/
startPlaceholder: {
type: String,
default: null
},
/**
* only for ranged types
*/
endPlaceholder: { type: String, default: null },
/**
* start with monday by default
*/
firstDayOfWeek: {
default: null,
type: Number,
validator: val => val === null || (val >= 0 && val <= 6)
},
/**
* as native name for input
*/
name: {
default: '',
validator
},
/**
* whether QDatePicker is disabled
*/
disabled: { type: Boolean, default: false },
/**
* whether DatePicker is clearable
*/
clearable: {
type: Boolean,
default: true
},
/**
* whether DatePicker is editable, for type is `date` only
*/
editable: {
type: Boolean,
default: true
},
/**
* type Date, type String (ISO), array for ranges
*/
value: {
type: [String, Array, Date],
default: '',
validator: dateValidator
},
/**
* separator in the middle of the range
*/
rangeSeparator: {
type: String,
default: '-'
},
/**
* type of each Object is { text: 'example', onClick(picker) }
*/
shortcuts: { type: Array, default: () => [] },
/**
* any field is optional:
* `to` - disable all before this date,
* `from` - disable after this date,
* `ranges` - array of dateranges, each daterange is object and must has `start` and `end` field
*/
disabledValues: {
type: Object,
default: () => ({})
},
/**
* whether to trigger form validation
*/
validateEvent: {
type: Boolean,
default: true
},
/**
* whether to append the popper to body
*/
appendToBody: {
type: Boolean,
default: false
}
},
data() {
return {
pickerVisible: false,
showClose: false,
userInput: null,
unwatchPickerOptions: null,
popper: null
};
},
provide() {
return {
picker: this
};
},
computed: {
calcFirstDayOfWeek() {
if (isNil(this.firstDayOfWeek)) return this.$Q.locale === 'ru' ? 1 : 0;
return this.firstDayOfWeek;
},
transformedValue() {
if (Array.isArray(this.value) && this.value.length) {
return [convertDate(this.value[0]), convertDate(this.value[1])];
}
if (isString(this.value)) return convertDate(this.value);
return this.value;
},
rangeClasses() {
return {
'q-date-editor': true,
'q-range-editor': true,
'q-range-editor_disabled': this.pickerDisabled,
'q-range-editor_focused': this.pickerVisible
};
},
iconClass() {
if (this.disabled) return 'q-icon-lock';
const calendarIcon = this.timepicker
? 'q-icon-calendar-clock'
: 'q-icon-calendar';
return this.showClose ? 'q-icon-close' : calendarIcon;
},
panelComponent() {
switch (this.type) {
case 'daterange':
case 'datetimerange':
return DateRangePanel;
case 'monthrange':
return MonthRangePanel;
case 'yearrange':
return YearRangePanel;
default:
return DatePanel;
}
},
isRanged() {
return this.type.includes('range');
},
timepicker() {
return this.type.includes('time');
},
isValueEmpty() {
if (Array.isArray(this.transformedValue)) {
return !this.transformedValue.length;
}
return !this.transformedValue;
},
displayValue() {
let formattedValue = '';
if (Array.isArray(this.transformedValue)) {
formattedValue = this.transformedValue.map(dateFromArray =>
formatLocalDate(dateFromArray, this.format, this.$Q.locale)
);
} else if (
isDate(this.transformedValue) &&
isValid(this.transformedValue)
) {
formattedValue = formatLocalDate(
this.transformedValue,
this.format,
this.$Q.locale
);
}
if (Array.isArray(this.userInput)) {
return [
this.userInput[0] ?? formattedValue?.[0] ?? '',
this.userInput[1] ?? formattedValue?.[1] ?? ''
];
}
if (this.userInput !== null) {
return this.userInput;
}
if (formattedValue) {
return formattedValue;
}
return '';
},
pickerDisabled() {
return this.disabled || this.qForm?.disabled;
}
},
watch: {
pickerVisible(val) {
if (this.pickerDisabled) return;
if (val) {
this.createPopper();
} else {
this.destroyPopper();
this.userInput = null;
if (this.validateEvent) {
this.qFormItem?.validateField('blur');
}
}
},
value() {
if (!this.pickerVisible && this.validateEvent) {
this.qFormItem?.validateField('change');
}
}
},
beforeDestroy() {
this.destroyPopper();
},
methods: {
handlePickClick(val, { hidePicker = true } = {}) {
this.pickerVisible = !hidePicker;
this.emitChange(val);
},
createPopper() {
if (this.appendToBody) {
document.body.appendChild(this.$refs.panel.$el);
}
const reference =
this.$refs.reference instanceof Element
? this.$refs.reference
: this.$refs.reference.$el;
const panel = this.$refs.panel.$el;
this.popper = createPopper(reference, panel, {
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8]
}
},
{
name: 'flip',
options: {
fallbackPlacements: ['top', 'bottom']
}
}
]
});
panel.style.zIndex = this.$Q?.zIndex ?? 2000;
document.addEventListener('keyup', this.handleKeyUp, true);
},
destroyPopper() {
if (this.popper) {
this.popper.destroy();
this.popper = null;
}
const dropdown = this.$refs?.panel?.$el;
if (dropdown?.parentNode === document.body) {
document.body.removeChild(dropdown);
}
document.removeEventListener('keyup', this.handleKeyUp, true);
},
focus() {
if (!this.isRanged) {
this.$refs.reference.focus();
} else {
this.handleFocus();
}
},
formatToISO(date) {
if (Array.isArray(date)) {
return [formatISO(date[0]), formatISO(date[1])];
}
return formatISO(date);
},
handleMouseEnter() {
if (this.pickerDisabled) return;
if (!this.isValueEmpty && this.clearable) {
this.showClose = true;
}
},
handleKeyUp(e) {
// if user is typing, do not let picker handle key input
if (this.userInput) {
e.stopPropagation();
}
switch (e.key) {
case 'ArrowRight':
case 'ArrowUp':
case 'ArrowLeft':
case 'ArrowDown': {
this.$refs.panel.navigateDropdown(e);
break;
}
case 'Escape': {
this.pickerVisible = false;
e.stopPropagation();
break;
}
case 'Tab': {
if (this.$el.contains(document.activeElement)) {
this.$refs.panel.navigateDropdown(e);
return;
}
if (!this.isRanged) {
this.handleChange();
}
this.pickerVisible = false;
e.stopPropagation();
break;
}
default:
break;
}
},
handleChange() {
let value;
let format;
const date = this.userInput;
if (date) {
format = date.length === 10 ? 'dd.MM.yyyy' : 'dd.MM.yy';
if (this.timepicker && date.length > 10) {
format = "dd.MM.yyyy', 'HH:mm:ss";
}
value = parse(date, format, new Date());
if (!Number.isNaN(Number(value))) {
let resultValue = value;
switch (this.type) {
case 'week':
resultValue = startOfWeek(value, { weekStartsOn: 1 });
break;
case 'month':
resultValue = startOfMonth(value);
break;
case 'year':
resultValue = startOfYear(value);
break;
default:
resultValue = value;
break;
}
this.emitChange(resultValue);
}
} else {
this.emitChange(null);
}
this.userInput = null;
},
handleIconClick(event) {
if (this.pickerDisabled) return;
if (this.showClose) {
event.stopPropagation();
this.emitChange(null);
this.userInput = null;
this.showClose = false;
} else {
this.pickerVisible = !this.pickerVisible;
}
},
handleClose(e) {
if (!this.pickerVisible) return;
if (this.appendToBody) {
const path = e.path || (e.composedPath && e.composedPath());
const isClickToPanel = path.find(
element => element === this.$refs.panel.$el
);
if (!isClickToPanel) {
this.pickerVisible = false;
}
} else {
this.pickerVisible = false;
}
},
handleFocus() {
this.pickerVisible = true;
this.$emit('focus', this);
if (
!isDate(this.transformedValue) ||
Array.isArray(this.transformedValue)
)
return;
const format = this.timepicker ? 'dd.MM.yyyy, HH:mm:ss' : 'dd.MM.yy';
this.userInput = formatLocalDate(
this.transformedValue,
format,
this.$Q.locale
);
},
handleRangeClick() {
this.pickerVisible = true;
this.$emit('focus', this);
},
emitChange(val) {
let formatted = val;
if (this.outputFormat === 'iso' && val) {
formatted = this.formatToISO(val);
}
if (formatted !== this.value) {
this.$emit('change', formatted);
if (this.validateEvent) {
this.qFormItem?.validateField('change');
}
}
}
}
};
</script>
================================================
FILE: src/qComponents/QDatePicker/src/basic/date-table.vue
================================================
<template>
<table
cellspacing="10"
cellpadding="2"
class="q-date-table"
@mousemove="handleMouseMove"
>
<thead>
<tr>
<th
v-for="day in days"
:key="day"
class="q-date-table_days"
>
{{ day }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, key) in rows"
:key="key"
class="q-date-table__row"
>
<td
v-for="(cell, index) in row"
:key="index"
class="q-date-table__cell-wrapper"
>
<button
:class="getCellClasses(cell)"
type="button"
tabindex="-1"
@click="handleClick(cell)"
>
{{ cell.text }}
</button>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import {
getDaysInMonth,
startOfMonth,
endOfMonth,
isSameDay,
isBefore,
isAfter,
isWithinInterval,
startOfWeek,
endOfWeek,
isToday,
isDate
} from 'date-fns';
import { ru, enGB as en } from 'date-fns/locale';
const locales = { ru, en };
const checkDisabled = ({ date }, disabledValues) => {
if (!disabledValues) return false;
const disabled = [];
if (Array.isArray(disabledValues.ranges)) {
disabledValues.ranges.forEach(({ start, end }) => {
disabled.push(
isWithinInterval(date, {
start,
end
})
);
});
}
if (isDate(disabledValues.to) && disabledValues.to) {
disabled.push(isBefore(date, disabledValues.to));
}
if (isDate(disabledValues.from) && disabledValues.from) {
disabled.push(isAfter(date, disabledValues.from));
}
return disabled.some(Boolean);
};
export default {
props: {
firstDayOfWeek: {
default: 1,
type: Number
},
disabledValues: {
type: Object,
default: null
},
year: {
type: [String, Number],
default: new Date().getFullYear()
},
month: {
type: [String, Number],
default: new Date().getMonth()
},
value: { type: [Array, String, Date], default: null },
selectionMode: {
type: String,
default: 'date'
},
minDate: { type: [Date, String], default: null },
maxDate: { type: [Date, String], default: null },
rangeState: {
type: Object,
default() {
return {
endDate: null,
selecting: false
};
}
}
},
data() {
return {
lastRow: null,
lastColumn: null,
tableRows: [[], [], [], [], [], []]
};
},
computed: {
offsetDay() {
const week = this.firstDayOfWeek;
return week > 3 ? 7 - week : -week;
},
days() {
const DAYS_OF_WEEK = [...Array(7).keys()].map(i => {
return locales[this.$Q.locale].localize.day(i, { width: 'short' });
});
const day = this.firstDayOfWeek;
return [...DAYS_OF_WEEK, ...DAYS_OF_WEEK].slice(day, day + 7);
},
startMonthDate() {
return startOfMonth(new Date(this.year, this.month, 1));
},
endMonthDate() {
return endOfMonth(new Date(this.year, this.month, 1));
},
rows() {
const date = new Date(this.year, this.month, 1);
const firstDay = this.startMonthDate.getDay();
const dateCountOfMonth = getDaysInMonth(date);
const dateCountOfLastMonth = getDaysInMonth(
new Date(this.year, this.month === 0 ? 11 : this.month - 1)
);
const offset = this.offsetDay;
let count = 1;
return this.tableRows.map((row, i) => {
const newRow = [];
for (let j = 0; j < 7; j += 1) {
const cell = {
row: i,
column: j,
type: 'normal',
inRange: false,
start: false,
end: false
};
if (i === 0 || i === 1) {
const numberOfDaysFromPreviousMonth =
firstDay + offset < 0 ? 7 + firstDay + offset : firstDay + offset;
if (j + i * 7 >= numberOfDaysFromPreviousMonth) {
cell.text = count;
count += 1;
cell.date = new Date(this.year, this.month, cell.text);
} else {
cell.text =
dateCountOfLastMonth -
(numberOfDaysFromPreviousMonth - (j % 7)) +
1 +
i * 7;
cell.type = 'prev-month';
cell.date = new Date(this.year, this.month - 1, cell.text);
}
} else if (count <= dateCountOfMonth) {
cell.text = count;
count += 1;
cell.date = new Date(this.year, this.month, cell.text);
} else {
cell.text = count - dateCountOfMonth;
count += 1;
cell.type = 'next-month';
cell.date = new Date(this.year, this.month + 1, cell.text);
}
let maxDate = this.maxDate;
let minDate = this.minDate;
if (this.rangeState.selecting) {
maxDate = this.rangeState.endDate;
}
minDate = minDate?.getTime() ?? null;
maxDate = maxDate?.getTime() ?? minDate;
[minDate, maxDate] = [
Math.min(minDate, maxDate),
Math.max(minDate, maxDate)
];
cell.inRange =
minDate &&
cell.date.getTime() >= minDate &&
cell.date.getTime() <= maxDate;
if (isToday(cell.date)) {
if (!['prev-month', 'next-month'].includes(cell.type)) {
cell.type = 'today';
}
}
cell.disabled = checkDisabled(cell, this.disabledValues);
newRow.push(cell);
}
return newRow;
});
}
},
methods: {
getCellClasses(cell) {
const classes = ['cell', 'cell_date'];
if (['today', 'prev-month', 'next-month'].includes(cell.type)) {
classes.push(`cell_${cell.type}`);
}
if (
['normal', 'today'].includes(cell.type) &&
this.value &&
isSameDay(cell.date, this.value)
) {
classes.push('cell_current');
}
if (
cell.inRange ||
(this.minDate && isSameDay(cell.date, this.minDate)) ||
(this.maxDate && isSameDay(cell.date, this.maxDate)) ||
(this.selectionMode === 'week' &&
this.value &&
isWithinInterval(cell.date, {
start: startOfWeek(this.value, { weekStartsOn: 1 }),
end: endOfWeek(this.value, { weekStartsOn: 1 })
}))
) {
classes.push('cell_in-range');
}
if (cell.disabled) {
classes.push('cell_disabled');
}
return classes;
},
handleMouseMove(event) {
if (!this.rangeState.selecting) return;
let target = event.target;
if (target.tagName === 'BUTTON') {
target = target.parentNode;
}
if (target.tagName !== 'TD') return;
const row = target.parentNode.rowIndex - 1;
const column = target.cellIndex;
// can not select disabled date
if (this.rows[row]?.[column].disabled) return;
// only update rangeState when mouse moves to a new cell
// this avoids frequent Date object creation and improves performance
if (row !== this.lastRow || column !== this.lastColumn) {
this.lastRow = row;
this.lastColumn = column;
this.$emit('changerange', {
minDate: this.minDate,
maxDate: this.maxDate,
rangeState: {
selecting: true,
endDate: this.rows[row][column].date
}
});
}
},
handleClick(cell) {
if (cell.disabled || cell.type === 'week') return;
const newDate = cell.date;
if (this.selectionMode === 'range') {
if (!this.rangeState.selecting) {
this.$emit('pick', { minDate: newDate, maxDate: null });
this.rangeState.selecting = true;
} else {
if (newDate >= this.minDate) {
this.$emit('pick', { minDate: this.minDate, maxDate: newDate });
} else {
this.$emit('pick', { minDate: newDate, maxDate: this.minDate });
}
this.rangeState.selecting = false;
}
} else if (this.selectionMode === 'day') {
this.$emit('pick', newDate);
} else if (this.selectionMode === 'datetime') {
this.$emit('pick', newDate, { hidePicker: false });
} else if (this.selectionMode === 'week') {
const value = startOfWeek(newDate, { weekStartsOn: 1 });
this.$emit('pick', value);
} else if (this.selectionMode === 'dates') {
const value = this.value || [];
const newValue = [...value, newDate];
this.$emit('pick', newValue);
} else {
this.$emit('pick', newDate);
}
}
}
};
</script>
================================================
FILE: src/qComponents/QDatePicker/src/basic/month-table.vue
================================================
<template>
<table
cellspacing="4"
cellpadding="5"
class="q-month-table"
@mousemove="handleMouseMove"
>
<tr
v-for="(row, index) in rows"
:key="index"
>
<td
v-for="(cell, key) in row"
:key="key"
class="q-month-table__cell-wrapper"
>
<button
:class="getCellClasses(cell)"
:disabled="cell.disabled"
type="button"
tabindex="-1"
@click="handleMonthTableClick(cell)"
>
{{ getMonthName(cell.text) }}
</button>
</td>
</tr>
</table>
</template>
<script>
import { startOfMonth, isSameMonth, isBefore, isAfter } from 'date-fns';
import { formatLocalDate } from '../helpers';
const checkDisabled = ({ date }, disabledValues) => {
if (!disabledValues) return false;
const disabled = [];
if (Array.isArray(disabledValues.ranges)) {
disabledValues.ranges.forEach(range => {
disabled.push(
(isSameMonth(date, range.start) || isBefore(range.start, date)) &&
(isAfter(range.end, date) || isSameMonth(range.end, date))
);
});
}
if (disabledValues.to) {
disabled.push(isBefore(date, disabledValues.to));
}
if (disabledValues.from) {
disabled.push(isAfter(date, disabledValues.from));
}
return disabled.some(Boolean);
};
export default {
props: {
disabledValues: {
type: Object,
default: null
},
value: { type: [Date, String], default: null },
selectionMode: {
type: String,
default: 'month'
},
minDate: { type: [Date, String], default: null },
maxDate: { type: [Date, String], default: null },
year: {
type: [String, Number],
default: new Date().getFullYear()
},
month: {
type: [String, Number],
default: new Date().getMonth()
},
rangeState: {
type: Object,
default() {
return {
endDate: null,
selecting: false
};
}
}
},
data() {
return {
tableRows: [[], [], []],
lastRow: null,
lastColumn: null
};
},
computed: {
rows() {
// TODO: refactory rows / getCellClasses
const rows = this.tableRows;
return rows.map((row, i) => {
const newRow = [];
for (let j = 0; j < 4; j += 1) {
const cell = {
row: i,
column: j,
type: 'normal',
inRange: false,
start: false,
end: false
};
cell.type = 'normal';
const index = i * 4 + j;
cell.text = index;
cell.month = startOfMonth(new Date(this.year, index));
cell.disabled = checkDisabled(
{ date: cell.month },
this.disabledValues
);
let maxDate = this.maxDate;
let minDate = this.minDate;
if (this.rangeState.selecting) {
maxDate = this.rangeState.endDate;
}
minDate = startOfMonth(minDate);
maxDate = maxDate ? startOfMonth(maxDate) : minDate;
[minDate, maxDate] = [
Math.min(minDate, maxDate),
Math.max(minDate, maxDate)
];
cell.inRange =
minDate &&
cell.month.getTime() >= minDate &&
cell.month.getTime() <= maxDate;
if (isSameMonth(cell.month, new Date())) {
cell.type = 'today';
}
newRow.push(cell);
}
return newRow;
});
}
},
methods: {
getMonthName(monthIndex) {
return formatLocalDate(
new Date(this.year, monthIndex, 1),
'MMM',
this.$Q.locale
);
},
getCellClasses(cell) {
const classes = ['cell', 'cell_month'];
if (this.value && isSameMonth(this.value, cell.month))
classes.push('cell_current');
if (cell.type === 'today') classes.push('cell_today');
if (cell.inRange) {
classes.push('cell_in-range');
}
return classes;
},
handleMouseMove(event) {
if (!this.rangeState.selecting) return;
let target = event.target;
if (target.tagName === 'BUTTON') {
target = target.parentNode;
}
if (target.tagName !== 'TD') return;
const row = target.parentNode.rowIndex;
const column = target.cellIndex;
// can not select disabled date
if (this.rows[row][column].disabled) return;
// only update rangeState when mouse moves to a new cell
// this avoids frequent Date object creation and improves performance
if (row !== this.lastRow || column !== this.lastColumn) {
this.lastRow = row;
this.lastColumn = column;
this.$emit('changerange', {
minDate: this.minDate,
maxDate: this.maxDate,
rangeState: {
selecting: true,
endDate: new Date(this.year, row * 4 + column, 1)
}
});
}
},
handleMonthTableClick(cell) {
if (cell.disabled) return;
const month = cell.month.getMonth();
const newDate = cell.month;
if (this.selectionMode === 'range') {
if (!this.rangeState.selecting) {
this.$emit('pick', {
minDate: newDate,
maxDate: null,
rangeState: { ...this.rangeState, selecting: true }
});
} else if (newDate >= this.minDate) {
this.$emit('pick', {
minDate: this.minDate,
maxDate: newDate,
rangeState: { ...this.rangeState, selecting: false }
});
} else {
this.$emit('pick', {
minDate: newDate,
maxDate: this.minDate,
rangeState: { ...this.rangeState, selecting: false }
});
}
} else {
this.$emit('pick', month, this.year);
}
}
}
};
</script>
================================================
FILE: src/qComponents/QDatePicker/src/basic/year-table.vue
================================================
<template>
<table
cellspacing="4"
cellpadding="5"
class="q-year-table"
>
<tr
v-for="(row, key) in rows"
:key="key"
>
<td
v-for="(cell, index) in row"
:key="index"
class="q-year-table__cell-wrapper"
@mousemove="event => handleMouseMove(event, cell)"
@click="event => handleYearTableClick(event, cell)"
>
<button
:class="getCellClasses(cell)"
:disabled="cell.disabled"
type="button"
tabindex="-1"
>
{{ cell.year.getFullYear() }}
</button>
</td>
</tr>
</table>
</template>
<script>
import {
isSameYear,
addYears,
isDate,
startOfMonth,
startOfDecade,
isBefore,
isAfter
} from 'date-fns';
const checkDisabled = (year, disabledValues) => {
if (!disabledValues) return false;
const disabled = [];
if (Array.isArray(disabledValues.ranges)) {
disabledValues.ranges.forEach(range => {
disabled.push(
(isSameYear(year, range.start) || isBefore(range.start, year)) &&
(isAfter(range.end, year) || isSameYear(range.end, year))
);
});
}
if (disabledValues.to) {
disabled.push(isBefore(year, disabledValues.to));
}
if (disabledValues.from) {
disabled.push(isAfter(year, disabledValues.from));
}
return disabled.some(Boolean);
};
export default {
props: {
disabledValues: {
type: Object,
default: null
},
value: { type: [Date, String], default: null },
year: { type: [String, Number], default: '' },
selectionMode: {
type: String,
default: 'year'
},
minDate: { type: [Date, String], default: null },
maxDate: { type: [Date, String], default: null },
rangeState: {
type: Object,
default() {
return {
endDate: null,
selecting: false
};
}
}
},
data() {
return {
tableRows: [[], [], []],
lastHoveredCell: null
};
},
computed: {
startYear() {
if (this.year) {
return startOfDecade(new Date(this.year, 0, 1));
}
return startOfDecade(new Date());
},
rows() {
let startYear = this.startYear;
return this.tableRows.map((row, i) => {
const newRow = [];
const cellCount = i < 2 ? 4 : 2;
for (let j = 0; j < cellCount; j += 1) {
let maxDate = this.maxDate;
let minDate = this.minDate;
if (this.rangeState.selecting) {
maxDate = this.rangeState.endDate;
}
minDate = startOfMonth(minDate);
maxDate = maxDate ? startOfMonth(maxDate) : minDate;
[minDate, maxDate] = [
Math.min(minDate, maxDate),
Math.max(minDate, maxDate)
];
newRow.push({
year: startYear,
disabled: checkDisabled(startYear, this.disabledValues),
inRange:
minDate &&
startYear.getTime() >= minDate &&
startYear.getTime() <= maxDate
});
startYear = addYears(startYear, 1);
}
return newRow;
});
}
},
methods: {
handleMouseMove(event, { year }) {
// update data only different cell's hover
if (event.target === this.lastHoveredCell) return;
this.lastHoveredCell = event.target;
if (!this.rangeState.selecting) return;
this.$emit('changerange', {
minDate: this.minDate,
maxDate: this.maxDate,
rangeState: {
selecting: true,
endDate: year
}
});
},
getCellClasses(cell) {
const style = ['cell', 'cell_year'];
if (this.selectionMode === 'year') {
if (
isDate(this.value) &&
this.value.getFullYear() === cell.year.getFullYear()
)
style.push('cell_current');
if (new Date().getFullYear() === cell.year.getFullYear())
style.push('cell_today');
} else {
if (cell.inRange) style.push('cell_in-range');
if (isSameYear(cell.year, new Date())) {
style.push('cell_today');
}
}
return style;
},
handleYearTableClick(event, { year }) {
if (this.selectionMode === 'range') {
if (!this.rangeState.selecting) {
this.$emit('pick', {
minDate: year,
maxDate: null,
rangeState: { selecting: true, end: null }
});
} else if (year.getTime() >= this.minDate.getTime()) {
this.$emit('pick', {
minDate: this.minDate,
maxDate: year,
rangeState: { selecting: false, end: null }
});
} else {
this.$emit('pick', {
minDate: year,
maxDate: this.minDate,
rangeState: { selecting: false, end: null }
});
}
} else {
this.$emit('pick', year);
}
}
}
};
</script>
================================================
FILE: src/qComponents/QDatePicker/src/helpers.js
================================================
import { format, isDate, parseISO } from 'date-fns';
import { ru, enGB as en } from 'date-fns/locale';
const locales = { ru, en };
const formatLocalDate = (value, dateFnsFormat, dateFnsLocale = 'ru') => {
let parsedValue = value;
if (!isDate(parsedValue)) {
parsedValue = parseISO(parsedValue);
}
if (isDate(parsedValue)) {
return format(parsedValue, dateFnsFormat ?? 'dd MMM yyyy', {
locale: locales[dateFnsLocale]
});
}
return parsedValue;
};
const setTimeToDate = (date, type, value) => {
let newDate = date;
if (isDate(date)) {
newDate = new Date(date);
switch (type) {
case 'hours':
newDate.setHours(Number(value));
break;
case 'minutes':
newDate.setMinutes(Number(value));
break;
case 'seconds':
newDate.setSeconds(Number(value));
break;
default:
break;
}
}
return newDate;
};
const clearTime = function(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
};
const clearMilliseconds = function(date) {
return new Date(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
0
);
};
export { clearTime, clearMilliseconds, formatLocalDate, setTimeToDate };
================================================
FILE: src/qComponents/QDatePicker/src/panel/date-range.vue
================================================
<template>
<div class="q-picker-panel">
<div class="q-picker-panel__body-wrapper">
<div class="q-picker-panel__body">
<slot
name="sidebar"
class="q-picker-panel__sidebar"
>
<div
v-if="shortcuts.length"
class="q-picker-panel__sidebar"
>
<button
v-for="(shortcut, key) in shortcuts"
:key="key"
type="button"
class="q-picker-panel__shortcut"
@click="handleShortcutClick(shortcut)"
>
{{ shortcut.text }}
</button>
</div>
</slot>
<div
ref="leftPanel"
:class="leftPanelClasses"
>
<div class="q-picker-panel__header">
<button
type="button"
class="q-picker-panel__icon-btn q-icon-double-triangle-left"
@click="handleLeftPrevYearClick"
/>
<button
type="button"
class="q-picker-panel__icon-btn q-icon-triangle-left"
@click="handleLeftPrevMonthClick"
/>
<div class="q-picker-panel__header-sign">{{ leftLabel }}</div>
<button
type="button"
:disabled="!enableMonthArrow"
:class="{
'q-picker-panel__icon-btn_disabled': !enableMonthArrow
}"
class="q-picker-panel__icon-btn q-icon-triangle-right"
@click="handleLeftNextMonthClick"
/>
<button
type="button"
:disabled="!enableYearArrow"
:class="{ 'q-picker-panel__icon-btn_disabled': !enableYearArrow }"
class="q-picker-panel__icon-btn q-icon-double-triangle-right"
@click="handleLeftNextYearClick"
/>
</div>
<date-table
selection-mode="range"
:min-date="minDate"
:max-date="maxDate"
:month="leftMonth"
:year="leftYear"
:range-state="rangeState"
:disabled-values="disabledValues"
:first-day-of-week="firstDayOfWeek"
@changerange="handleChangeRange"
@pick="handleRangePick"
/>
</div>
<div
ref="rightPanel"
:class="rightPanelClasses"
>
<div class="q-picker-panel__header">
<button
type="button"
:disabled="!enableYearArrow"
:class="{ 'q-picker-panel__icon-btn_disabled': !enableYearArrow }"
class="q-picker-panel__icon-btn q-icon-double-triangle-left"
@click="handleRightPrevYearClick"
/>
<button
type="button"
:disabled="!enableMonthArrow"
:class="{
'q-picker-panel__icon-btn_disabled': !enableMonthArrow
}"
class="q-picker-panel__icon-btn q-icon-triangle-left"
@click="handleRightPrevMonthClick"
/>
<div class="q-picker-panel__header-sign">
{{ rightLabel }}
</div>
<button
type="button"
class="q-picker-panel__icon-btn q-icon-triangle-right"
@click="handleRightNextMonthClick"
/>
<button
type="button"
class="q-picker-panel__icon-btn q-icon-double-triangle-right"
@click="handleRightNextYearClick"
/>
</div>
<date-table
selection-mode="range"
:min-date="minDate"
:max-date="maxDate"
:month="rightMonth"
:year="rightYear"
:range-state="rangeState"
:disabled-values="disabledValues"
:first-day-of-week="firstDayOfWeek"
@changerange="handleChangeRange"
@pick="handleRangePick"
/>
</div>
<div
v-show="showTime"
class="q-picker-panel__timepickers"
>
<div class="q-picker-panel__timepicker">
<time-panel
ref="leftTimePanel"
class="time-panel time-panel_no-left-borders time-panel_no-right-borders"
:value="parsedLeftTime"
:panel-in-focus="panelInFocus === 'timeLeft'"
:disabled="isLeftTimeDisabled"
:disabled-values="disabledValues.time"
:prefix-to-time="$t('QDatePicker.timeFrom')"
@change="handleLeftTimeChange"
/>
</div>
<div class="q-picker-panel__timepicker">
<time-panel
ref="rightTimePanel"
:panel-in-focus="panelInFocus === 'timeRight'"
:disabled="isLeftTimeDisabled"
class="time-panel time-panel_no-left-borders"
:value="parsedRightTime"
:disabled-values="disabledRightTimeValues"
:prefix-to-time="$t('QDatePicker.timeTo')"
@change="handleRightTimeChange"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { addMonths, isDate, isSameDay, isSameMonth, subMonths } from 'date-fns';
import DateTable from '
gitextract_kt2b5v6x/
├── .eslintrc
├── .github/
│ └── workflows/
│ ├── codeql-analysis.yml
│ └── deploy.yml
├── .gitignore
├── .lintstagedrc
├── .npmignore
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── .storybook/
│ ├── locales/
│ │ ├── en.js
│ │ ├── index.js
│ │ └── ru.js
│ ├── main.js
│ ├── manager.js
│ ├── preview.js
│ └── theme.js
├── .stylelintignore
├── .stylelintrc
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── babel.config.js
├── jest.config.js
├── package.json
├── scripts/
│ └── badges.js
├── src/
│ ├── components.scss
│ ├── fonts/
│ │ └── index.scss
│ ├── icons/
│ │ └── index.scss
│ ├── main.scss
│ ├── normalize.scss
│ ├── onDemand.js
│ ├── qComponents/
│ │ ├── QBreadcrumbs/
│ │ │ ├── QBreadcrumbs.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QBreadcrumbs.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QBreadcrumbs.vue
│ │ │ └── q-breadcrumbs.scss
│ │ ├── QButton/
│ │ │ ├── QButton.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QButton.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QButton.vue
│ │ │ └── q-button.scss
│ │ ├── QCascader/
│ │ │ ├── QCascader.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QCascader.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCascader.vue
│ │ │ ├── QCascaderMenu.vue
│ │ │ ├── QCascaderPanel.vue
│ │ │ ├── q-cascader-menu.scss
│ │ │ └── q-cascader.scss
│ │ ├── QCheckbox/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCheckbox.vue
│ │ │ └── q-checkbox.scss
│ │ ├── QCheckboxGroup/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCheckboxGroup.vue
│ │ │ └── q-checkbox-group.scss
│ │ ├── QCol/
│ │ │ ├── QCol.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QCol.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCol.vue
│ │ │ └── q-col.scss
│ │ ├── QCollapse/
│ │ │ ├── QCollapse.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QCollapse.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCollapse.vue
│ │ │ └── q-collapse.scss
│ │ ├── QCollapseItem/
│ │ │ ├── QCollapseItem.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QCollapseItem.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QCollapseItem.vue
│ │ │ └── q-collapse-item.scss
│ │ ├── QColorPicker/
│ │ │ ├── QColorAlphaSlider.test.js
│ │ │ ├── QColorHueSlider.test.js
│ │ │ ├── QColorPicker.test.js
│ │ │ ├── QColorSvpanel.test.js
│ │ │ ├── QPickerDropdown.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ ├── QColorAlphaSlider.test.js.snap
│ │ │ │ ├── QColorHueSlider.test.js.snap
│ │ │ │ ├── QColorPicker.test.js.snap
│ │ │ │ ├── QColorSvpanel.test.js.snap
│ │ │ │ └── QPickerDropdown.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QColorAlphaSlider.vue
│ │ │ ├── QColorHueSlider.vue
│ │ │ ├── QColorPicker.vue
│ │ │ ├── QColorSvpanel.vue
│ │ │ ├── QPickerDropdown.vue
│ │ │ ├── draggable.js
│ │ │ ├── q-color-alpha-slider.scss
│ │ │ ├── q-color-hue-slider.scss
│ │ │ ├── q-color-picker.scss
│ │ │ ├── q-color-svpanel.scss
│ │ │ └── q-picker-dropdown.scss
│ │ ├── QContextMenu/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QContextMenu.vue
│ │ │ └── q-context-menu.scss
│ │ ├── QDatePicker/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QDatePicker.vue
│ │ │ ├── basic/
│ │ │ │ ├── date-table.vue
│ │ │ │ ├── month-table.vue
│ │ │ │ └── year-table.vue
│ │ │ ├── helpers.js
│ │ │ ├── panel/
│ │ │ │ ├── date-range.vue
│ │ │ │ ├── date.vue
│ │ │ │ ├── focus-mixin.js
│ │ │ │ ├── focus-time-mixin.js
│ │ │ │ ├── month-range.vue
│ │ │ │ ├── range-mixin.js
│ │ │ │ └── year-range.vue
│ │ │ ├── q-date-picker.scss
│ │ │ └── styles/
│ │ │ ├── date-table.scss
│ │ │ ├── month-table.scss
│ │ │ ├── picker-panel.scss
│ │ │ ├── picker.scss
│ │ │ └── year-table.scss
│ │ ├── QDialog/
│ │ │ ├── QDialog.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QDialog.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QDialog.js
│ │ │ ├── QDialog.vue
│ │ │ └── q-dialog.scss
│ │ ├── QDrawer/
│ │ │ ├── QDrawer.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QDrawer.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QDrawer.vue
│ │ │ └── q-drawer.scss
│ │ ├── QForm/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QForm.vue
│ │ │ └── q-form.scss
│ │ ├── QFormItem/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QFormItem.vue
│ │ │ └── q-form-item.scss
│ │ ├── QInput/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QInput.vue
│ │ │ └── q-input.scss
│ │ ├── QInputNumber/
│ │ │ ├── QInputNumber.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QInputNumber.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QInputNumber.vue
│ │ │ └── q-input-number.scss
│ │ ├── QMessageBox/
│ │ │ ├── QMessageBox.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QMessageBox.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QMessageBox.js
│ │ │ ├── QMessageBox.vue
│ │ │ └── q-message-box.scss
│ │ ├── QNotification/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QNotification.js
│ │ │ ├── QNotification.vue
│ │ │ └── q-notification.scss
│ │ ├── QOption/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QOption.vue
│ │ │ └── q-option.scss
│ │ ├── QPagination/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QPagination.vue
│ │ │ └── q-pagination.scss
│ │ ├── QPopover/
│ │ │ ├── QPopover.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QPopover.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QPopover.vue
│ │ │ └── q-popover.scss
│ │ ├── QRadio/
│ │ │ ├── QRadio.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QRadio.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QRadio.vue
│ │ │ └── q-radio.scss
│ │ ├── QRadioGroup/
│ │ │ ├── QRadioGroup.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QRadioGroup.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QRadioGroup.vue
│ │ │ └── q-radio-group.scss
│ │ ├── QRow/
│ │ │ ├── QRow.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QRow.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QRow.vue
│ │ │ └── q-row.scss
│ │ ├── QScrollbar/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QBar.vue
│ │ │ ├── QScrollbar.vue
│ │ │ ├── q-scrollbar.scss
│ │ │ └── util.js
│ │ ├── QSelect/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QSelect.vue
│ │ │ ├── QSelectDropdown.vue
│ │ │ ├── QSelectTags.vue
│ │ │ ├── q-select-dropdown.scss
│ │ │ ├── q-select-tags.scss
│ │ │ └── q-select.scss
│ │ ├── QSlider/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QSlider.vue
│ │ │ ├── components/
│ │ │ │ ├── QSliderBar/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── QSliderButton/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── QSliderCaptions/
│ │ │ │ │ └── index.vue
│ │ │ │ └── QSliderSteps/
│ │ │ │ └── index.vue
│ │ │ ├── q-slider.scss
│ │ │ └── styles/
│ │ │ ├── q-slider-bar.scss
│ │ │ ├── q-slider-button.scss
│ │ │ ├── q-slider-captions.scss
│ │ │ ├── q-slider-steps.scss
│ │ │ └── q-slider.scss
│ │ ├── QTabPane/
│ │ │ ├── QTabPane.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QTabPane.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTabPane.vue
│ │ │ └── q-tab-pane.scss
│ │ ├── QTable/
│ │ │ ├── QTable.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QTable.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTable.vue
│ │ │ ├── components/
│ │ │ │ ├── DragElements/
│ │ │ │ │ └── index.vue
│ │ │ │ └── QTableRow/
│ │ │ │ └── index.vue
│ │ │ ├── hocs/
│ │ │ │ └── withQTableRow/
│ │ │ │ └── index.js
│ │ │ └── q-table.scss
│ │ ├── QTabs/
│ │ │ ├── QTabs.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QTabs.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTabs.vue
│ │ │ └── q-tabs.scss
│ │ ├── QTag/
│ │ │ ├── QTag.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── QTag.test.js.snap
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTag.vue
│ │ │ └── q-tag.scss
│ │ ├── QTextarea/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTextarea.vue
│ │ │ ├── calcTextareaHeight.js
│ │ │ └── q-textarea.scss
│ │ ├── QTimePicker/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QTimePicker.vue
│ │ │ ├── components/
│ │ │ │ ├── panel.vue
│ │ │ │ └── time-panel.scss
│ │ │ └── q-time-picker.scss
│ │ ├── QUpload/
│ │ │ ├── index.js
│ │ │ └── src/
│ │ │ ├── QUpload.vue
│ │ │ ├── QUploadDropZone.vue
│ │ │ ├── QUploadFileMultiple.vue
│ │ │ ├── QUploadFileSingle.vue
│ │ │ ├── q-upload-drop-zone.scss
│ │ │ ├── q-upload-file-multiple.scss
│ │ │ ├── q-upload-file-single.scss
│ │ │ └── q-upload.scss
│ │ ├── constants/
│ │ │ ├── locales/
│ │ │ │ ├── en.js
│ │ │ │ ├── index.js
│ │ │ │ └── ru.js
│ │ │ └── popperPlacements.js
│ │ ├── helpers/
│ │ │ ├── collapse-transition.js
│ │ │ ├── dateHelpers.js
│ │ │ └── index.js
│ │ ├── index.js
│ │ └── mixins/
│ │ ├── emitter.js
│ │ ├── inputs.js
│ │ └── pickers.js
│ ├── transition.scss
│ └── vars.scss
├── stories/
│ ├── components/
│ │ ├── Layout/
│ │ │ ├── Layout.stories.js
│ │ │ ├── QCol.stories.js
│ │ │ ├── QRow.stories.js
│ │ │ └── layout.scss
│ │ ├── QBreadcrumbs.stories.js
│ │ ├── QButton.stories.mdx
│ │ ├── QCascader.stories.js
│ │ ├── QCheckbox.stories.js
│ │ ├── QCheckboxGroup.stories.js
│ │ ├── QCollapse.stories.js
│ │ ├── QColorPicker.stories.js
│ │ ├── QContextMenu.stories.js
│ │ ├── QDatePicker/
│ │ │ ├── DateRange.js
│ │ │ ├── DateTime.js
│ │ │ ├── DateTimeRange.js
│ │ │ ├── Default.js
│ │ │ ├── Month.js
│ │ │ ├── MonthRange.js
│ │ │ ├── QDatePicker.stories.js
│ │ │ ├── Year.js
│ │ │ └── YearRange.js
│ │ ├── QDialog/
│ │ │ ├── DialogFormTest.vue
│ │ │ └── QDialog.stories.js
│ │ ├── QDrawer.stories.js
│ │ ├── QForm.stories.js
│ │ ├── QInput.stories.js
│ │ ├── QInputNumber.stories.js
│ │ ├── QMessageBox/
│ │ │ ├── MessageBoxFormTest.vue
│ │ │ └── QMessageBox.stories.js
│ │ ├── QNotification.stories.js
│ │ ├── QPagination.stories.js
│ │ ├── QPopover.stories.js
│ │ ├── QRadio.stories.js
│ │ ├── QRadioGroup.stories.js
│ │ ├── QScrollbar/
│ │ │ ├── QScrollbar.stories.js
│ │ │ └── q-scrollbar.scss
│ │ ├── QSelect/
│ │ │ ├── Default.js
│ │ │ ├── Multiple.js
│ │ │ └── QSelect.stories.js
│ │ ├── QSlider/
│ │ │ ├── Breakpoints.js
│ │ │ ├── Captions.js
│ │ │ ├── Default.js
│ │ │ ├── Disabled.js
│ │ │ ├── QSlider.stories.js
│ │ │ ├── Range.js
│ │ │ ├── Vertical.js
│ │ │ └── WithoutTooltip.js
│ │ ├── QTabPane.stories.js
│ │ ├── QTable/
│ │ │ ├── CustomRows.js
│ │ │ ├── CustomWidth.js
│ │ │ ├── Default.js
│ │ │ ├── Draggable.js
│ │ │ ├── Groups.js
│ │ │ ├── QTable.stories.js
│ │ │ ├── Selectable.js
│ │ │ ├── StickyColumn.js
│ │ │ └── Total.js
│ │ ├── QTabs.stories.js
│ │ ├── QTag.stories.js
│ │ ├── QTextarea.stories.js
│ │ ├── QTimePicker.stories.js
│ │ └── QUpload/
│ │ ├── Default.js
│ │ ├── Multiple.js
│ │ └── QUpload.stories.js
│ ├── core/
│ │ ├── colors.js
│ │ ├── colors.stories.mdx
│ │ └── icons.stories.mdx
│ └── intro.stories.mdx
└── tests/
└── unit/
└── setup.js
SYMBOL INDEX (145 symbols across 50 files)
FILE: src/onDemand.js
method get (line 19) | get() {
FILE: src/qComponents/QDatePicker/src/panel/focus-mixin.js
constant DATE_CELLS_COUNT (line 1) | const DATE_CELLS_COUNT = 42;
constant DATE_CELLS_IN_ROW_COUNT (line 2) | const DATE_CELLS_IN_ROW_COUNT = 7;
constant PERIOD_CELLS_IN_ROW_COUNT (line 4) | const PERIOD_CELLS_IN_ROW_COUNT = 4;
constant LEFT_MONTH_PANEL_START_INDEX (line 6) | const LEFT_MONTH_PANEL_START_INDEX = 0;
constant RIGHT_MONTH_PANEL_START_INDEX (line 7) | const RIGHT_MONTH_PANEL_START_INDEX = 12;
constant RIGHT_YEAR_PANEL_START_INDEX (line 8) | const RIGHT_YEAR_PANEL_START_INDEX = 10;
method data (line 11) | data() {
method mounted (line 17) | mounted() {
method setPanelFocus (line 24) | setPanelFocus() {
method navigateDropdown (line 48) | navigateDropdown(e) {
method moveWithinPeriod (line 70) | moveWithinPeriod({ period, e }) {
method moveWithinDates (line 150) | moveWithinDates(e) {
FILE: src/qComponents/QDatePicker/src/panel/focus-time-mixin.js
constant DATE_CELLS_IN_ROW_COUNT (line 1) | const DATE_CELLS_IN_ROW_COUNT = 7;
constant RIGHT_DATE_PANEL_START_INDEX (line 3) | const RIGHT_DATE_PANEL_START_INDEX = 42;
constant LEFT_TIME_PANEL_HOURS_INDEX (line 5) | const LEFT_TIME_PANEL_HOURS_INDEX = 0;
constant LEFT_TIME_PANEL_MINUTES_INDEX (line 6) | const LEFT_TIME_PANEL_MINUTES_INDEX = 24;
constant LEFT_TIME_PANEL_SECONDS_INDEX (line 7) | const LEFT_TIME_PANEL_SECONDS_INDEX = 84;
constant RIGHT_TIME_PANEL_HOURS_INDEX (line 9) | const RIGHT_TIME_PANEL_HOURS_INDEX = 144;
constant RIGHT_TIME_PANEL_MINUTES_INDEX (line 10) | const RIGHT_TIME_PANEL_MINUTES_INDEX = 168;
constant RIGHT_TIME_PANEL_SECONDS_INDEX (line 11) | const RIGHT_TIME_PANEL_SECONDS_INDEX = 228;
method mounted (line 14) | mounted() {
method moveWithinTime (line 19) | moveWithinTime(e) {
FILE: src/qComponents/QDatePicker/src/panel/range-mixin.js
method btnDisabled (line 22) | btnDisabled() {
method leftLabel (line 31) | leftLabel() {
method rightLabel (line 46) | rightLabel() {
method leftYear (line 62) | leftYear() {
method leftMonth (line 69) | leftMonth() {
method rightMonth (line 76) | rightMonth() {
method handler (line 85) | handler(newVal) {
method handleLeftPrevYearClick (line 115) | handleLeftPrevYearClick() {
method handleRightNextYearClick (line 119) | handleRightNextYearClick() {
method handleLeftNextYearClick (line 123) | handleLeftNextYearClick() {
method handleRightPrevYearClick (line 127) | handleRightPrevYearClick() {
method handleShortcutClick (line 131) | handleShortcutClick(shortcut) {
method handleRangePick (line 137) | handleRangePick(val, close = true) {
method handleChangeRange (line 176) | handleChangeRange(val) {
method isValidValue (line 182) | isValidValue(value) {
FILE: src/qComponents/QScrollbar/src/util.js
constant BAR_MAP (line 1) | const BAR_MAP = {
function renderThumbStyle (line 24) | function renderThumbStyle({ move, size, bar }) {
FILE: src/qComponents/QTable/src/hocs/withQTableRow/index.js
method render (line 48) | render(renderContext, context) {
FILE: src/qComponents/QTextarea/src/calcTextareaHeight.js
constant HIDDEN_STYLE (line 3) | const HIDDEN_STYLE = `
constant CONTEXT_STYLE (line 13) | const CONTEXT_STYLE = [
function calculateNodeStyling (line 31) | function calculateNodeStyling(targetElement) {
function calcTextareaHeight (line 51) | function calcTextareaHeight(
FILE: src/qComponents/helpers/collapse-transition.js
method render (line 8) | render(h, { children }) {
FILE: src/qComponents/index.js
method get (line 116) | get() {
method set (line 123) | set(newLocale) {
FILE: src/qComponents/mixins/emitter.js
function broadcast (line 1) | function broadcast(componentName, eventName, params) {
method dispatch (line 15) | dispatch(componentName, eventName, params) {
method broadcast (line 31) | broadcast(componentName, eventName, params) {
FILE: src/qComponents/mixins/inputs.js
method data (line 42) | data() {
method inputDisabled (line 51) | inputDisabled() {
method nativeInputValue (line 55) | nativeInputValue() {
method isClearButtonShown (line 59) | isClearButtonShown() {
method isSymbolLimitShown (line 69) | isSymbolLimitShown() {
method upperLimit (line 79) | upperLimit() {
method textLength (line 83) | textLength() {
method nativeInputValue (line 89) | nativeInputValue() {
method created (line 94) | created() {
method focus (line 99) | focus() {
method blur (line 103) | blur() {
method handleBlur (line 107) | handleBlur(event) {
method select (line 113) | select() {
method setNativeInputValue (line 117) | setNativeInputValue() {
method handleFocus (line 123) | handleFocus(event) {
method handleCompositionStart (line 128) | handleCompositionStart() {
method handleCompositionEnd (line 132) | handleCompositionEnd(event) {
method handleInput (line 139) | handleInput(event) {
method handleChange (line 147) | handleChange(event) {
FILE: src/qComponents/mixins/pickers.js
method handleInput (line 3) | handleInput(val, { inputType }) {
FILE: stories/components/Layout/Layout.stories.js
method demoStyles (line 26) | demoStyles() {
FILE: stories/components/QBreadcrumbs.stories.js
method get (line 21) | get() {
method data (line 68) | data() {
FILE: stories/components/QCascader.stories.js
method data (line 66) | data() {
method handleValueChange (line 70) | handleValueChange(value) {
FILE: stories/components/QCheckbox.stories.js
method data (line 14) | data() {
FILE: stories/components/QCheckboxGroup.stories.js
method data (line 25) | data() {
method data (line 52) | data() {
method handleCheckAllChange (line 61) | handleCheckAllChange(val) {
method handleCheckedCitiesChange (line 65) | handleCheckedCitiesChange(value) {
FILE: stories/components/QCollapse.stories.js
method data (line 15) | data() {
method handleChange (line 21) | handleChange(value) {
FILE: stories/components/QColorPicker.stories.js
method data (line 21) | data() {
FILE: stories/components/QContextMenu.stories.js
method handleAction (line 16) | handleAction(action) {
FILE: stories/components/QDatePicker/Default.js
method data (line 5) | data() {
method handleRangePickClick (line 12) | handleRangePickClick(val) {
method handleChange (line 15) | handleChange(value) {
method handleInput (line 18) | handleInput(value) {
method type (line 23) | type(value) {
FILE: stories/components/QDatePicker/QDatePicker.stories.js
method onClick (line 45) | onClick(picker) {
method onClick (line 51) | onClick(picker) {
method onClick (line 59) | onClick(picker) {
FILE: stories/components/QDialog/QDialog.stories.js
method data (line 13) | data() {
method beforeDestroy (line 16) | beforeDestroy() {
method handleClick (line 21) | async handleClick() {
FILE: stories/components/QDrawer.stories.js
method data (line 17) | data() {
FILE: stories/components/QForm.stories.js
method data (line 16) | data() {
method handleSubmitClick (line 26) | async handleSubmitClick() {
method handleResetClick (line 35) | handleResetClick() {
FILE: stories/components/QInput.stories.js
method data (line 55) | data() {
FILE: stories/components/QInputNumber.stories.js
method data (line 17) | data() {
method handleEmit (line 23) | handleEmit(value, type) {
FILE: stories/components/QMessageBox/QMessageBox.stories.js
method beforeClose (line 14) | async beforeClose({ action, ctx }) {
method handleClick (line 36) | async handleClick() {
method handleClick (line 65) | async handleClick() {
method handleClick (line 87) | async handleClick() {
method handleClick (line 115) | async handleClick() {
FILE: stories/components/QNotification.stories.js
method handleClick (line 27) | handleClick() {
method handleCloseAllClick (line 37) | handleCloseAllClick() {
FILE: stories/components/QPagination.stories.js
method handleCurrentChange (line 11) | handleCurrentChange(val) {
FILE: stories/components/QPopover.stories.js
method data (line 32) | data() {
method trigger (line 38) | trigger() {
FILE: stories/components/QRadio.stories.js
method data (line 15) | data() {
FILE: stories/components/QRadioGroup.stories.js
method data (line 18) | data() {
FILE: stories/components/QSelect/Default.js
method data (line 34) | data() {
method multiple (line 43) | multiple() {
method remote (line 47) | remote(value) {
method handleSearch (line 53) | handleSearch(query) {
FILE: stories/components/QSlider/Breakpoints.js
method data (line 4) | data() {
FILE: stories/components/QSlider/Captions.js
method data (line 4) | data() {
FILE: stories/components/QSlider/Default.js
method data (line 4) | data() {
FILE: stories/components/QSlider/Disabled.js
method data (line 4) | data() {
FILE: stories/components/QSlider/Range.js
method data (line 4) | data() {
FILE: stories/components/QSlider/Vertical.js
method data (line 4) | data() {
FILE: stories/components/QSlider/WithoutTooltip.js
method data (line 4) | data() {
FILE: stories/components/QTabPane.stories.js
method data (line 13) | data() {
FILE: stories/components/QTable/Default.js
method handleRowClick (line 4) | handleRowClick(row) {
method changeColumnsOrder (line 7) | changeColumnsOrder(groupsOfColumns) {
FILE: stories/components/QTable/QTable.stories.js
method changeSort (line 122) | changeSort(sort) {
FILE: stories/components/QTabs.stories.js
method data (line 16) | data() {
FILE: stories/components/QTag.stories.js
method data (line 10) | data() {
method handleCloseClick (line 16) | handleCloseClick(clickedTag) {
FILE: stories/components/QTextarea.stories.js
method data (line 31) | data() {
FILE: stories/components/QTimePicker.stories.js
method data (line 25) | data() {
method handleTimeChange (line 31) | handleTimeChange(val) {
FILE: stories/components/QUpload/Default.js
method data (line 3) | data() {
method handleFileSelect (line 11) | async handleFileSelect(sourceFile) {
method handleAbort (line 34) | handleAbort() {
method handleClear (line 38) | handleClear() {
FILE: stories/components/QUpload/Multiple.js
method data (line 3) | data() {
method handleFileSelect (line 11) | async handleFileSelect(sourceFile, fileId) {
method handleAbort (line 37) | handleAbort(fileId) {
method handleClear (line 41) | handleClear(fileId) {
method handleClearAll (line 47) | handleClearAll() {
Condensed preview — 322 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (660K chars).
[
{
"path": ".eslintrc",
"chars": 1094,
"preview": "{\n \"root\": false,\n\n \"env\": {\n \"browser\": true\n },\n\n \"parserOptions\": {\n \"parser\": \"babel-eslint\",\n \"sourceT"
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 2397,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/deploy.yml",
"chars": 682,
"preview": "name: github pages\n\non:\n push:\n branches:\n - master\n\njobs:\n deploy:\n runs-on: ubuntu-18.04\n steps:\n "
},
{
"path": ".gitignore",
"chars": 245,
"preview": ".DS_Store\nnode_modules\ncoverage\n/dist\n.out\n\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-"
},
{
"path": ".lintstagedrc",
"chars": 365,
"preview": "{\n \"*.js\": [\"prettier --write\", \"git add\"],\n \"*.scss\": [\"stylelint --fix\", \"prettier --write\", \"git add\"],\n \"*.vue\": "
},
{
"path": ".npmignore",
"chars": 52,
"preview": "yarn-error.log\n/.storybook\n/.github\n/.readme-assets\n"
},
{
"path": ".nvmrc",
"chars": 8,
"preview": "12.22.6\n"
},
{
"path": ".prettierignore",
"chars": 13,
"preview": ".nvmrc\n/dist\n"
},
{
"path": ".prettierrc",
"chars": 228,
"preview": "{\n \"printWidth\": 80,\n \"tabWidth\": 2,\n \"useTabs\": false,\n \"semi\": true,\n \"singleQuote\": true,\n \"quoteProps\": \"as-ne"
},
{
"path": ".storybook/locales/en.js",
"chars": 140,
"preview": "export default {\n qBreadcrumbsStories: {\n routeA: 'Route A',\n routeB: 'Route B',\n routeC: 'Route C',\n route"
},
{
"path": ".storybook/locales/index.js",
"chars": 252,
"preview": "import en from './en';\nimport ru from './ru';\nimport {\n en as qMessagesEn,\n ru as qMessagesRu\n} from '../../src/qCompo"
},
{
"path": ".storybook/locales/ru.js",
"chars": 153,
"preview": "export default {\n qBreadcrumbsStories: {\n routeA: 'Роут А',\n routeB: 'Очень длинный маршрут Б',\n routeC: 'Роут"
},
{
"path": ".storybook/main.js",
"chars": 275,
"preview": "module.exports = {\n stories: ['../stories/**/**/*.stories.@(ts|js|mdx)'],\n logLevel: 'debug',\n addons: [\n '@storyb"
},
{
"path": ".storybook/manager.js",
"chars": 101,
"preview": "import addons from '@storybook/addons';\nimport theme from './theme';\n\naddons.setConfig({\n theme\n});\n"
},
{
"path": ".storybook/preview.js",
"chars": 1026,
"preview": "import Vue from 'vue';\nimport Qui from '../src/qComponents';\nimport VueI18n from 'vue-i18n';\nimport messages from './loc"
},
{
"path": ".storybook/theme.js",
"chars": 255,
"preview": "import { create } from '@storybook/theming/create';\nimport logo from '../.readme-assets/qui-logo.svg';\n\nexport default c"
},
{
"path": ".stylelintignore",
"chars": 20,
"preview": "/src/normalize.scss\n"
},
{
"path": ".stylelintrc",
"chars": 5007,
"preview": "{\n \"extends\": [\"stylelint-config-prettier\"],\n \"plugins\": [\"stylelint-order\"],\n \"rules\": {\n \"color-hex-length\": \"sh"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3332,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "LICENSE",
"chars": 1062,
"preview": "MIT License\n\nCopyright (c) 2020 Qvant\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
},
{
"path": "README.md",
"chars": 6701,
"preview": "<p align=\"center\">\n <img src=\"/.readme-assets/qui-logo.svg?raw=true\" />\n</p>\n\n<p align=\"center\" class=\"unchanged rich-d"
},
{
"path": "babel.config.js",
"chars": 66,
"preview": "module.exports = {\n presets: ['@vue/cli-plugin-babel/preset']\n};\n"
},
{
"path": "jest.config.js",
"chars": 682,
"preview": "module.exports = {\n moduleFileExtensions: ['js', 'vue', 'json'],\n transform: {\n '^.+\\\\.vue$': 'vue-jest',\n '^.+\\"
},
{
"path": "package.json",
"chars": 3418,
"preview": "{\n \"name\": \"@qvant/qui\",\n \"version\": \"1.4.5\",\n \"private\": false,\n \"description\": \"A Vue.js Design system for Web.\",\n"
},
{
"path": "scripts/badges.js",
"chars": 1713,
"preview": "// edit `list` & run `node scripts/badges.js` for creating new badges\n\nconst { renderBadges } = require('badges');\nconst"
},
{
"path": "src/components.scss",
"chars": 1846,
"preview": "@import '../qComponents/QBreadcrumbs/src/q-breadcrumbs.scss';\n@import '../qComponents/QButton/src/q-button.scss';\n@impor"
},
{
"path": "src/fonts/index.scss",
"chars": 983,
"preview": "$--base-path: '..' !default;\n\n@font-face {\n font-display: swap;\n font-style: normal;\n font-weight: 800;\n font-family"
},
{
"path": "src/icons/index.scss",
"chars": 6246,
"preview": "$--base-path: '..' !default;\n\n@font-face {\n font-style: normal;\n font-weight: 400;\n font-family: 'qicon';\n src: url("
},
{
"path": "src/main.scss",
"chars": 930,
"preview": "@import './normalize.scss';\n@import './vars.scss';\n@import './transition.scss';\n\n*,\n*::before,\n*::after {\n box-sizing: "
},
{
"path": "src/normalize.scss",
"chars": 5921,
"preview": "/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */\n\n/* Document\n ==========================="
},
{
"path": "src/onDemand.js",
"chars": 763,
"preview": "/* eslint-disable global-require */\n/* eslint-disable no-param-reassign */\nimport vClickOutside from 'v-click-outside';\n"
},
{
"path": "src/qComponents/QBreadcrumbs/QBreadcrumbs.test.js",
"chars": 1154,
"preview": "import Component from './src/QBreadcrumbs';\n\ndescribe('QBreadcrumbs', () => {\n let instance;\n let options;\n\n beforeEa"
},
{
"path": "src/qComponents/QBreadcrumbs/__snapshots__/QBreadcrumbs.test.js.snap",
"chars": 233,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QBreadcrumbs should match snapshot 1`] = `\n<div\n class=\"q-breadcru"
},
{
"path": "src/qComponents/QBreadcrumbs/index.js",
"chars": 201,
"preview": "import QBreadcrumbs from './src/QBreadcrumbs.vue';\n\n/* istanbul ignore next */\nQBreadcrumbs.install = function(Vue) {\n "
},
{
"path": "src/qComponents/QBreadcrumbs/src/QBreadcrumbs.vue",
"chars": 1727,
"preview": "<template>\n <div class=\"q-breadcrumbs\">\n <template v-for=\"crumb in breadcrumbs\">\n <router-link\n :key=\"cr"
},
{
"path": "src/qComponents/QBreadcrumbs/src/q-breadcrumbs.scss",
"chars": 860,
"preview": ".q-breadcrumbs {\n display: inline-grid;\n grid-auto-flow: column;\n align-items: baseline;\n max-width: 100%;\n\n &__div"
},
{
"path": "src/qComponents/QButton/QButton.test.js",
"chars": 969,
"preview": "import Component from './src/QButton';\n\ndescribe('QButton', () => {\n it('should match snapshot', () => {\n const { el"
},
{
"path": "src/qComponents/QButton/__snapshots__/QButton.test.js.snap",
"chars": 253,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QButton should match snapshot 1`] = `\n<button\n class=\"q-button q-b"
},
{
"path": "src/qComponents/QButton/index.js",
"chars": 171,
"preview": "import QButton from './src/QButton.vue';\n\n/* istanbul ignore next */\nQButton.install = function(Vue) {\n Vue.component(Q"
},
{
"path": "src/qComponents/QButton/src/QButton.vue",
"chars": 2423,
"preview": "<template>\n <button\n class=\"q-button\"\n :disabled=\"disabled || loading\"\n :autofocus=\"autofocus\"\n :type=\"nati"
},
{
"path": "src/qComponents/QButton/src/q-button.scss",
"chars": 4801,
"preview": ".q-button {\n display: inline-block;\n box-sizing: border-box;\n max-height: 40px;\n padding: 12px 40px;\n font-weight: "
},
{
"path": "src/qComponents/QCascader/QCascader.test.js",
"chars": 2098,
"preview": "import Component from './src/QCascader';\n\nconst module = require('../helpers');\n\nmodule.randId = jest.fn();\n\ndescribe('Q"
},
{
"path": "src/qComponents/QCascader/__snapshots__/QCascader.test.js.snap",
"chars": 3177,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QCascader data should match snapshot 1`] = `\nObject {\n \"areTagsHov"
},
{
"path": "src/qComponents/QCascader/index.js",
"chars": 179,
"preview": "import QCascader from './src/QCascader';\n\n/* istanbul ignore next */\nQCascader.install = function(Vue) {\n Vue.component"
},
{
"path": "src/qComponents/QCascader/src/QCascader.vue",
"chars": 10909,
"preview": "<template>\n <div\n ref=\"reference\"\n v-click-outside=\"hidePopper\"\n :class=\"['q-cascader', { 'q-cascader_disabled"
},
{
"path": "src/qComponents/QCascader/src/QCascaderMenu.vue",
"chars": 6963,
"preview": "<template>\n <div class=\"q-cascader-menu\">\n <div :class=\"classes\">\n <q-scrollbar wrap-class=\"q-cascader-menu__sc"
},
{
"path": "src/qComponents/QCascader/src/QCascaderPanel.vue",
"chars": 3306,
"preview": "<template>\n <div class=\"q-cascader-panel\">\n <q-cascader-menu\n v-for=\"(menu, index) in menus\"\n :key=\"index\""
},
{
"path": "src/qComponents/QCascader/src/q-cascader-menu.scss",
"chars": 3224,
"preview": ".q-cascader-menu {\n min-width: 200px;\n height: auto;\n max-height: 304px;\n margin-left: 1px;\n\n &__wrap {\n overflo"
},
{
"path": "src/qComponents/QCascader/src/q-cascader.scss",
"chars": 2240,
"preview": ".q-cascader {\n position: relative;\n display: inline-block;\n width: 100%;\n font-size: var(--font-size-base);\n\n &:not"
},
{
"path": "src/qComponents/QCheckbox/index.js",
"chars": 183,
"preview": "import QCheckbox from './src/QCheckbox.vue';\n\n/* istanbul ignore next */\nQCheckbox.install = function(Vue) {\n Vue.compo"
},
{
"path": "src/qComponents/QCheckbox/src/QCheckbox.vue",
"chars": 5106,
"preview": "<template>\n <component\n :is=\"rootTag\"\n class=\"q-checkbox\"\n :class=\"{\n 'q-checkbox_disabled': isDisabled,\n"
},
{
"path": "src/qComponents/QCheckbox/src/q-checkbox.scss",
"chars": 3454,
"preview": ".q-checkbox {\n --checkbox-color-base: var(--color-primary-black);\n --checkbox-color-disabled: rgba(var(--color-rgb-gra"
},
{
"path": "src/qComponents/QCheckboxGroup/index.js",
"chars": 213,
"preview": "import QCheckboxGroup from './src/QCheckboxGroup.vue';\n\n/* istanbul ignore next */\nQCheckboxGroup.install = function(Vue"
},
{
"path": "src/qComponents/QCheckboxGroup/src/QCheckboxGroup.vue",
"chars": 1153,
"preview": "<template>\n <component\n :is=\"tag\"\n :class=\"['q-checkbox-group', `q-checkbox-group_${direction}`]\"\n role=\"group"
},
{
"path": "src/qComponents/QCheckboxGroup/src/q-checkbox-group.scss",
"chars": 306,
"preview": ".q-checkbox-group {\n display: flex;\n align-items: flex-start;\n font-size: 0;\n line-height: 1;\n\n &_vertical {\n fl"
},
{
"path": "src/qComponents/QCol/QCol.test.js",
"chars": 205,
"preview": "import Component from './src/QCol';\n\ndescribe('QCol', () => {\n it('should match snapshot', async () => {\n const { el"
},
{
"path": "src/qComponents/QCol/__snapshots__/QCol.test.js.snap",
"chars": 115,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QCol should match snapshot 1`] = `\n<div\n class=\"q-col\"\n/>\n`;\n"
},
{
"path": "src/qComponents/QCol/index.js",
"chars": 115,
"preview": "import QCol from './src/QCol';\n\nQCol.install = Vue => {\n Vue.component(QCol.name, QCol);\n};\n\nexport default QCol;\n"
},
{
"path": "src/qComponents/QCol/src/QCol.vue",
"chars": 983,
"preview": "<template>\n <component\n :is=\"tag\"\n class=\"q-col\"\n :class=\"classes\"\n >\n <slot />\n </component>\n</template>"
},
{
"path": "src/qComponents/QCol/src/q-col.scss",
"chars": 495,
"preview": ".q-col {\n position: relative;\n flex: 1 0 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--layout-gutte"
},
{
"path": "src/qComponents/QCollapse/QCollapse.test.js",
"chars": 299,
"preview": "import Component from './src/QCollapse';\n\ndescribe('QCollapse', () => {\n it('should match snapshot', () => {\n const "
},
{
"path": "src/qComponents/QCollapse/__snapshots__/QCollapse.test.js.snap",
"chars": 247,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QCollapse data should match snapshot 1`] = `\nObject {\n \"activeName"
},
{
"path": "src/qComponents/QCollapse/index.js",
"chars": 183,
"preview": "import QCollapse from './src/QCollapse.vue';\n\n/* istanbul ignore next */\nQCollapse.install = function(Vue) {\n Vue.compo"
},
{
"path": "src/qComponents/QCollapse/src/QCollapse.vue",
"chars": 1166,
"preview": "<template>\n <div class=\"q-collapse\">\n <slot />\n </div>\n</template>\n\n<script>\nimport { uniqueId } from 'lodash-es';\n"
},
{
"path": "src/qComponents/QCollapse/src/q-collapse.scss",
"chars": 0,
"preview": ""
},
{
"path": "src/qComponents/QCollapseItem/QCollapseItem.test.js",
"chars": 307,
"preview": "import Component from './src/QCollapseItem';\n\ndescribe('QCollapseItem', () => {\n it('should match snapshot', () => {\n "
},
{
"path": "src/qComponents/QCollapseItem/__snapshots__/QCollapseItem.test.js.snap",
"chars": 639,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QCollapseItem data should match snapshot 1`] = `\nObject {\n \"active"
},
{
"path": "src/qComponents/QCollapseItem/index.js",
"chars": 207,
"preview": "import QCollapseItem from './src/QCollapseItem.vue';\n\n/* istanbul ignore next */\nQCollapseItem.install = function(Vue) {"
},
{
"path": "src/qComponents/QCollapseItem/src/QCollapseItem.vue",
"chars": 1535,
"preview": "<template>\n <div\n class=\"q-collapse-item\"\n :class=\"{\n 'q-collapse-item_active': isActive\n }\"\n >\n <but"
},
{
"path": "src/qComponents/QCollapseItem/src/q-collapse-item.scss",
"chars": 1577,
"preview": ".q-collapse-item {\n &:not(:last-child) {\n margin-bottom: 16px;\n }\n\n &__header {\n display: flex;\n align-items"
},
{
"path": "src/qComponents/QColorPicker/QColorAlphaSlider.test.js",
"chars": 428,
"preview": "import Component from './src/QColorAlphaSlider';\n\ndescribe('QColorAlphaSlider', () => {\n it('should match snapshot', as"
},
{
"path": "src/qComponents/QColorPicker/QColorHueSlider.test.js",
"chars": 384,
"preview": "import Component from './src/QColorHueSlider';\n\ndescribe('QColorHueSlider', () => {\n it('should match snapshot', async "
},
{
"path": "src/qComponents/QColorPicker/QColorPicker.test.js",
"chars": 325,
"preview": "import Component from './src/QColorPicker';\n\ndescribe('QColorPicker', () => {\n it('should match snapshot', async () => "
},
{
"path": "src/qComponents/QColorPicker/QColorSvpanel.test.js",
"chars": 423,
"preview": "import Component from './src/QColorSvpanel';\n\ndescribe('QColorSvpanel', () => {\n it('should match snapshot', async () ="
},
{
"path": "src/qComponents/QColorPicker/QPickerDropdown.test.js",
"chars": 331,
"preview": "import Component from './src/QPickerDropdown';\n\ndescribe('QPickerDropdown', () => {\n it('should match snapshot', async "
},
{
"path": "src/qComponents/QColorPicker/__snapshots__/QColorAlphaSlider.test.js.snap",
"chars": 373,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QColorAlphaSlider data should match snapshot 1`] = `\nObject {\n \"th"
},
{
"path": "src/qComponents/QColorPicker/__snapshots__/QColorHueSlider.test.js.snap",
"chars": 361,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QColorHueSlider data should match snapshot 1`] = `\nObject {\n \"thum"
},
{
"path": "src/qComponents/QColorPicker/__snapshots__/QColorPicker.test.js.snap",
"chars": 679,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QColorPicker data should match snapshot 1`] = `\nObject {\n \"isClick"
},
{
"path": "src/qComponents/QColorPicker/__snapshots__/QColorSvpanel.test.js.snap",
"chars": 373,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QColorSvpanel data should match snapshot 1`] = `\nObject {\n \"cursor"
},
{
"path": "src/qComponents/QColorPicker/__snapshots__/QPickerDropdown.test.js.snap",
"chars": 1235,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QPickerDropdown data should match snapshot 1`] = `\nObject {\n \"alph"
},
{
"path": "src/qComponents/QColorPicker/index.js",
"chars": 163,
"preview": "import QColorPicker from './src/QColorPicker';\n\nQColorPicker.install = Vue => {\n Vue.component(QColorPicker.name, QColo"
},
{
"path": "src/qComponents/QColorPicker/src/QColorAlphaSlider.vue",
"chars": 2044,
"preview": "<template>\n <div class=\"q-color-alpha-slider\">\n <div\n ref=\"bar\"\n class=\"q-color-alpha-slider__bar\"\n :"
},
{
"path": "src/qComponents/QColorPicker/src/QColorHueSlider.vue",
"chars": 1795,
"preview": "<template>\n <div class=\"q-color-hue-slider\">\n <div\n ref=\"bar\"\n class=\"q-color-hue-slider__bar\"\n @clic"
},
{
"path": "src/qComponents/QColorPicker/src/QColorPicker.vue",
"chars": 5088,
"preview": "<template>\n <div class=\"q-color-picker\">\n <div\n ref=\"trigger\"\n class=\"q-color-picker-trigger\"\n :class"
},
{
"path": "src/qComponents/QColorPicker/src/QColorSvpanel.vue",
"chars": 1775,
"preview": "<template>\n <div\n class=\"q-color-svpanel\"\n :style=\"rootStyles\"\n >\n <div\n class=\"q-color-svpanel__cursor\""
},
{
"path": "src/qComponents/QColorPicker/src/QPickerDropdown.vue",
"chars": 4258,
"preview": "<template>\n <transition name=\"fade\">\n <div\n v-show=\"isShown\"\n ref=\"dropdown\"\n v-click-outside=\"closeD"
},
{
"path": "src/qComponents/QColorPicker/src/draggable.js",
"chars": 758,
"preview": "let isDragging = false;\n\nexport default function(element, options) {\n const moveFn = event => {\n if (options.drag) o"
},
{
"path": "src/qComponents/QColorPicker/src/q-color-alpha-slider.scss",
"chars": 1185,
"preview": ".q-color-alpha-slider {\n position: relative;\n width: 464px;\n height: 16px;\n margin-top: 8px;\n overflow: hidden;\n b"
},
{
"path": "src/qComponents/QColorPicker/src/q-color-hue-slider.scss",
"chars": 776,
"preview": ".q-color-hue-slider {\n position: relative;\n width: 16px;\n height: 208px;\n margin-left: 16px;\n cursor: pointer;\n\n &"
},
{
"path": "src/qComponents/QColorPicker/src/q-color-picker.scss",
"chars": 3867,
"preview": "@import './q-picker-dropdown';\n@import './q-color-svpanel';\n@import './q-color-hue-slider';\n@import './q-color-alpha-sli"
},
{
"path": "src/qComponents/QColorPicker/src/q-color-svpanel.scss",
"chars": 887,
"preview": ".q-color-svpanel {\n position: relative;\n width: 464px;\n height: 208px;\n cursor: pointer;\n\n &::before,\n &::after {\n"
},
{
"path": "src/qComponents/QColorPicker/src/q-picker-dropdown.scss",
"chars": 431,
"preview": ".q-picker-dropdown {\n padding: 16px;\n background-color: var(--color-tertiary-gray-ultra-light);\n border-radius: var(-"
},
{
"path": "src/qComponents/QContextMenu/index.js",
"chars": 201,
"preview": "import QContextMenu from './src/QContextMenu.vue';\n\n/* istanbul ignore next */\nQContextMenu.install = function(Vue) {\n "
},
{
"path": "src/qComponents/QContextMenu/src/QContextMenu.vue",
"chars": 4456,
"preview": "<template>\n <div class=\"q-context-wrapper\">\n <div\n ref=\"reference\"\n class=\"q-context-trigger\"\n @click"
},
{
"path": "src/qComponents/QContextMenu/src/q-context-menu.scss",
"chars": 1845,
"preview": ".q-context {\n &-menu {\n min-width: 150px;\n overflow: hidden;\n background-color: var(--color-tertiary-gray-ligh"
},
{
"path": "src/qComponents/QDatePicker/index.js",
"chars": 203,
"preview": "import QDatePicker from './src/QDatePicker.vue';\n\n/* istanbul ignore next */\nQDatePicker.install = function install(Vue)"
},
{
"path": "src/qComponents/QDatePicker/src/QDatePicker.vue",
"chars": 14764,
"preview": "<template>\n <div v-click-outside=\"handleClose\">\n <q-input\n v-if=\"!isRanged\"\n ref=\"reference\"\n :class="
},
{
"path": "src/qComponents/QDatePicker/src/basic/date-table.vue",
"chars": 8852,
"preview": "<template>\n <table\n cellspacing=\"10\"\n cellpadding=\"2\"\n class=\"q-date-table\"\n @mousemove=\"handleMouseMove\"\n "
},
{
"path": "src/qComponents/QDatePicker/src/basic/month-table.vue",
"chars": 5892,
"preview": "<template>\n <table\n cellspacing=\"4\"\n cellpadding=\"5\"\n class=\"q-month-table\"\n @mousemove=\"handleMouseMove\"\n "
},
{
"path": "src/qComponents/QDatePicker/src/basic/year-table.vue",
"chars": 4970,
"preview": "<template>\n <table\n cellspacing=\"4\"\n cellpadding=\"5\"\n class=\"q-year-table\"\n >\n <tr\n v-for=\"(row, key)"
},
{
"path": "src/qComponents/QDatePicker/src/helpers.js",
"chars": 1313,
"preview": "import { format, isDate, parseISO } from 'date-fns';\nimport { ru, enGB as en } from 'date-fns/locale';\n\nconst locales = "
},
{
"path": "src/qComponents/QDatePicker/src/panel/date-range.vue",
"chars": 10525,
"preview": "<template>\n <div class=\"q-picker-panel\">\n <div class=\"q-picker-panel__body-wrapper\">\n <div class=\"q-picker-pane"
},
{
"path": "src/qComponents/QDatePicker/src/panel/date.vue",
"chars": 9124,
"preview": "<template>\n <div class=\"q-picker-panel\">\n <div class=\"q-picker-panel__body\">\n <slot\n name=\"sidebar\"\n "
},
{
"path": "src/qComponents/QDatePicker/src/panel/focus-mixin.js",
"chars": 7342,
"preview": "const DATE_CELLS_COUNT = 42;\nconst DATE_CELLS_IN_ROW_COUNT = 7;\n\nconst PERIOD_CELLS_IN_ROW_COUNT = 4;\n\nconst LEFT_MONTH_"
},
{
"path": "src/qComponents/QDatePicker/src/panel/focus-time-mixin.js",
"chars": 5202,
"preview": "const DATE_CELLS_IN_ROW_COUNT = 7;\n\nconst RIGHT_DATE_PANEL_START_INDEX = 42;\n\nconst LEFT_TIME_PANEL_HOURS_INDEX = 0;\ncon"
},
{
"path": "src/qComponents/QDatePicker/src/panel/month-range.vue",
"chars": 4657,
"preview": "<template>\n <div class=\"q-picker-panel\">\n <div class=\"q-picker-panel__body-wrapper\">\n <div class=\"q-picker-pane"
},
{
"path": "src/qComponents/QDatePicker/src/panel/range-mixin.js",
"chars": 4761,
"preview": "import {\n subYears,\n addYears,\n isDate,\n startOfDecade,\n endOfDecade,\n isSameMonth,\n addMonths,\n getDecade,\n en"
},
{
"path": "src/qComponents/QDatePicker/src/panel/year-range.vue",
"chars": 5087,
"preview": "<template>\n <div class=\"q-picker-panel\">\n <div class=\"q-picker-panel__body-wrapper\">\n <slot\n name=\"sideb"
},
{
"path": "src/qComponents/QDatePicker/src/q-date-picker.scss",
"chars": 239,
"preview": "@import './styles/date-table.scss';\n@import './styles/month-table.scss';\n@import './styles/year-table.scss';\n@import './"
},
{
"path": "src/qComponents/QDatePicker/src/styles/date-table.scss",
"chars": 2966,
"preview": ".q-date-table {\n width: 100%;\n table-layout: fixed;\n font-size: 10px;\n user-select: none;\n\n &_days {\n line-heigh"
},
{
"path": "src/qComponents/QDatePicker/src/styles/month-table.scss",
"chars": 1953,
"preview": ".q-month-table {\n width: 100%;\n padding: 10px 0 20px;\n table-layout: fixed;\n font-size: 12px;\n\n &__cell-wrapper {\n "
},
{
"path": "src/qComponents/QDatePicker/src/styles/picker-panel.scss",
"chars": 3607,
"preview": ".q-picker-panel {\n z-index: 1;\n display: none;\n border-radius: 4px;\n\n &_shown {\n display: block;\n }\n\n &__sideba"
},
{
"path": "src/qComponents/QDatePicker/src/styles/picker.scss",
"chars": 2147,
"preview": ".q-date-editor {\n --field-box-shadow-focus: -1px -1px 3px rgba(var(--color-rgb-white), 0.25),\n 1px 1px 3px rgba(var("
},
{
"path": "src/qComponents/QDatePicker/src/styles/year-table.scss",
"chars": 1954,
"preview": ".q-year-table {\n width: 100%;\n padding: 10px 0 20px;\n table-layout: fixed;\n font-size: 12px;\n\n &__cell-wrapper {\n "
},
{
"path": "src/qComponents/QDialog/QDialog.test.js",
"chars": 418,
"preview": "import Component from './src/QDialog.vue';\n\ndescribe('QDialog', () => {\n it('should match snapshot', async () => {\n "
},
{
"path": "src/qComponents/QDialog/__snapshots__/QDialog.test.js.snap",
"chars": 1171,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QDialog data should match snapshot 1`] = `\nObject {\n \"callback\": n"
},
{
"path": "src/qComponents/QDialog/index.js",
"chars": 65,
"preview": "import QDialog from './src/QDialog.js';\n\nexport default QDialog;\n"
},
{
"path": "src/qComponents/QDialog/src/QDialog.js",
"chars": 992,
"preview": "import Vue from 'vue';\n\nimport QDialog from './QDialog.vue';\n\nlet currentPromise;\n\nconst defaultCallback = ({ payload })"
},
{
"path": "src/qComponents/QDialog/src/QDialog.vue",
"chars": 3503,
"preview": "<template>\n <transition\n name=\"q-dialog-fade\"\n @after-enter=\"handleHookAfterEnter\"\n @after-leave=\"handleHookAf"
},
{
"path": "src/qComponents/QDialog/src/q-dialog.scss",
"chars": 1242,
"preview": ".q-dialog {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n display: flex;\n justify-content: center;\n align-item"
},
{
"path": "src/qComponents/QDrawer/QDrawer.test.js",
"chars": 374,
"preview": "import Component from './src/QDrawer';\n\ndescribe('QDrawer', () => {\n it('should match snapshot', async () => {\n cons"
},
{
"path": "src/qComponents/QDrawer/__snapshots__/QDrawer.test.js.snap",
"chars": 896,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QDrawer data should match snapshot 1`] = `\nObject {\n \"elementToFoc"
},
{
"path": "src/qComponents/QDrawer/index.js",
"chars": 133,
"preview": "import QDrawer from './src/QDrawer';\n\nQDrawer.install = Vue => {\n Vue.component(QDrawer.name, QDrawer);\n};\n\nexport defa"
},
{
"path": "src/qComponents/QDrawer/src/QDrawer.vue",
"chars": 4401,
"preview": "<template>\n <transition\n name=\"q-drawer-fade\"\n @after-enter=\"afterEnter\"\n @after-leave=\"afterLeave\"\n >\n <d"
},
{
"path": "src/qComponents/QDrawer/src/q-drawer.scss",
"chars": 2573,
"preview": "@keyframes left-drawer-in {\n 0% {\n transform: translate(-100%, 0);\n }\n\n 100% {\n transform: translate(0, 0);\n }"
},
{
"path": "src/qComponents/QForm/index.js",
"chars": 121,
"preview": "import QForm from './src/QForm';\n\nQForm.install = Vue => {\n Vue.component(QForm.name, QForm);\n};\n\nexport default QForm;"
},
{
"path": "src/qComponents/QForm/src/QForm.vue",
"chars": 3438,
"preview": "<template>\n <form class=\"q-form\">\n <slot />\n </form>\n</template>\n\n<script>\nimport { concat } from 'lodash-es';\n\n/**"
},
{
"path": "src/qComponents/QForm/src/q-form.scss",
"chars": 0,
"preview": ""
},
{
"path": "src/qComponents/QFormItem/index.js",
"chars": 145,
"preview": "import QFormItem from './src/QFormItem';\n\nQFormItem.install = Vue => {\n Vue.component(QFormItem.name, QFormItem);\n};\n\ne"
},
{
"path": "src/qComponents/QFormItem/src/QFormItem.vue",
"chars": 5095,
"preview": "<template>\n <div\n class=\"q-form-item\"\n :class=\"rootClasses\"\n >\n <div\n v-if=\"isHeaderShown\"\n class=\""
},
{
"path": "src/qComponents/QFormItem/src/q-form-item.scss",
"chars": 880,
"preview": ".q-form-item {\n position: relative;\n\n &__header {\n position: relative;\n z-index: 1;\n display: flex;\n align"
},
{
"path": "src/qComponents/QInput/index.js",
"chars": 165,
"preview": "import QInput from './src/QInput.vue';\n\n/* istanbul ignore next */\nQInput.install = function(Vue) {\n Vue.component(QInp"
},
{
"path": "src/qComponents/QInput/src/QInput.vue",
"chars": 4074,
"preview": "<template>\n <div\n :class=\"classes\"\n @mouseenter=\"hovering = true\"\n @mouseleave=\"hovering = false\"\n @click=\""
},
{
"path": "src/qComponents/QInput/src/q-input.scss",
"chars": 4612,
"preview": ".q-input {\n --field-color-base: var(--color-primary-black);\n --field-color-placeholder: rgba(var(--color-rgb-gray), 0."
},
{
"path": "src/qComponents/QInputNumber/QInputNumber.test.js",
"chars": 524,
"preview": "import Component from './src/QInputNumber';\n\ndescribe('QInputNumber', () => {\n it('should match snapshot', () => {\n "
},
{
"path": "src/qComponents/QInputNumber/__snapshots__/QInputNumber.test.js.snap",
"chars": 950,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QInputNumber data should match snapshot 1`] = `\nObject {\n \"number\""
},
{
"path": "src/qComponents/QInputNumber/index.js",
"chars": 201,
"preview": "import QInputNumber from './src/QInputNumber.vue';\n\n/* istanbul ignore next */\nQInputNumber.install = function(Vue) {\n "
},
{
"path": "src/qComponents/QInputNumber/src/QInputNumber.vue",
"chars": 4979,
"preview": "<template>\n <div\n class=\"q-input-number\"\n :class=\"withControlsClass\"\n >\n <button\n v-if=\"controls\"\n "
},
{
"path": "src/qComponents/QInputNumber/src/q-input-number.scss",
"chars": 1185,
"preview": ".q-input-number {\n position: relative;\n display: inline-block;\n width: 100%;\n line-height: 40px;\n\n &_with-controls "
},
{
"path": "src/qComponents/QMessageBox/QMessageBox.test.js",
"chars": 431,
"preview": "import Component from './src/QMessageBox.vue';\n\ndescribe('QMessageBox', () => {\n it('should match snapshot', async () ="
},
{
"path": "src/qComponents/QMessageBox/__snapshots__/QMessageBox.test.js.snap",
"chars": 1243,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QMessageBox data should match snapshot 1`] = `\nObject {\n \"callback"
},
{
"path": "src/qComponents/QMessageBox/index.js",
"chars": 77,
"preview": "import QMessageBox from './src/QMessageBox.js';\n\nexport default QMessageBox;\n"
},
{
"path": "src/qComponents/QMessageBox/src/QMessageBox.js",
"chars": 1324,
"preview": "import Vue from 'vue';\n\nimport QMessageBox from './QMessageBox.vue';\n\nlet currentPromise;\n\nconst defaultCallback = ({ ac"
},
{
"path": "src/qComponents/QMessageBox/src/QMessageBox.vue",
"chars": 6399,
"preview": "<template>\n <transition\n name=\"q-msgbox-fade\"\n @after-leave=\"handleHookAfterLeave\"\n >\n <div\n v-if=\"isSho"
},
{
"path": "src/qComponents/QMessageBox/src/q-message-box.scss",
"chars": 3066,
"preview": ".q-message-box {\n --message-box-indent: 32px;\n --message-box-container-width: 488px;\n\n position: fixed;\n top: 0;\n r"
},
{
"path": "src/qComponents/QNotification/index.js",
"chars": 83,
"preview": "import QNotification from './src/QNotification.js';\n\nexport default QNotification;\n"
},
{
"path": "src/qComponents/QNotification/src/QNotification.js",
"chars": 1495,
"preview": "import Vue from 'vue';\n\nimport QNotification from './QNotification.vue';\n\nconst instances = [];\nlet instancesCounter = 1"
},
{
"path": "src/qComponents/QNotification/src/QNotification.vue",
"chars": 2862,
"preview": "<template>\n <transition\n name=\"q-notification-fade\"\n @after-leave=\"handleHookAfterLeave\"\n >\n <div\n v-if="
},
{
"path": "src/qComponents/QNotification/src/q-notification.scss",
"chars": 1832,
"preview": ".q-notification-box {\n position: fixed;\n top: 16px;\n right: 16px;\n}\n\n.q-notification {\n position: relative;\n displa"
},
{
"path": "src/qComponents/QOption/index.js",
"chars": 140,
"preview": "import QOption from './src/QOption';\n\nQOption.install = function(Vue) {\n Vue.component(QOption.name, QOption);\n};\n\nexpo"
},
{
"path": "src/qComponents/QOption/src/QOption.vue",
"chars": 3379,
"preview": "<template>\n <div\n v-show=\"isVisible\"\n class=\"q-option\"\n :class=\"{\n 'q-option_selected': isSelected,\n "
},
{
"path": "src/qComponents/QOption/src/q-option.scss",
"chars": 2183,
"preview": ".q-option {\n --option-background-color-base: var(--color-tertiary-gray-light);\n --option-background-color-hover: var(-"
},
{
"path": "src/qComponents/QPagination/index.js",
"chars": 161,
"preview": "import QPagination from './src/QPagination.vue';\n\nQPagination.install = Vue => {\n Vue.component(QPagination.name, QPagi"
},
{
"path": "src/qComponents/QPagination/src/QPagination.vue",
"chars": 5636,
"preview": "<template>\n <div\n class=\"q-pagination\"\n :class=\"{\n 'q-pagination_disabled': disabled\n }\"\n >\n <button\n"
},
{
"path": "src/qComponents/QPagination/src/q-pagination.scss",
"chars": 2545,
"preview": ".q-pagination {\n --pagination-color-base: var(--color-primary-blue);\n --pagination-color-hover: var(--color-primary-bl"
},
{
"path": "src/qComponents/QPopover/QPopover.test.js",
"chars": 2756,
"preview": "import Component from './index';\n\ndescribe('QPopover', () => {\n const options = {\n slots: {\n reference: '<butto"
},
{
"path": "src/qComponents/QPopover/__snapshots__/QPopover.test.js.snap",
"chars": 720,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QPopover data should match snapshot 1`] = `\nObject {\n \"isPopoverSh"
},
{
"path": "src/qComponents/QPopover/index.js",
"chars": 177,
"preview": "import QPopover from './src/QPopover.vue';\n\n/* istanbul ignore next */\nQPopover.install = function(Vue) {\n Vue.componen"
},
{
"path": "src/qComponents/QPopover/src/QPopover.vue",
"chars": 7019,
"preview": "<template>\n <component :is=\"tagName\">\n <transition\n :name=\"transition\"\n @after-leave=\"destroy\"\n >\n "
},
{
"path": "src/qComponents/QPopover/src/q-popover.scss",
"chars": 1044,
"preview": ".q-popover {\n display: flex;\n align-items: flex-start;\n min-width: 320px;\n max-width: 552px;\n padding: 24px;\n text"
},
{
"path": "src/qComponents/QRadio/QRadio.test.js",
"chars": 203,
"preview": "import Component from './src/QRadio';\n\ndescribe('QRadio', () => {\n it('should match snapshot', () => {\n const { elem"
},
{
"path": "src/qComponents/QRadio/__snapshots__/QRadio.test.js.snap",
"chars": 447,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QRadio should match snapshot 1`] = `\n<label\n class=\"q-radio\"\n rol"
},
{
"path": "src/qComponents/QRadio/index.js",
"chars": 127,
"preview": "import QRadio from './src/QRadio';\n\nQRadio.install = Vue => {\n Vue.component(QRadio.name, QRadio);\n};\n\nexport default Q"
},
{
"path": "src/qComponents/QRadio/src/QRadio.vue",
"chars": 2563,
"preview": "<template>\n <label\n class=\"q-radio\"\n :class=\"wrapClass\"\n role=\"radio\"\n :aria-checked=\"isChecked\"\n :aria-"
},
{
"path": "src/qComponents/QRadio/src/q-radio.scss",
"chars": 3307,
"preview": ".q-radio {\n --radio-color-base: var(--color-primary-black);\n --radio-color-disabled: rgba(var(--color-rgb-gray), 0.64)"
},
{
"path": "src/qComponents/QRadioGroup/QRadioGroup.test.js",
"chars": 213,
"preview": "import Component from './src/QRadioGroup';\n\ndescribe('QRadioGroup', () => {\n it('should match snapshot', () => {\n co"
},
{
"path": "src/qComponents/QRadioGroup/__snapshots__/QRadioGroup.test.js.snap",
"chars": 173,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QRadioGroup should match snapshot 1`] = `\n<div\n class=\"q-radio-gro"
},
{
"path": "src/qComponents/QRadioGroup/index.js",
"chars": 157,
"preview": "import QRadioGroup from './src/QRadioGroup';\n\nQRadioGroup.install = Vue => {\n Vue.component(QRadioGroup.name, QRadioGro"
},
{
"path": "src/qComponents/QRadioGroup/src/QRadioGroup.vue",
"chars": 2529,
"preview": "<template>\n <component\n :is=\"tag\"\n class=\"q-radio-group\"\n :class=\"`q-radio-group_${direction}`\"\n role=\"radi"
},
{
"path": "src/qComponents/QRadioGroup/src/q-radio-group.scss",
"chars": 297,
"preview": ".q-radio-group {\n display: flex;\n align-items: flex-start;\n font-size: 0;\n line-height: 1;\n\n &_vertical {\n flex-"
},
{
"path": "src/qComponents/QRow/QRow.test.js",
"chars": 205,
"preview": "import Component from './src/QRow';\n\ndescribe('QRow', () => {\n it('should match snapshot', async () => {\n const { el"
},
{
"path": "src/qComponents/QRow/__snapshots__/QRow.test.js.snap",
"chars": 115,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`QRow should match snapshot 1`] = `\n<div\n class=\"q-row\"\n/>\n`;\n"
},
{
"path": "src/qComponents/QRow/index.js",
"chars": 115,
"preview": "import QRow from './src/QRow';\n\nQRow.install = Vue => {\n Vue.component(QRow.name, QRow);\n};\n\nexport default QRow;\n"
},
{
"path": "src/qComponents/QRow/src/QRow.vue",
"chars": 1067,
"preview": "<template>\n <component\n :is=\"tag\"\n class=\"q-row\"\n :class=\"classes\"\n >\n <slot />\n </component>\n</template>"
},
{
"path": "src/qComponents/QRow/src/q-row.scss",
"chars": 576,
"preview": "$align-h: (\n start: flex-start,\n end: flex-end,\n center: center,\n between: space-between,\n around: space-around\n);\n"
},
{
"path": "src/qComponents/QScrollbar/index.js",
"chars": 185,
"preview": "import QScrollbar from './src/QScrollbar';\n\n/* istanbul ignore next */\nQScrollbar.install = function(Vue) {\n Vue.compon"
},
{
"path": "src/qComponents/QScrollbar/src/QBar.vue",
"chars": 3207,
"preview": "<template>\n <div\n class=\"q-scrollbar__bar\"\n :class=\"classes\"\n @mousedown=\"handleTrackerClick\"\n >\n <div\n "
},
{
"path": "src/qComponents/QScrollbar/src/QScrollbar.vue",
"chars": 3798,
"preview": "<template>\n <div :class=\"classes\">\n <div\n ref=\"wrap\"\n class=\"q-scrollbar__wrap\"\n :class=\"wrapClasses\""
},
{
"path": "src/qComponents/QScrollbar/src/q-scrollbar.scss",
"chars": 1631,
"preview": ".q-scrollbar {\n position: relative;\n height: 100%;\n overflow: hidden;\n\n &_visible,\n &:hover,\n &:active,\n &:focus "
},
{
"path": "src/qComponents/QScrollbar/src/util.js",
"chars": 716,
"preview": "export const BAR_MAP = {\n vertical: {\n offset: 'offsetHeight',\n scroll: 'scrollTop',\n scrollSize: 'scrollHeigh"
},
{
"path": "src/qComponents/QSelect/index.js",
"chars": 144,
"preview": "import QSelect from './src/QSelect.vue';\n\nQSelect.install = function(Vue) {\n Vue.component(QSelect.name, QSelect);\n};\n\n"
},
{
"path": "src/qComponents/QSelect/src/QSelect.vue",
"chars": 16715,
"preview": "<template>\n <div\n ref=\"reference\"\n v-click-outside=\"handleOutsideClick\"\n class=\"q-select\"\n @click=\"toggleMe"
},
{
"path": "src/qComponents/QSelect/src/QSelectDropdown.vue",
"chars": 5548,
"preview": "<template>\n <div\n class=\"q-select-dropdown\"\n :class=\"{\n 'q-select-dropdown_multiple': multiple,\n 'q-sel"
},
{
"path": "src/qComponents/QSelect/src/QSelectTags.vue",
"chars": 2011,
"preview": "<template>\n <div\n class=\"q-select-tags\"\n :class=\"{\n 'q-select-tags_filterable': filterable,\n 'q-select-"
},
{
"path": "src/qComponents/QSelect/src/q-select-dropdown.scss",
"chars": 629,
"preview": ".q-select-dropdown {\n --dropdown-max-height: 204px;\n --dropdown-box-shadow-base: var(--box-shadow-primary);\n --dropdo"
},
{
"path": "src/qComponents/QSelect/src/q-select-tags.scss",
"chars": 879,
"preview": ".q-select-tags {\n --field-color-base: var(--color-primary-black);\n\n display: flex;\n flex-wrap: wrap;\n align-items: c"
},
{
"path": "src/qComponents/QSelect/src/q-select.scss",
"chars": 356,
"preview": "@import './q-select-tags';\n@import './q-select-dropdown';\n\n.q-select {\n position: relative;\n display: inline-flex;\n w"
},
{
"path": "src/qComponents/QSlider/index.js",
"chars": 144,
"preview": "import QSlider from './src/QSlider.vue';\n\nQSlider.install = function(Vue) {\n Vue.component(QSlider.name, QSlider);\n};\n\n"
},
{
"path": "src/qComponents/QSlider/src/QSlider.vue",
"chars": 8736,
"preview": "<template>\n <div\n class=\"q-slider\"\n :class=\"wrapperClasses\"\n :style=\"wrapperStyles\"\n >\n <div\n ref=\"pa"
},
{
"path": "src/qComponents/QSlider/src/components/QSliderBar/index.vue",
"chars": 1193,
"preview": "<template>\n <div\n class=\"q-slider-bar\"\n :style=\"barStyle\"\n />\n</template>\n\n<script>\nexport default {\n name: 'QS"
},
{
"path": "src/qComponents/QSlider/src/components/QSliderButton/index.vue",
"chars": 5321,
"preview": "<template>\n <button\n type=\"button\"\n :tabindex=\"tabIndex\"\n class=\"q-slider-button\"\n :class=\"btnClasses\"\n "
},
{
"path": "src/qComponents/QSlider/src/components/QSliderCaptions/index.vue",
"chars": 1621,
"preview": "<template>\n <div class=\"q-slider-captions\">\n <button\n v-for=\"(caption, index) in captionsList\"\n :key=\"inde"
},
{
"path": "src/qComponents/QSlider/src/components/QSliderSteps/index.vue",
"chars": 1045,
"preview": "<template>\n <div class=\"q-slider-steps\">\n <div\n v-for=\"(stepItem, key) in stepsList\"\n :key=\"key\"\n cla"
},
{
"path": "src/qComponents/QSlider/src/q-slider.scss",
"chars": 196,
"preview": "@import './styles/q-slider.scss';\n@import './styles/q-slider-bar.scss';\n@import './styles/q-slider-button.scss';\n@import"
},
{
"path": "src/qComponents/QSlider/src/styles/q-slider-bar.scss",
"chars": 145,
"preview": ".q-slider-bar {\n position: absolute;\n z-index: 1;\n height: 8px;\n background-color: rgba(var(--color-rgb-red), 0.32);"
},
{
"path": "src/qComponents/QSlider/src/styles/q-slider-button.scss",
"chars": 1361,
"preview": ".q-slider-button {\n position: absolute;\n top: 50%;\n z-index: 2;\n display: flex;\n justify-content: center;\n align-i"
},
{
"path": "src/qComponents/QSlider/src/styles/q-slider-captions.scss",
"chars": 492,
"preview": ".q-slider-captions {\n position: relative;\n width: 100%;\n height: 20px;\n margin-top: 8px;\n\n &__btn {\n position: a"
},
{
"path": "src/qComponents/QSlider/src/styles/q-slider-steps.scss",
"chars": 443,
"preview": ".q-slider-steps {\n position: relative;\n z-index: 1;\n width: 100%;\n height: 100%;\n overflow: hidden;\n\n &__step {\n "
},
{
"path": "src/qComponents/QSlider/src/styles/q-slider.scss",
"chars": 1939,
"preview": ".q-slider {\n --box-shadow-line: inset 1px 1px 2px rgba(var(--color-rgb-blue), 0.2),\n inset -1px -1px 1px rgba(var(--"
},
{
"path": "src/qComponents/QTabPane/QTabPane.test.js",
"chars": 310,
"preview": "import Component from './src/QTabPane';\n\ndescribe('QTabPane', () => {\n it('should match snapshot', async () => {\n co"
}
]
// ... and 122 more files (download for full content)
About this extraction
This page contains the full source code of the Qvant-lab/qui GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 322 files (590.1 KB), approximately 165.2k tokens, and a symbol index with 145 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.