├── .editorconfig
├── .eslintrc.js
├── .github
├── dependabot.yml
└── workflows
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .nvmrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── lerna.json
├── netlify.toml
├── package-lock.json
├── package.json
├── packages
├── chusho
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── babel.config.js
│ ├── cypress.config.ts
│ ├── cypress
│ │ ├── .gitignore
│ │ └── support
│ │ │ ├── commands.js
│ │ │ ├── component-index.html
│ │ │ ├── component.ts
│ │ │ └── e2e.js
│ ├── index.html
│ ├── lib
│ │ ├── chusho.spec.js
│ │ ├── chusho.ts
│ │ ├── components
│ │ │ ├── CAlert
│ │ │ │ ├── CAlert.cy.tsx
│ │ │ │ ├── CAlert.ts
│ │ │ │ └── index.ts
│ │ │ ├── CBtn
│ │ │ │ ├── CBtn.cy.tsx
│ │ │ │ ├── CBtn.ts
│ │ │ │ └── index.ts
│ │ │ ├── CCheckbox
│ │ │ │ ├── CCheckbox.cy.tsx
│ │ │ │ ├── CCheckbox.ts
│ │ │ │ └── index.ts
│ │ │ ├── CCollapse
│ │ │ │ ├── CCollapse.cy.tsx
│ │ │ │ ├── CCollapse.ts
│ │ │ │ ├── CCollapseBtn.ts
│ │ │ │ ├── CCollapseContent.ts
│ │ │ │ └── index.ts
│ │ │ ├── CDialog
│ │ │ │ ├── CDialog.cy.tsx
│ │ │ │ ├── CDialog.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── utils.ts
│ │ │ ├── CFormGroup
│ │ │ │ ├── CFormGroup.cy.tsx
│ │ │ │ ├── CFormGroup.ts
│ │ │ │ └── index.ts
│ │ │ ├── CIcon
│ │ │ │ ├── CIcon.cy.tsx
│ │ │ │ ├── CIcon.ts
│ │ │ │ └── index.ts
│ │ │ ├── CLabel
│ │ │ │ ├── CLabel.cy.tsx
│ │ │ │ ├── CLabel.ts
│ │ │ │ └── index.ts
│ │ │ ├── CMenu
│ │ │ │ ├── CMenu.cy.tsx
│ │ │ │ ├── CMenu.ts
│ │ │ │ ├── CMenuBtn.ts
│ │ │ │ ├── CMenuItem.ts
│ │ │ │ ├── CMenuLink.ts
│ │ │ │ ├── CMenuList.ts
│ │ │ │ ├── CMenuSeparator.ts
│ │ │ │ └── index.ts
│ │ │ ├── CPicture
│ │ │ │ ├── CPicture.cy.tsx
│ │ │ │ ├── CPicture.ts
│ │ │ │ └── index.ts
│ │ │ ├── CRadio
│ │ │ │ ├── CRadio.cy.tsx
│ │ │ │ ├── CRadio.ts
│ │ │ │ └── index.ts
│ │ │ ├── CSelect
│ │ │ │ ├── CSelect.cy.tsx
│ │ │ │ ├── CSelect.ts
│ │ │ │ ├── CSelectBtn.ts
│ │ │ │ ├── CSelectGroup.ts
│ │ │ │ ├── CSelectGroupLabel.ts
│ │ │ │ ├── CSelectOption.ts
│ │ │ │ ├── CSelectOptions.ts
│ │ │ │ └── index.ts
│ │ │ ├── CTabs
│ │ │ │ ├── CTab.ts
│ │ │ │ ├── CTabList.ts
│ │ │ │ ├── CTabPanel.ts
│ │ │ │ ├── CTabPanels.ts
│ │ │ │ ├── CTabs.cy.tsx
│ │ │ │ ├── CTabs.ts
│ │ │ │ └── index.ts
│ │ │ ├── CTextField
│ │ │ │ ├── CTextField.cy.tsx
│ │ │ │ ├── CTextField.ts
│ │ │ │ └── index.ts
│ │ │ ├── CTextarea
│ │ │ │ ├── CTextarea.cy.tsx
│ │ │ │ ├── CTextarea.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ └── mixins
│ │ │ │ ├── componentMixin.ts
│ │ │ │ ├── fieldMixin.ts
│ │ │ │ ├── textFieldMixin.ts
│ │ │ │ └── transitionMixin.ts
│ │ ├── composables
│ │ │ ├── useActiveElement.spec.js
│ │ │ ├── useActiveElement.ts
│ │ │ ├── useCachedUid.spec.js
│ │ │ ├── useCachedUid.ts
│ │ │ ├── useComponentConfig.ts
│ │ │ ├── useFormGroup.spec.js
│ │ │ ├── useFormGroup.ts
│ │ │ ├── useInteractiveList.spec.js
│ │ │ ├── useInteractiveList.ts
│ │ │ ├── useInteractiveListItem.spec.js
│ │ │ ├── useInteractiveListItem.ts
│ │ │ ├── useKeyboardListNavigation.spec.js
│ │ │ ├── useKeyboardListNavigation.ts
│ │ │ ├── usePopup.spec.js
│ │ │ ├── usePopup.ts
│ │ │ ├── usePopupBtn.spec.js
│ │ │ ├── usePopupBtn.ts
│ │ │ ├── usePopupTarget.spec.js
│ │ │ └── usePopupTarget.ts
│ │ ├── directives
│ │ │ ├── clickOutside
│ │ │ │ ├── clickOutside.spec.js
│ │ │ │ ├── clickOutside.ts
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── types
│ │ │ ├── index.ts
│ │ │ └── utils.ts
│ │ └── utils
│ │ │ ├── __mocks__
│ │ │ └── uid.ts
│ │ │ ├── arrays.ts
│ │ │ ├── components.ts
│ │ │ ├── debounce.ts
│ │ │ ├── debug.ts
│ │ │ ├── keyboard.ts
│ │ │ ├── objects.ts
│ │ │ ├── ssr.ts
│ │ │ ├── tests
│ │ │ ├── arrays.spec.js
│ │ │ ├── components.spec.js
│ │ │ ├── debounce.spec.js
│ │ │ ├── keyboard.spec.js
│ │ │ ├── objects.spec.js
│ │ │ └── uid.spec.js
│ │ │ └── uid.ts
│ ├── nyc.config.js
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ │ ├── favicon.ico
│ │ ├── highlightWorker.js
│ │ └── icons.svg
│ ├── server.js
│ ├── src
│ │ ├── App.vue
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ ├── building.jpg
│ │ │ │ ├── building.webp
│ │ │ │ ├── building@2x.jpg
│ │ │ │ ├── building@2x.webp
│ │ │ │ └── index.d.ts
│ │ │ └── tailwind.css
│ │ ├── chusho.config.ts
│ │ ├── components
│ │ │ ├── Examples.vue
│ │ │ ├── Playground.vue
│ │ │ └── examples
│ │ │ │ ├── components
│ │ │ │ ├── alert
│ │ │ │ │ └── Default.vue
│ │ │ │ ├── btn
│ │ │ │ │ ├── AsLink.vue
│ │ │ │ │ ├── AsRouterLink.vue
│ │ │ │ │ ├── Bare.vue
│ │ │ │ │ ├── Default.vue
│ │ │ │ │ ├── Disabled.vue
│ │ │ │ │ ├── TypeSubmit.vue
│ │ │ │ │ └── WithVariant.vue
│ │ │ │ ├── checkbox
│ │ │ │ │ └── Controlled.vue
│ │ │ │ ├── collapse
│ │ │ │ │ ├── Controlled.vue
│ │ │ │ │ └── Default.vue
│ │ │ │ ├── dialog
│ │ │ │ │ ├── Default.vue
│ │ │ │ │ ├── DynamicContent.vue
│ │ │ │ │ ├── Nested.vue
│ │ │ │ │ ├── OpenByDefault.vue
│ │ │ │ │ └── WithTransition.vue
│ │ │ │ ├── formGroup
│ │ │ │ │ ├── AsDiv.vue
│ │ │ │ │ ├── MultipleNested.vue
│ │ │ │ │ ├── Renderless.vue
│ │ │ │ │ ├── WithHelp.vue
│ │ │ │ │ └── WithSelect.vue
│ │ │ │ ├── icon
│ │ │ │ │ ├── CustomScale.vue
│ │ │ │ │ ├── Default.vue
│ │ │ │ │ └── WithAlternateText.vue
│ │ │ │ ├── label
│ │ │ │ │ ├── Default.vue
│ │ │ │ │ ├── WithCheckbox.vue
│ │ │ │ │ └── WithTextField.vue
│ │ │ │ ├── menu
│ │ │ │ │ ├── Controlled.vue
│ │ │ │ │ ├── Default.vue
│ │ │ │ │ ├── Disabled.vue
│ │ │ │ │ ├── MenuLink.vue
│ │ │ │ │ ├── MultiSelectable.vue
│ │ │ │ │ ├── MultiSelectableObject.vue
│ │ │ │ │ ├── RouterLink.vue
│ │ │ │ │ ├── Selectable.vue
│ │ │ │ │ └── SelectableObject.vue
│ │ │ │ ├── picture
│ │ │ │ │ ├── Default.vue
│ │ │ │ │ └── WithSources.vue
│ │ │ │ ├── radio
│ │ │ │ │ └── Controlled.vue
│ │ │ │ ├── select
│ │ │ │ │ ├── Default.vue
│ │ │ │ │ ├── Disabled.vue
│ │ │ │ │ ├── OptionsGroup.vue
│ │ │ │ │ └── WithValidation.vue
│ │ │ │ ├── tabs
│ │ │ │ │ ├── Controlled.vue
│ │ │ │ │ ├── Default.vue
│ │ │ │ │ ├── Dynamic.vue
│ │ │ │ │ └── OverrideStyle.vue
│ │ │ │ ├── textField
│ │ │ │ │ ├── Controlled.vue
│ │ │ │ │ └── WithValidation.vue
│ │ │ │ └── textarea
│ │ │ │ │ └── Controlled.vue
│ │ │ │ └── directives
│ │ │ │ └── click-outside
│ │ │ │ ├── Default.vue
│ │ │ │ └── IgnoringElements.vue
│ │ ├── entry-client.js
│ │ ├── entry-server.js
│ │ ├── main.js
│ │ └── router.js
│ ├── tailwind.config.js
│ ├── test
│ │ ├── setup.js
│ │ └── utils.js
│ ├── tsconfig.base.json
│ ├── tsconfig.json
│ ├── tsconfig.prod.json
│ ├── types
│ │ └── vue-shims.d.ts
│ ├── vite.config.js
│ └── vitest.config.js
└── docs
│ ├── .vitepress
│ ├── config.js
│ └── theme
│ │ ├── custom.css
│ │ └── index.js
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── assets
│ └── showcase.css
│ ├── chusho.config.ts
│ ├── components
│ ├── ComponentSpecs.vue
│ ├── Docgen.vue
│ ├── Example
│ │ ├── Checkbox.vue
│ │ ├── Dialog.vue
│ │ ├── Radio.vue
│ │ └── Select.vue
│ ├── PropsTable.vue
│ └── Showcase.vue
│ ├── guide
│ ├── browsers-support.md
│ ├── components
│ │ ├── alert.md
│ │ ├── button.md
│ │ ├── checkbox.md
│ │ ├── collapse.md
│ │ ├── dialog.md
│ │ ├── formgroup.md
│ │ ├── icon.md
│ │ ├── index.md
│ │ ├── label.md
│ │ ├── menu.md
│ │ ├── picture.md
│ │ ├── radio.md
│ │ ├── select.md
│ │ ├── tabs.md
│ │ ├── textarea.md
│ │ └── textfield.md
│ ├── config.md
│ ├── directives
│ │ ├── click-outside.md
│ │ └── index.md
│ ├── index.md
│ ├── styling-components.md
│ └── using-components.md
│ ├── index.md
│ ├── package.json
│ ├── plugins
│ └── docGen.js
│ ├── postcss.config.js
│ ├── public
│ ├── favicon.ico
│ ├── icons.svg
│ ├── logo-dark.svg
│ ├── logo.svg
│ └── showcase.js
│ ├── tailwind.config.js
│ └── vite.config.js
├── prettier.config.js
└── vetur.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | indent_size = 4
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | extends: [
7 | 'plugin:vue/vue3-recommended',
8 | 'eslint:recommended',
9 | 'plugin:@typescript-eslint/recommended',
10 | 'plugin:prettier/recommended',
11 | 'prettier',
12 | ],
13 | parser: 'vue-eslint-parser',
14 | parserOptions: {
15 | parser: '@typescript-eslint/parser',
16 | },
17 | plugins: ['@typescript-eslint'],
18 | rules: {
19 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
21 | '@typescript-eslint/explicit-module-boundary-types': 0,
22 | 'vue/multi-word-component-names': 0,
23 | },
24 | overrides: [
25 | {
26 | files: ['**/*.{js,vue}'],
27 | rules: {
28 | '@typescript-eslint/no-var-requires': 0,
29 | '@typescript-eslint/no-non-null-assertion': 0,
30 | },
31 | },
32 | {
33 | files: ['**/*.spec.{j,t}s', '**/test/**/*', '**/__mocks__/**'],
34 | globals: {
35 | vi: true,
36 | describe: true,
37 | it: true,
38 | expect: true,
39 | beforeEach: true,
40 | afterEach: true,
41 | beforeAll: true,
42 | afterAll: true,
43 | },
44 | },
45 | {
46 | files: ['**/cypress/**/*', '**/*.cy.tsx'],
47 | plugins: ['cypress'],
48 | env: {
49 | mocha: true,
50 | 'cypress/globals': true,
51 | },
52 | rules: {
53 | strict: 'off',
54 | 'vue/one-component-per-file': 'off',
55 | },
56 | },
57 | ],
58 | };
59 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: npm
5 | directory: '/'
6 | schedule:
7 | interval: 'weekly'
8 | day: 'wednesday'
9 | time: '06:00'
10 | timezone: 'Europe/Zurich'
11 | versioning-strategy: 'increase'
12 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | bump:
7 | description: 'Version bump'
8 | required: false
9 | preid:
10 | description: 'Pre-version ID'
11 | required: true
12 | default: 'beta'
13 |
14 | jobs:
15 | release:
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - name: Checkout code
20 | uses: actions/checkout@master
21 | with:
22 | fetch-depth: '0'
23 | token: ${{ secrets.ADMIN_TOKEN }}
24 |
25 | - name: Use Node.js
26 | uses: actions/setup-node@v1
27 | with:
28 | node-version: '16.x'
29 | registry-url: 'https://registry.npmjs.org'
30 |
31 | - name: Set Git identity
32 | run: |
33 | git config user.name github-actions
34 | git config user.email github-actions@github.com
35 |
36 | - name: Install dependencies
37 | run: npm ci
38 |
39 | - name: Create release
40 | run: npx lerna version ${{ github.event.inputs.bump }} --conventional-commits --create-release github --yes --preid ${{ github.event.inputs.preid }}
41 | env:
42 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 |
44 | - name: Publish release
45 | run: npx lerna publish from-git --yes --no-verify-access
46 | env:
47 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
48 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v3
18 | - name: Setup Node.js
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: '16.x'
22 | - name: Install dependencies
23 | run: npm ci
24 | - name: Run tests
25 | run: npm test
26 | env:
27 | CI: true
28 | - name: Upload coverage to Codecov
29 | uses: codecov/codecov-action@v3
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | **/dist/**/*
4 | packages/docs/.vitepress/cache
5 | packages/docs/.vitepress/dist
6 |
7 | # local env files
8 | .env.local
9 | .env.*.local
10 |
11 | # Log files
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Project setup
4 |
5 | The Node.js version required is specified in `/.nvmrc`. The project use npm workspaces to manage dependencies.
6 |
7 | Start by cloning the repository, then install the dependencies:
8 |
9 | ```sh
10 | npm install
11 | ```
12 |
13 | ## Working on the library
14 |
15 | If you want to make changes to the library, go to the Chūshō package directory:
16 |
17 | ```sh
18 | cd packages/chusho
19 | ```
20 |
21 | Then run the playground:
22 |
23 | ```sh
24 | npm start
25 | ```
26 |
27 | You’re ready to make changes in the library and view them live in your favorite browser at [localhost:3000](http://localhost:3000).
28 |
29 | The library code is in `lib` while the playground and examples are in `src`.
30 |
31 | ## Working on the docs
32 |
33 | To make changes in the documentation, go to the docs package:
34 |
35 | ```sh
36 | cd packages/docs
37 | ```
38 |
39 | Then start the dev server:
40 |
41 | ```sh
42 | npm start
43 | ```
44 |
45 | You can now see your changes live at [localhost:8080](http://localhost:8080).
46 |
47 | ## Code quality
48 |
49 | tl;dr From the root directory, you can run all the tests in a single command with:
50 |
51 | ```
52 | npm test
53 | ```
54 |
55 | ### Code styling
56 |
57 | Code styling is enforced by EsLint & Prettier. The following commands should be run in the root directory.
58 |
59 | ```bash
60 | npm run validate
61 | ```
62 |
63 | Autofix as many offenses as possible:
64 |
65 | ```bash
66 | npm run format
67 | ```
68 |
69 | ### Unit tests
70 |
71 | Unit tests are located in the [chusho](https://github.com/liip/chusho/tree/main/packages/chusho/) package, the following commands should therefor be run in the `packages/chusho` directory.
72 |
73 | To run the unit test suites:
74 |
75 | ```bash
76 | npm run test:unit
77 | ```
78 |
79 | ### End-to-end tests
80 |
81 | End-to-end tests are located in the [chusho](https://github.com/liip/chusho/tree/main/packages/chusho/) package, the following commands should therefor be run in the `packages/chusho` directory.
82 |
83 | To start the playground server and run the suite in headless mode once, use:
84 |
85 | ```bash
86 | npm run test:e2e
87 | ```
88 |
89 | You can also run Cypress in interactive mode while developing. This requires you to have started the playground server in a separate process (see [Working on the library](#working-on-the-library) above), after which you can run:
90 |
91 | ```bash
92 | npm run test:e2e:dev
93 | ```
94 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Liip AG
4 |
5 | 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:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | 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.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/chusho)
2 | [](https://github.com/liip/chusho/actions)
3 | [](https://bundlephobia.com/result?p=chusho)
4 | [](https://codecov.io/gh/liip/chusho)
5 | [](https://github.com/liip/chusho/blob/main/LICENSE.md)
6 |
7 | # Chūshō
8 |
9 | A library of bare & accessible components and tools for Vue.js 3
10 |
11 | 👉️ [Documentation](https://www.chusho.dev/guide/)
12 |
13 | ## Packages
14 |
15 | - [chusho](https://github.com/liip/chusho/tree/main/packages/chusho/): the library core source code and a playground to develop and execute tests
16 | - [docs](https://github.com/liip/chusho/tree/main/packages/docs/): Chūshō’s documentation website
17 |
18 | ## Contributing
19 |
20 | See [contributing guide](https://github.com/liip/chusho/blob/main/CONTRIBUTING.md)
21 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.6.1",
3 | "packages": ["packages/*"],
4 | "useWorkspaces": true
5 | }
6 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | publish = "packages/docs/.vitepress/dist"
3 | command = "npm run build"
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "private": true,
4 | "workspaces": [
5 | "packages/*"
6 | ],
7 | "scripts": {
8 | "start": "cd packages/chusho && npm start",
9 | "test": "npm run validate && lerna run --parallel test",
10 | "build:chusho": "cd packages/chusho && npm run build",
11 | "build:docs": "cd packages/docs && npm run build",
12 | "build": "npm run build:chusho && npm run build:docs",
13 | "validate": "eslint . --ignore-path .gitignore --ext .js,.ts,.tsx,.vue",
14 | "format": "npm run validate -- --fix"
15 | },
16 | "devDependencies": {
17 | "@trivago/prettier-plugin-sort-imports": "^4.0.0",
18 | "@typescript-eslint/eslint-plugin": "^5.47.0",
19 | "@typescript-eslint/parser": "^5.45.0",
20 | "eslint": "^8.28.0",
21 | "eslint-config-prettier": "^8.5.0",
22 | "eslint-plugin-cypress": "^2.12.1",
23 | "eslint-plugin-prettier": "^4.2.1",
24 | "eslint-plugin-vue": "^9.8.0",
25 | "lerna": "^6.5.1",
26 | "prettier": "^2.8.0",
27 | "typescript": "^4.9.5"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/chusho/.gitignore:
--------------------------------------------------------------------------------
1 | /tests/e2e/videos
2 | /tests/e2e/screenshots
3 | /coverage
4 | /.nyc_output
5 |
--------------------------------------------------------------------------------
/packages/chusho/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/chusho)
2 | [](https://github.com/liip/chusho/actions)
3 | [](https://bundlephobia.com/result?p=chusho)
4 | [](https://codecov.io/gh/liip/chusho)
5 | [](https://github.com/liip/chusho/blob/main/LICENSE.md)
6 |
7 | # Chūshō
8 |
9 | A library of bare & accessible components and tools for Vue.js 3
10 |
11 | 👉️ [Documentation](https://www.chusho.dev/guide/)
12 |
--------------------------------------------------------------------------------
/packages/chusho/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | useBuiltIns: 'usage',
7 | corejs: 3,
8 | },
9 | ],
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/packages/chusho/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import coverage from '@cypress/code-coverage/task';
2 | import { defineConfig } from 'cypress';
3 |
4 | export default defineConfig({
5 | video: false,
6 |
7 | component: {
8 | viewportWidth: 800,
9 | viewportHeight: 600,
10 |
11 | devServer: {
12 | framework: 'vue',
13 | bundler: 'vite',
14 | },
15 |
16 | setupNodeEvents(on, config) {
17 | coverage(on, config);
18 |
19 | return config;
20 | },
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/packages/chusho/cypress/.gitignore:
--------------------------------------------------------------------------------
1 | /videos
2 | /screenshots
3 |
--------------------------------------------------------------------------------
/packages/chusho/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // https://on.cypress.io/custom-commands
3 | // ***********************************************
4 |
5 | Cypress.Commands.add('visitComponent', (path) => {
6 | cy.visit(`/examples/components/${path}`);
7 |
8 | cy.get('html').should('have.attr', 'data-test-state', 'interactive');
9 | });
10 |
--------------------------------------------------------------------------------
/packages/chusho/cypress/support/component-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Components App
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/chusho/cypress/support/component.ts:
--------------------------------------------------------------------------------
1 | import '@cypress/code-coverage/support';
2 | import { VueWrapper } from '@vue/test-utils';
3 | import 'cypress-real-events/support';
4 | import { mount } from 'cypress/vue';
5 | import { reactive } from 'vue';
6 |
7 | import '../../src/assets/tailwind.css';
8 | import chushoConfig from '../../src/chusho.config';
9 | import './commands';
10 |
11 | declare global {
12 | // eslint-disable-next-line @typescript-eslint/no-namespace
13 | namespace Cypress {
14 | interface Chainable {
15 | mount(
16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
17 | component: any,
18 | options?: Parameters[1]
19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
20 | ): Chainable;
21 |
22 | getWrapper(): Chainable;
23 | }
24 | }
25 | }
26 |
27 | Cypress.Commands.add('mount', (component, options = {}) => {
28 | options.extensions = options.extensions || {};
29 | options.extensions.components = options.extensions.components || {};
30 |
31 | options.extensions.provide = options.extensions.provide || {};
32 | options.extensions.provide.$chusho = reactive({
33 | options: chushoConfig,
34 | openDialogs: [],
35 | });
36 |
37 | return mount(component, options).then(({ wrapper }) => {
38 | return cy.wrap(wrapper).as('vue');
39 | });
40 | });
41 |
42 | Cypress.Commands.add('getWrapper', () => {
43 | return cy.get('@vue').then((wrapper) => {
44 | return cy.wrap(wrapper) as unknown as VueWrapper;
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/packages/chusho/cypress/support/e2e.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 | import './commands';
16 |
--------------------------------------------------------------------------------
/packages/chusho/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Chūshō Dev Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/packages/chusho/lib/chusho.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable vue/one-component-per-file */
2 | import { createApp } from 'vue';
3 |
4 | import * as components from './components';
5 |
6 | import * as directives from './directives';
7 |
8 | import * as main from './chusho';
9 |
10 | describe('Chūshō', () => {
11 | it('provides the result of user options merged with default options to the app', () => {
12 | const app = createApp({
13 | template: '',
14 | });
15 | app.use(main.default, {
16 | components: {
17 | btn: {
18 | class: 'foo',
19 | },
20 | },
21 | });
22 | expect(app._context.provides.$chusho).toBe(main.$chusho);
23 | expect(main.$chusho).toEqual({
24 | options: {
25 | components: {
26 | btn: {
27 | class: 'foo',
28 | },
29 | },
30 | rtl: expect.any(Function),
31 | },
32 | openDialogs: [],
33 | });
34 | });
35 |
36 | it('defineConfig returns the config', () => {
37 | const c = {
38 | components: {
39 | btn: {
40 | class: 'foo',
41 | },
42 | },
43 | };
44 |
45 | const config = main.defineConfig(c);
46 |
47 | expect(config).toEqual(c);
48 | });
49 |
50 | it('set direction based on document dir attribute', () => {
51 | expect(main.$chusho.options.rtl()).toEqual(false);
52 | document.dir = 'rtl';
53 | expect(main.$chusho.options.rtl()).toEqual(true);
54 | });
55 |
56 | it('exports an object of components', () => {
57 | expect(main.components).toEqual(components);
58 | });
59 |
60 | it('exports components individually', () => {
61 | expect(main).toMatchObject(components);
62 | });
63 |
64 | it('exports an object of directives', () => {
65 | expect(main.directives).toEqual(directives);
66 | });
67 |
68 | it('exports directives individually', () => {
69 | expect(main).toMatchObject(directives);
70 | });
71 |
72 | it('exports some utils', () => {
73 | expect(main.mergeDeep).toEqual(expect.any(Function));
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/packages/chusho/lib/chusho.ts:
--------------------------------------------------------------------------------
1 | import { App, Plugin, reactive } from 'vue';
2 |
3 | import { ChushoOptions, ChushoUserOptions, DollarChusho } from './types';
4 |
5 | import { mergeDeep } from './utils/objects';
6 |
7 | const defaultOptions: ChushoOptions = {
8 | rtl: function () {
9 | return document && document.dir === 'rtl';
10 | },
11 | components: {},
12 | };
13 |
14 | export function defineConfig(options: ChushoUserOptions): ChushoUserOptions {
15 | return options;
16 | }
17 |
18 | export const $chusho: DollarChusho = reactive({
19 | options: defaultOptions,
20 | openDialogs: [],
21 | });
22 |
23 | const Chusho: Plugin = {
24 | install: function (app: App, userOptions?: ChushoUserOptions) {
25 | const options = mergeDeep(defaultOptions, userOptions) as ChushoOptions;
26 | $chusho.options = options;
27 |
28 | app.provide('$chusho', $chusho);
29 | },
30 | };
31 |
32 | export { mergeDeep };
33 | export * as components from './components';
34 | export * as directives from './directives';
35 | export * from './components';
36 | export * from './directives';
37 | export type { ChushoUserOptions } from './types';
38 | export default Chusho;
39 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CAlert/CAlert.cy.tsx:
--------------------------------------------------------------------------------
1 | import { ChushoUserOptions } from '../../types';
2 |
3 | import { CAlert } from '.';
4 |
5 | describe('CAlert', () => {
6 | it('applies local and global classes', () => {
7 | cy.mount(
8 |
9 | Message
10 | ,
11 | {
12 | global: {
13 | provide: {
14 | $chusho: {
15 | options: {
16 | components: {
17 | alert: {
18 | class({ variant }) {
19 | return [
20 | 'config-alert',
21 | { 'config-alert-error': variant?.error },
22 | ];
23 | },
24 | },
25 | },
26 | } as ChushoUserOptions,
27 | },
28 | },
29 | },
30 | }
31 | );
32 |
33 | cy.get('[data-test="alert"]').should(
34 | 'have.class',
35 | 'alert config-alert config-alert-error'
36 | );
37 |
38 | cy.getWrapper().then((wrapper) => {
39 | wrapper.setProps({ bare: true });
40 |
41 | cy.get('[data-test="alert"]').should(
42 | 'not.have.class',
43 | 'config-alert config-alert-error'
44 | );
45 | });
46 | });
47 |
48 | it('applies the right attributes', () => {
49 | cy.mount(Message);
50 |
51 | cy.get('[data-test="alert"]').should('have.attr', 'role', 'alert');
52 | });
53 |
54 | it('renders the content', () => {
55 | cy.mount(Message);
56 |
57 | cy.get('[data-test="alert"]').should('contain', 'Message');
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CAlert/CAlert.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 |
7 | import { generateConfigClass } from '../../utils/components';
8 |
9 | export default defineComponent({
10 | name: 'CAlert',
11 |
12 | mixins: [componentMixin],
13 |
14 | setup() {
15 | return {
16 | config: useComponentConfig('alert'),
17 | };
18 | },
19 |
20 | render() {
21 | const elementProps: Record = {
22 | role: 'alert',
23 | ...generateConfigClass(this.config?.class, this.$props),
24 | };
25 |
26 | return h('div', mergeProps(this.$attrs, elementProps), this.$slots);
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CAlert/index.ts:
--------------------------------------------------------------------------------
1 | import CAlert from './CAlert';
2 |
3 | export { CAlert };
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CBtn/index.ts:
--------------------------------------------------------------------------------
1 | import CBtn from './CBtn';
2 |
3 | export { CBtn };
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CCheckbox/CCheckbox.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 | import fieldMixin from '../mixins/fieldMixin';
5 |
6 | import useComponentConfig from '../../composables/useComponentConfig';
7 | import useFormGroup from '../../composables/useFormGroup';
8 |
9 | import { ALL_TYPES, generateConfigClass } from '../../utils/components';
10 |
11 | export default defineComponent({
12 | name: 'CCheckbox',
13 |
14 | mixins: [componentMixin, fieldMixin],
15 |
16 | props: {
17 | /**
18 | * Bind the Checkbox state with the parent component.
19 | * @type {any}
20 | */
21 | modelValue: {
22 | type: ALL_TYPES,
23 | default: null,
24 | },
25 | /**
26 | * Value set when the checkbox is checked.
27 | * @type {any}
28 | */
29 | trueValue: {
30 | type: ALL_TYPES,
31 | default: true,
32 | },
33 | /**
34 | * Value set when the checkbox is unchecked.
35 | * @type {any}
36 | */
37 | falseValue: {
38 | type: ALL_TYPES,
39 | default: false,
40 | },
41 | },
42 |
43 | emits: [
44 | /**
45 | * Emitted when the checkbox checked state changes.
46 | * @arg {any} modelValue `trueValue` or `falseValue` depending on the checkbox state.
47 | */
48 | 'update:modelValue',
49 | ],
50 |
51 | setup(props) {
52 | const { formGroup, flags } = useFormGroup(props, ['required', 'disabled']);
53 |
54 | return {
55 | config: useComponentConfig('checkbox'),
56 | formGroup,
57 | flags,
58 | };
59 | },
60 |
61 | render() {
62 | const checked = this.modelValue === this.trueValue;
63 | const elementProps: Record = {
64 | ...generateConfigClass(this.config?.class, {
65 | ...this.$props,
66 | ...this.flags,
67 | checked,
68 | }),
69 | type: 'checkbox',
70 | checked,
71 | id: this.$attrs.id ?? this.formGroup?.ids.field,
72 | onChange: (e: Event) => {
73 | const checked = (e.target as HTMLInputElement).checked;
74 | this.$emit(
75 | 'update:modelValue',
76 | checked ? this.trueValue : this.falseValue
77 | );
78 | },
79 | };
80 |
81 | return h(
82 | 'input',
83 | mergeProps(this.$attrs, elementProps, this.flags),
84 | this.$slots
85 | );
86 | },
87 | });
88 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CCheckbox/index.ts:
--------------------------------------------------------------------------------
1 | import CCheckbox from './CCheckbox';
2 |
3 | export { CCheckbox };
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CCollapse/CCollapse.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 | import usePopup from '../../composables/usePopup';
7 |
8 | import { generateConfigClass } from '../../utils/components';
9 |
10 | export default defineComponent({
11 | name: 'CCollapse',
12 |
13 | mixins: [componentMixin],
14 |
15 | props: {
16 | /**
17 | * Optionally bind the Collapse opening state with the parent component.
18 | */
19 | modelValue: {
20 | type: Boolean,
21 | default: false,
22 | },
23 | },
24 |
25 | emits: [
26 | /**
27 | * Emitted when the collapse open state changes.
28 | * @arg {boolean} modelValue Whether the collapse is open or not
29 | */
30 | 'update:modelValue',
31 | ],
32 |
33 | setup(props) {
34 | const popup = usePopup({
35 | closeOnClickOutside: false,
36 | expanded: props.modelValue,
37 | expandedPropName: 'modelValue',
38 | });
39 |
40 | return {
41 | config: useComponentConfig('collapse'),
42 | popup,
43 | };
44 | },
45 |
46 | /**
47 | * @slot
48 | * @binding {boolean} active `true` when collapse is open
49 | */
50 | render() {
51 | const isActive = this.popup?.expanded.value;
52 | const elementProps: Record = {
53 | ...generateConfigClass(this.config?.class, {
54 | ...this.$props,
55 | active: isActive,
56 | }),
57 | ...this.popup.attrs,
58 | };
59 |
60 | return h(
61 | 'div',
62 | mergeProps(this.$attrs, elementProps),
63 | this.$slots?.default?.({ active: isActive })
64 | );
65 | },
66 | });
67 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CCollapse/CCollapseBtn.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 | import usePopupBtn from '../../composables/usePopupBtn';
7 |
8 | import { generateConfigClass } from '../../utils/components';
9 |
10 | import { CBtn } from '../CBtn';
11 |
12 | export default defineComponent({
13 | name: 'CCollapseBtn',
14 |
15 | mixins: [componentMixin],
16 |
17 | setup() {
18 | const popupBtn = usePopupBtn();
19 |
20 | return {
21 | config: useComponentConfig('collapseBtn'),
22 | popupBtn,
23 | };
24 | },
25 |
26 | render() {
27 | const active = this.popupBtn.popup.expanded.value;
28 | const elementProps: Record = {
29 | ref: this.popupBtn.ref,
30 | ...this.popupBtn.attrs,
31 | onClick: this.popupBtn.events.onClick,
32 | ...generateConfigClass(this.config?.class, {
33 | ...this.$props,
34 | active,
35 | }),
36 | bare: true,
37 | active,
38 | };
39 |
40 | return h(
41 | CBtn,
42 | mergeProps(this.$attrs, this.$props, elementProps),
43 | this.$slots
44 | );
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CCollapse/CCollapseContent.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, inject, mergeProps } from 'vue';
2 |
3 | import { DollarChusho } from '../../types';
4 |
5 | import componentMixin from '../mixins/componentMixin';
6 | import transitionMixin from '../mixins/transitionMixin';
7 |
8 | import useComponentConfig from '../../composables/useComponentConfig';
9 | import usePopupTarget from '../../composables/usePopupTarget';
10 |
11 | import {
12 | generateConfigClass,
13 | renderWithTransition,
14 | } from '../../utils/components';
15 |
16 | export default defineComponent({
17 | name: 'CCollapseContent',
18 |
19 | mixins: [componentMixin, transitionMixin],
20 |
21 | setup() {
22 | const chusho = inject('$chusho', null);
23 | const popupTarget = usePopupTarget();
24 | const popup = popupTarget.popup;
25 |
26 | return {
27 | config: useComponentConfig('collapseContent'),
28 | chusho,
29 | popupTarget,
30 | popup,
31 | };
32 | },
33 |
34 | methods: {
35 | renderContent() {
36 | const elementProps: Record = {
37 | ...this.popupTarget.attrs,
38 | ...generateConfigClass(
39 | this.chusho?.options?.components?.collapseContent?.class,
40 | this.$props
41 | ),
42 | };
43 |
44 | return this.popup.renderPopup(() =>
45 | h('div', mergeProps(this.$attrs, elementProps), this.$slots)
46 | );
47 | },
48 | },
49 |
50 | render() {
51 | return renderWithTransition(
52 | this.renderContent,
53 | this.transition,
54 | this.config?.transition
55 | );
56 | },
57 | });
58 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CCollapse/index.ts:
--------------------------------------------------------------------------------
1 | import CCollapse from './CCollapse';
2 | import CCollapseBtn from './CCollapseBtn';
3 | import CCollapseContent from './CCollapseContent';
4 |
5 | export { CCollapse, CCollapseBtn, CCollapseContent };
6 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CDialog/index.ts:
--------------------------------------------------------------------------------
1 | import CDialog from './CDialog';
2 |
3 | export { CDialog };
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CDialog/utils.ts:
--------------------------------------------------------------------------------
1 | export const PORTAL_ID = 'chusho-dialogs-portal';
2 |
3 | export function createPortalIfNotExists(): void {
4 | if (!document.getElementById(PORTAL_ID)) {
5 | const portalNode = document.createElement('div');
6 | portalNode.setAttribute('id', PORTAL_ID);
7 | document.body.insertAdjacentElement('beforeend', portalNode);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CFormGroup/index.ts:
--------------------------------------------------------------------------------
1 | import CFormGroup, { FormGroup, FormGroupSymbol } from './CFormGroup';
2 |
3 | export { CFormGroup, FormGroupSymbol };
4 |
5 | export type { FormGroup };
6 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CIcon/CIcon.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useCachedUid from '../../composables/useCachedUid';
6 | import useComponentConfig from '../../composables/useComponentConfig';
7 |
8 | import { generateConfigClass } from '../../utils/components';
9 |
10 | export default defineComponent({
11 | name: 'CIcon',
12 |
13 | mixins: [componentMixin],
14 |
15 | props: {
16 | /**
17 | * The id of the symbol (icon) to display from the sprite.
18 | */
19 | id: {
20 | type: String,
21 | required: true,
22 | },
23 |
24 | /**
25 | * Multiply the width/height defined in the config to change the icon display size.
26 | */
27 | scale: {
28 | type: Number,
29 | default: 1,
30 | },
31 |
32 | /**
33 | * Provides an alternate text to describe the icon meaning in the context its used.
34 | * In cases where an icon would be used without any label close by, this is important
35 | * to provide for accessibility.
36 | *
37 | * Example: imagine a lonely trash icon within a button dedicated to delete an article,
38 | * this prop should be set to a value like "Delete article".
39 | */
40 | alt: {
41 | type: String,
42 | default: null,
43 | },
44 | },
45 |
46 | setup() {
47 | return {
48 | config: useComponentConfig('icon'),
49 | uid: useCachedUid('chusho-icon'),
50 | };
51 | },
52 |
53 | render() {
54 | const elementProps: Record = mergeProps(this.$attrs, {
55 | focusable: 'false',
56 | width: `${(this.config?.width || 24) * this.scale}`,
57 | height: `${(this.config?.height || 24) * this.scale}`,
58 | ...generateConfigClass(this.config?.class, this.$props),
59 | ...this.uid.cacheAttrs,
60 | });
61 |
62 | const children = [
63 | h('use', {
64 | key: this.id,
65 | 'xlink:href': `${this.config?.spriteUrl || ''}#${this.id}`,
66 | }),
67 | ];
68 |
69 | if (this.alt) {
70 | elementProps['aria-labelledby'] = this.uid.id.value;
71 | children.unshift(
72 | h(
73 | 'title',
74 | {
75 | id: this.uid.id.value,
76 | },
77 | this.alt
78 | )
79 | );
80 | } else {
81 | elementProps['aria-hidden'] = true;
82 | }
83 |
84 | return h('svg', elementProps, children);
85 | },
86 | });
87 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CIcon/index.ts:
--------------------------------------------------------------------------------
1 | import CIcon from './CIcon';
2 |
3 | export { CIcon };
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CLabel/CLabel.cy.tsx:
--------------------------------------------------------------------------------
1 | import { ChushoUserOptions } from '../../types';
2 |
3 | import { CLabel } from '.';
4 | import { CFormGroup } from '../CFormGroup';
5 | import { CTextField } from '../CTextField';
6 |
7 | describe('CLabel', () => {
8 | it('applies local and global classes', () => {
9 | cy.mount(
10 |
11 | Label
12 | ,
13 | {
14 | global: {
15 | provide: {
16 | $chusho: {
17 | options: {
18 | components: {
19 | label: {
20 | class: 'config-label',
21 | },
22 | },
23 | } as ChushoUserOptions,
24 | },
25 | },
26 | },
27 | }
28 | );
29 |
30 | cy.get('[data-test="label"]').should('have.class', 'label config-label');
31 |
32 | cy.getWrapper().then((wrapper) => {
33 | wrapper.setProps({ bare: true });
34 |
35 | cy.get('[data-test="label"]').should('not.have.class', 'config-label');
36 | });
37 | });
38 |
39 | it('renders the default slot', () => {
40 | cy.mount(Label);
41 |
42 | cy.get('[data-test="label"]').should('contain', 'Label');
43 | });
44 |
45 | describe('in a CFormGroup', () => {
46 | it('inherits `id` and `for` attributes', () => {
47 | cy.mount(
48 |
49 | Label
50 |
51 |
52 | );
53 |
54 | cy.get('[data-test="label"]')
55 | .should('have.attr', 'id')
56 | .and('match', /^chusho-label-/);
57 |
58 | cy.get('[data-test="field"]').then(([field]) => {
59 | const id = field.getAttribute('id');
60 | cy.get(`[data-test="label"]`).should('have.attr', 'for', id);
61 | });
62 | });
63 |
64 | it('keeps its own ID when specified', () => {
65 | cy.mount(
66 |
67 |
68 | Label
69 |
70 |
71 | );
72 |
73 | cy.get('[data-test="label"]').should('have.attr', 'id', 'custom');
74 | });
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CLabel/CLabel.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, inject, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 |
7 | import { generateConfigClass } from '../../utils/components';
8 |
9 | import { FormGroupSymbol } from '../CFormGroup/CFormGroup';
10 |
11 | export default defineComponent({
12 | name: 'CLabel',
13 |
14 | mixins: [componentMixin],
15 |
16 | setup() {
17 | return {
18 | config: useComponentConfig('label'),
19 | formGroup: inject(FormGroupSymbol, null),
20 | };
21 | },
22 |
23 | render() {
24 | const elementProps: Record = {
25 | ...generateConfigClass(this.config?.class, this.$props),
26 | id: this.$attrs.id ?? this.formGroup?.ids.label,
27 | for: this.$attrs.for ?? this.formGroup?.ids.field,
28 | };
29 |
30 | return h('label', mergeProps(this.$attrs, elementProps), this.$slots);
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CLabel/index.ts:
--------------------------------------------------------------------------------
1 | import CLabel from './CLabel';
2 |
3 | export { CLabel };
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CMenu/CMenuBtn.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 | import usePopupBtn from '../../composables/usePopupBtn';
7 |
8 | import { generateConfigClass } from '../../utils/components';
9 |
10 | import { CBtn } from '../CBtn';
11 |
12 | export default defineComponent({
13 | name: 'CMenuBtn',
14 |
15 | mixins: [componentMixin],
16 |
17 | setup() {
18 | return {
19 | config: useComponentConfig('menuBtn'),
20 | popupBtn: usePopupBtn(),
21 | };
22 | },
23 |
24 | render() {
25 | const active = this.popupBtn.popup.expanded.value;
26 | const elementProps: Record = {
27 | ref: this.popupBtn.ref,
28 | ...this.popupBtn.attrs,
29 | ...this.popupBtn.events,
30 | ...generateConfigClass(this.config?.class, {
31 | ...this.$props,
32 | disabled: this.popupBtn.attrs.disabled,
33 | active,
34 | }),
35 | bare: true,
36 | active,
37 | };
38 |
39 | return h(
40 | CBtn,
41 | mergeProps(this.$attrs, this.$props, elementProps),
42 | this.$slots
43 | );
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CMenu/CMenuItem.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, inject, mergeProps, toRef } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 | import useInteractiveListItem, {
7 | InteractiveListItemRoles,
8 | } from '../../composables/useInteractiveListItem';
9 | import { UsePopupSymbol } from '../../composables/usePopup';
10 |
11 | import { ALL_TYPES, generateConfigClass } from '../../utils/components';
12 |
13 | export default defineComponent({
14 | name: 'CMenuItem',
15 |
16 | mixins: [componentMixin],
17 |
18 | props: {
19 | /**
20 | * The value used when this item is selected.
21 | *
22 | * This will set the item role to `menuitemradio`, or `menuitemcheckbox` when CMenu is `multiple`. Otherwise, the role will be `menuitem`.
23 | *
24 | * @type {any}
25 | */
26 | value: {
27 | type: ALL_TYPES,
28 | // `null` is considered an acceptable value
29 | default: undefined,
30 | },
31 | /**
32 | * Prevent selecting this item while still displaying it.
33 | */
34 | disabled: {
35 | type: Boolean,
36 | default: false,
37 | },
38 | },
39 |
40 | emits: {
41 | /**
42 | * When the item is clicked or selected with Enter/Space; only if it’s not disabled.
43 | * @param {object} payload
44 | * @param {boolean} `payload.selected` Whether the item is selected or not, only when the `value` prop is set.
45 | * @param {any} `payload.value` The value of the item, only when the `value` prop is set.
46 | */
47 | select: null,
48 | },
49 |
50 | setup(props) {
51 | const popup = inject(UsePopupSymbol);
52 | const interactiveListItem = useInteractiveListItem({
53 | value: toRef(props, 'value'),
54 | disabled: toRef(props, 'disabled'),
55 | onSelect: ({ role }) => {
56 | role === InteractiveListItemRoles.menuitem && popup?.collapse();
57 | },
58 | });
59 |
60 | return {
61 | config: useComponentConfig('menuItem'),
62 | interactiveListItem,
63 | };
64 | },
65 |
66 | /**
67 | * @slot
68 | * @binding {boolean} selected `true` when the item is selected
69 | */
70 | render() {
71 | const elementProps: Record = {
72 | ref: this.interactiveListItem.itemRef,
73 | ...this.interactiveListItem.attrs,
74 | ...this.interactiveListItem.events,
75 | ...generateConfigClass(this.config?.class, {
76 | ...this.$props,
77 | role: this.interactiveListItem.attrs.role,
78 | selected: this.interactiveListItem.selected.value,
79 | }),
80 | };
81 |
82 | return h(
83 | 'li',
84 | mergeProps(this.$attrs, elementProps),
85 | this.$slots?.default?.({ selected: this.interactiveListItem.selected })
86 | );
87 | },
88 | });
89 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CMenu/CMenuLink.ts:
--------------------------------------------------------------------------------
1 | import { PropType, defineComponent, h, inject, mergeProps } from 'vue';
2 | import { RouteLocationRaw } from 'vue-router';
3 |
4 | import componentMixin from '../mixins/componentMixin';
5 |
6 | import useComponentConfig from '../../composables/useComponentConfig';
7 | import useInteractiveListItem from '../../composables/useInteractiveListItem';
8 | import { UsePopupSymbol } from '../../composables/usePopup';
9 |
10 | import { generateConfigClass } from '../../utils/components';
11 |
12 | import { CBtn } from '../CBtn';
13 |
14 | export default defineComponent({
15 | name: 'CMenuLink',
16 |
17 | mixins: [componentMixin],
18 |
19 | props: {
20 | /**
21 | * Prevent clicking the link while still displaying it.
22 | */
23 | disabled: {
24 | type: Boolean,
25 | default: false,
26 | },
27 | /**
28 | * The link to open when this item is selected.
29 | */
30 | href: {
31 | type: String,
32 | default: null,
33 | },
34 | /**
35 | * The route to open when this item is selected.
36 | */
37 | to: {
38 | type: [String, Object] as PropType,
39 | default: null,
40 | },
41 | },
42 |
43 | emits: {
44 | /**
45 | * When the item is clicked or selected with Enter/Space; only if it’s not disabled.
46 | */
47 | select: null,
48 | },
49 |
50 | setup(props) {
51 | const popup = inject(UsePopupSymbol);
52 |
53 | const interactiveListItem = useInteractiveListItem({
54 | disabled: props.disabled,
55 | onSelect: () => popup?.collapse(),
56 | });
57 |
58 | return {
59 | config: useComponentConfig('menuLink'),
60 | interactiveListItem,
61 | };
62 | },
63 |
64 | render() {
65 | const elementProps: Record = {
66 | role: 'none',
67 | };
68 |
69 | const linkProps: Record = {
70 | ref: this.interactiveListItem.itemRef,
71 | href: this.href,
72 | to: this.to,
73 | ...this.interactiveListItem.attrs,
74 | ...this.interactiveListItem.events,
75 | ...generateConfigClass(this.config?.class, this.$props),
76 | bare: true,
77 | };
78 |
79 | return h('li', elementProps, [
80 | h(CBtn, mergeProps(this.$attrs, linkProps), this.$slots),
81 | ]);
82 | },
83 | });
84 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CMenu/CMenuList.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, inject, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 | import transitionMixin from '../mixins/transitionMixin';
5 |
6 | import useComponentConfig from '../../composables/useComponentConfig';
7 | import { UseInteractiveListSymbol } from '../../composables/useInteractiveList';
8 | import usePopupTarget from '../../composables/usePopupTarget';
9 |
10 | import {
11 | generateConfigClass,
12 | renderWithTransition,
13 | } from '../../utils/components';
14 |
15 | export default defineComponent({
16 | name: 'CMenuList',
17 |
18 | mixins: [componentMixin, transitionMixin],
19 |
20 | setup() {
21 | const popupTarget = usePopupTarget();
22 | const popup = popupTarget.popup;
23 | const interactiveList = inject(UseInteractiveListSymbol);
24 |
25 | if (!interactiveList) {
26 | throw new Error('CMenuList must be used inside a CMenu');
27 | }
28 |
29 | return {
30 | config: useComponentConfig('menuList'),
31 | popupTarget,
32 | popup,
33 | interactiveList,
34 | };
35 | },
36 |
37 | methods: {
38 | renderDropdown() {
39 | const elementProps: Record = {
40 | ...generateConfigClass(this.config?.class, this.$props),
41 | ...this.interactiveList.attrs,
42 | ...this.popupTarget.attrs,
43 | ...mergeProps(this.popupTarget.events, this.interactiveList.events),
44 | };
45 |
46 | return this.popup.renderPopup(() =>
47 | h('ul', mergeProps(this.$attrs, elementProps), this.$slots)
48 | );
49 | },
50 | },
51 |
52 | render() {
53 | return renderWithTransition(
54 | this.renderDropdown,
55 | this.transition,
56 | this.config?.transition
57 | );
58 | },
59 | });
60 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CMenu/CMenuSeparator.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 |
7 | import { generateConfigClass } from '../../utils/components';
8 |
9 | export default defineComponent({
10 | name: 'CMenuSeparator',
11 |
12 | mixins: [componentMixin],
13 |
14 | setup() {
15 | return {
16 | config: useComponentConfig('menuSeparator'),
17 | };
18 | },
19 |
20 | render() {
21 | const elementProps: Record = {
22 | role: 'separator',
23 | ...generateConfigClass(this.config?.class, this.$props),
24 | };
25 |
26 | return h('li', mergeProps(this.$attrs, elementProps));
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CMenu/index.ts:
--------------------------------------------------------------------------------
1 | export { default as CMenu } from './CMenu';
2 | export { default as CMenuBtn } from './CMenuBtn';
3 | export { default as CMenuList } from './CMenuList';
4 | export { default as CMenuLink } from './CMenuLink';
5 | export { default as CMenuItem } from './CMenuItem';
6 | export { default as CMenuSeparator } from './CMenuSeparator';
7 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CPicture/CPicture.cy.tsx:
--------------------------------------------------------------------------------
1 | import { ChushoUserOptions } from '../../types';
2 |
3 | import { CPicture } from '.';
4 | import building from '../../../src/assets/images/building.jpg';
5 | import buildingWebp from '../../../src/assets/images/building.webp';
6 | import building2x from '../../../src/assets/images/building@2x.jpg';
7 | import buildingWebp2x from '../../../src/assets/images/building@2x.webp';
8 |
9 | describe('CPicture', () => {
10 | it('applies local and global classes to the `img` element', () => {
11 | cy.mount(, {
12 | global: {
13 | provide: {
14 | $chusho: {
15 | options: {
16 | components: {
17 | picture: {
18 | class: 'config-picture',
19 | },
20 | },
21 | } as ChushoUserOptions,
22 | },
23 | },
24 | },
25 | });
26 |
27 | cy.get('img[data-test="picture"]').should(
28 | 'have.class',
29 | 'picture config-picture'
30 | );
31 |
32 | cy.getWrapper().then((wrapper) => {
33 | wrapper.setProps({ bare: true });
34 |
35 | cy.get('[data-test="picture"]').should(
36 | 'not.have.class',
37 | 'config-picture'
38 | );
39 | });
40 | });
41 |
42 | it('applies the right attributes', () => {
43 | cy.mount();
44 |
45 | cy.get('[data-test="picture"]').should('have.attr', 'alt', '');
46 |
47 | cy.getWrapper().then((wrapper) => {
48 | wrapper.setProps({ alt: 'Building' });
49 |
50 | cy.get('[data-test="picture"]').should('have.attr', 'alt', 'Building');
51 | });
52 | });
53 |
54 | it('renders the given sources', () => {
55 | cy.mount(
56 |
70 | );
71 |
72 | cy.get('[data-test="picture"]').should('have.attr', 'src', building);
73 | cy.get('picture source')
74 | .should('have.length', 2)
75 | .eq(0)
76 | .should('have.attr', 'srcset', `${buildingWebp2x} 2x, ${buildingWebp}`)
77 | .and('have.attr', 'type', 'image/webp');
78 | cy.get('picture source')
79 | .eq(1)
80 | .should('have.attr', 'srcset', `${building2x} 2x, ${building}`)
81 | .and('have.attr', 'type', 'image/jpeg');
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CPicture/CPicture.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PropType,
3 | SourceHTMLAttributes,
4 | defineComponent,
5 | h,
6 | mergeProps,
7 | } from 'vue';
8 |
9 | import componentMixin from '../mixins/componentMixin';
10 |
11 | import useComponentConfig from '../../composables/useComponentConfig';
12 |
13 | import { generateConfigClass } from '../../utils/components';
14 |
15 | export default defineComponent({
16 | name: 'CPicture',
17 |
18 | mixins: [componentMixin],
19 |
20 | props: {
21 | /**
22 | * Default/fallback image URL used in the `src` attribute.
23 | */
24 | src: {
25 | type: String,
26 | required: true,
27 | },
28 | /**
29 | * Alternative text description; leave empty if the image is not a key part of the content, otherwise describe what can be seen.
30 | */
31 | alt: {
32 | type: String,
33 | default: '',
34 | },
35 | /**
36 | * Generate multiple `source` elements with the given attributes.
37 | */
38 | sources: {
39 | type: Array as PropType,
40 | default: () => [],
41 | },
42 | },
43 |
44 | setup() {
45 | return {
46 | config: useComponentConfig('picture'),
47 | };
48 | },
49 |
50 | render() {
51 | const elementProps: Record = mergeProps(this.$attrs, {
52 | src: this.$props.src,
53 | alt: this.$props.alt,
54 | ...generateConfigClass(this.config?.class, this.$props),
55 | });
56 | const sources = this.$props.sources.map((source) => h('source', source));
57 |
58 | return h('picture', null, [...sources, h('img', elementProps)]);
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CPicture/index.ts:
--------------------------------------------------------------------------------
1 | import CPicture from './CPicture';
2 |
3 | export { CPicture };
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CRadio/CRadio.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 | import fieldMixin from '../mixins/fieldMixin';
5 |
6 | import useComponentConfig from '../../composables/useComponentConfig';
7 | import useFormGroup from '../../composables/useFormGroup';
8 |
9 | import { ALL_TYPES, generateConfigClass } from '../../utils/components';
10 |
11 | export default defineComponent({
12 | name: 'CRadio',
13 |
14 | mixins: [componentMixin, fieldMixin],
15 |
16 | props: {
17 | /**
18 | * Bind the Radio state with the parent component.
19 | * @type {any}
20 | */
21 | modelValue: {
22 | type: ALL_TYPES,
23 | default: null,
24 | },
25 | /**
26 | * The value to be used when the Radio is checked.
27 | * @type {any}
28 | */
29 | value: {
30 | type: ALL_TYPES,
31 | required: true,
32 | },
33 | },
34 |
35 | emits: [
36 | /**
37 | * Emitted when the selected radio changes.
38 | * @arg {any} modelValue The currently selected radio value.
39 | */
40 | 'update:modelValue',
41 | ],
42 |
43 | setup(props) {
44 | const { formGroup, flags } = useFormGroup(props, ['required', 'disabled']);
45 |
46 | return {
47 | config: useComponentConfig('radio'),
48 | formGroup,
49 | flags,
50 | };
51 | },
52 |
53 | render() {
54 | const checked = this.modelValue === this.value;
55 | const elementProps: Record = {
56 | ...generateConfigClass(this.config?.class, {
57 | ...this.$props,
58 | ...this.flags,
59 | checked,
60 | }),
61 | type: 'radio',
62 | value: this.$props.value,
63 | checked,
64 | id: this.$attrs.id ?? this.formGroup?.ids.field,
65 | onChange: () => this.$emit('update:modelValue', this.$props.value),
66 | };
67 |
68 | return h(
69 | 'input',
70 | mergeProps(this.$attrs, elementProps, this.flags),
71 | this.$slots
72 | );
73 | },
74 | });
75 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CRadio/index.ts:
--------------------------------------------------------------------------------
1 | import CRadio from './CRadio';
2 |
3 | export { CRadio };
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CSelect/CSelectBtn.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, inject, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useCachedUid from '../../composables/useCachedUid';
6 | import useComponentConfig from '../../composables/useComponentConfig';
7 | import { UsePopupSymbol } from '../../composables/usePopup';
8 | import usePopupBtn from '../../composables/usePopupBtn';
9 |
10 | import { generateConfigClass } from '../../utils/components';
11 |
12 | import { CBtn } from '../CBtn';
13 | import { FormGroupSymbol } from '../CFormGroup/CFormGroup';
14 |
15 | export default defineComponent({
16 | name: 'CSelectBtn',
17 |
18 | mixins: [componentMixin],
19 |
20 | setup() {
21 | const popup = inject(UsePopupSymbol);
22 |
23 | return {
24 | config: useComponentConfig('selectBtn'),
25 | formGroup: inject(FormGroupSymbol, null),
26 | uid: useCachedUid('chusho-select-btn'),
27 | popupBtn: usePopupBtn(),
28 | popup,
29 | };
30 | },
31 |
32 | render() {
33 | const elementProps: Record = {
34 | ...this.popupBtn.attrs,
35 | ...this.popupBtn.events,
36 | ...this.uid.cacheAttrs,
37 | ...generateConfigClass(this.config?.class, {
38 | ...this.$props,
39 | disabled: this.popupBtn?.popup.disabled.value,
40 | active: this.popupBtn?.popup.expanded.value,
41 | }),
42 | bare: true,
43 | };
44 |
45 | if (this.formGroup) {
46 | // Combine the form group label and the select btn (self) content as label for the select
47 | const id = this.$attrs.id ?? this.uid.id.value;
48 | const labels = [
49 | this.$attrs['aria-labelledby'],
50 | this.formGroup?.ids.label,
51 | id,
52 | ]
53 | .filter((s) => !!s)
54 | .join(' ');
55 |
56 | elementProps.id = id;
57 | elementProps['aria-labelledby'] = labels;
58 | }
59 |
60 | return h(
61 | h(CBtn, mergeProps(this.$attrs, this.$props, elementProps), this.$slots),
62 | // There’s already a ref from cachedUid
63 | { ref: this.popup?.btnRef }
64 | );
65 | },
66 | });
67 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CSelect/CSelectGroup.ts:
--------------------------------------------------------------------------------
1 | import { InjectionKey, defineComponent, h, mergeProps, provide } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useCachedUid, { UseCachedUid } from '../../composables/useCachedUid';
6 | import useComponentConfig from '../../composables/useComponentConfig';
7 |
8 | import { generateConfigClass } from '../../utils/components';
9 |
10 | export const SelectGroupSymbol: InjectionKey =
11 | Symbol('CSelectGroup');
12 |
13 | export interface SelectGroup {
14 | labelUid: UseCachedUid;
15 | }
16 |
17 | export default defineComponent({
18 | name: 'CSelectGroup',
19 |
20 | mixins: [componentMixin],
21 |
22 | setup() {
23 | const selectGroup: SelectGroup = {
24 | labelUid: useCachedUid('chusho-select-group-label'),
25 | };
26 |
27 | provide(SelectGroupSymbol, selectGroup);
28 |
29 | return {
30 | config: useComponentConfig('selectGroup'),
31 | selectGroup,
32 | };
33 | },
34 |
35 | render() {
36 | const elementProps: Record = {
37 | ...generateConfigClass(this.config?.class, this.$props),
38 | ...this.selectGroup.labelUid.cacheAttrs,
39 | role: 'group',
40 | 'aria-labelledby': this.selectGroup.labelUid.id.value,
41 | };
42 |
43 | return h('div', mergeProps(this.$attrs, elementProps), this.$slots);
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CSelect/CSelectGroupLabel.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, inject, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 |
7 | import { generateConfigClass } from '../../utils/components';
8 |
9 | import { SelectGroupSymbol } from './CSelectGroup';
10 |
11 | export default defineComponent({
12 | name: 'CSelectGroupLabel',
13 |
14 | mixins: [componentMixin],
15 |
16 | setup() {
17 | return {
18 | config: useComponentConfig('selectGroupLabel'),
19 | selectGroup: inject(SelectGroupSymbol),
20 | };
21 | },
22 |
23 | render() {
24 | const elementProps: Record = {
25 | ...generateConfigClass(this.config?.class, this.$props),
26 | id: this.selectGroup?.labelUid.id.value,
27 | };
28 |
29 | return h('div', mergeProps(this.$attrs, elementProps), this.$slots);
30 | },
31 | });
32 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CSelect/CSelectOption.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, inject, mergeProps, toRef } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 | import useInteractiveListItem from '../../composables/useInteractiveListItem';
7 | import { UsePopupSymbol } from '../../composables/usePopup';
8 |
9 | import { ALL_TYPES, generateConfigClass } from '../../utils/components';
10 |
11 | export default defineComponent({
12 | name: 'CSelectOption',
13 |
14 | mixins: [componentMixin],
15 |
16 | props: {
17 | /**
18 | * The value used when this option is selected.
19 | * @type {any}
20 | */
21 | value: {
22 | type: ALL_TYPES,
23 | required: true,
24 | },
25 | /**
26 | * Prevent selecting this option while still displaying it.
27 | */
28 | disabled: {
29 | type: Boolean,
30 | default: false,
31 | },
32 | },
33 |
34 | setup(props) {
35 | const popup = inject(UsePopupSymbol);
36 |
37 | const interactiveListItem = useInteractiveListItem({
38 | value: toRef(props, 'value'),
39 | disabled: toRef(props, 'disabled'),
40 | onSelect: () => {
41 | popup?.collapse();
42 | },
43 | });
44 |
45 | return {
46 | config: useComponentConfig('selectOption'),
47 | popup,
48 | interactiveListItem,
49 | };
50 | },
51 |
52 | render() {
53 | const elementProps: Record = {
54 | ref: this.interactiveListItem.itemRef,
55 | ...this.interactiveListItem.attrs,
56 | ...this.interactiveListItem.events,
57 | ...generateConfigClass(this.config?.class, {
58 | ...this.$props,
59 | selected: this.interactiveListItem.selected.value,
60 | }),
61 | };
62 |
63 | /**
64 | * @slot If no content is provided, it defaults to a non-breaking space to ensure the element is clickable.
65 | */
66 | return h(
67 | 'div',
68 | mergeProps(this.$attrs, elementProps),
69 | this.$slots.default?.() ?? ' '
70 | );
71 | },
72 | });
73 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CSelect/CSelectOptions.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, inject, mergeProps, ref } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 | import transitionMixin from '../mixins/transitionMixin';
5 |
6 | import useComponentConfig from '../../composables/useComponentConfig';
7 | import { UseInteractiveListSymbol } from '../../composables/useInteractiveList';
8 | import usePopupTarget from '../../composables/usePopupTarget';
9 |
10 | import {
11 | generateConfigClass,
12 | renderWithTransition,
13 | } from '../../utils/components';
14 |
15 | export default defineComponent({
16 | name: 'CSelectOptions',
17 |
18 | mixins: [componentMixin, transitionMixin],
19 |
20 | setup() {
21 | const el = ref();
22 | const popupTarget = usePopupTarget();
23 | const interactiveList = inject(UseInteractiveListSymbol);
24 |
25 | if (!interactiveList) {
26 | throw new Error('CSelectOptions must be used inside a CSelect');
27 | }
28 |
29 | return {
30 | config: useComponentConfig('selectOptions'),
31 | el,
32 | popupTarget,
33 | interactiveList,
34 | };
35 | },
36 |
37 | methods: {
38 | renderOptions() {
39 | const elementProps: Record = {
40 | ref: 'el',
41 | ...generateConfigClass(this.config?.class, this.$props),
42 | ...this.interactiveList.attrs,
43 | ...this.popupTarget.attrs,
44 | ...mergeProps(this.popupTarget.events, this.interactiveList.events),
45 | };
46 |
47 | return this.popupTarget.popup.renderPopup(() =>
48 | h('div', mergeProps(this.$attrs, elementProps), this.$slots)
49 | );
50 | },
51 | },
52 |
53 | render() {
54 | return renderWithTransition(
55 | this.renderOptions,
56 | this.transition,
57 | this.config?.transition
58 | );
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CSelect/index.ts:
--------------------------------------------------------------------------------
1 | import CSelect from './CSelect';
2 | import CSelectBtn from './CSelectBtn';
3 | import CSelectGroup from './CSelectGroup';
4 | import CSelectGroupLabel from './CSelectGroupLabel';
5 | import CSelectOption from './CSelectOption';
6 | import CSelectOptions from './CSelectOptions';
7 |
8 | export {
9 | CSelect,
10 | CSelectBtn,
11 | CSelectOptions,
12 | CSelectOption,
13 | CSelectGroup,
14 | CSelectGroupLabel,
15 | };
16 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CTabs/CTab.ts:
--------------------------------------------------------------------------------
1 | import { PropType, defineComponent, h, inject, mergeProps, toRef } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 | import { InteractiveItemId } from '../../composables/useInteractiveList';
7 | import useInteractiveListItem from '../../composables/useInteractiveListItem';
8 |
9 | import { generateConfigClass } from '../../utils/components';
10 |
11 | import { TabsSymbol } from './CTabs';
12 |
13 | export default defineComponent({
14 | name: 'CTab',
15 |
16 | mixins: [componentMixin],
17 |
18 | props: {
19 | /**
20 | * The id of the Tab this button should control.
21 | *
22 | * @type {string|number}
23 | */
24 | target: {
25 | type: [String, Number] as PropType,
26 | required: true,
27 | },
28 | },
29 |
30 | setup(props) {
31 | const tabs = inject(TabsSymbol);
32 | const interactiveListItem = useInteractiveListItem({
33 | id: props.target,
34 | value: toRef(props, 'target'),
35 | });
36 |
37 | return {
38 | config: useComponentConfig('tab'),
39 | tabs,
40 | interactiveListItem,
41 | };
42 | },
43 |
44 | render() {
45 | if (!this.tabs) return null;
46 |
47 | const isActive = this.interactiveListItem.selected.value;
48 | const elementProps = {
49 | ref: this.interactiveListItem.itemRef,
50 | ...this.interactiveListItem.attrs,
51 | ...this.interactiveListItem.events,
52 | type: 'button',
53 | id: `${this.tabs.uid.id.value}-tab-${this.target}`,
54 | 'aria-controls': `${this.tabs.uid.id.value}-tabpanel-${this.target}`,
55 | ...generateConfigClass(this.config?.class, {
56 | ...this.$props,
57 | active: isActive,
58 | }),
59 | };
60 |
61 | return h('button', mergeProps(this.$attrs, elementProps), this.$slots);
62 | },
63 | });
64 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CTabs/CTabList.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, inject, mergeProps } from 'vue';
2 |
3 | import { DollarChusho } from '../../types';
4 |
5 | import componentMixin from '../mixins/componentMixin';
6 |
7 | import useComponentConfig from '../../composables/useComponentConfig';
8 | import { UseInteractiveListSymbol } from '../../composables/useInteractiveList';
9 |
10 | import { generateConfigClass } from '../../utils/components';
11 |
12 | import { TabsSymbol } from './CTabs';
13 |
14 | export default defineComponent({
15 | name: 'CTabList',
16 |
17 | mixins: [componentMixin],
18 |
19 | setup() {
20 | const chusho = inject('$chusho', null);
21 | const tabs = inject(TabsSymbol);
22 | const interactiveList = inject(UseInteractiveListSymbol);
23 |
24 | if (!interactiveList) {
25 | throw new Error('CTabList must be used inside CTabs');
26 | }
27 |
28 | return {
29 | config: useComponentConfig('tabList'),
30 | chusho,
31 | tabs,
32 | interactiveList,
33 | };
34 | },
35 |
36 | render() {
37 | const elementProps: Record = {
38 | ref: 'tabList',
39 | ...this.interactiveList.attrs,
40 | ...this.interactiveList.events,
41 | ...generateConfigClass(this.config?.class, this.$props),
42 | };
43 |
44 | return h('div', mergeProps(this.$attrs, elementProps), this.$slots);
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CTabs/CTabPanel.ts:
--------------------------------------------------------------------------------
1 | import { PropType, defineComponent, h, inject, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 | import {
7 | InteractiveItemId,
8 | UseInteractiveListSymbol,
9 | } from '../../composables/useInteractiveList';
10 |
11 | import { generateConfigClass } from '../../utils/components';
12 |
13 | import { TabsSymbol } from './CTabs';
14 |
15 | export default defineComponent({
16 | name: 'CTabPanel',
17 |
18 | mixins: [componentMixin],
19 |
20 | props: {
21 | /**
22 | * A unique ID to target the panel with CTab.
23 | *
24 | * @type {string|number}
25 | */
26 | id: {
27 | type: [String, Number] as PropType,
28 | required: true,
29 | },
30 | },
31 |
32 | setup() {
33 | const tabs = inject(TabsSymbol);
34 | const interactiveList = inject(UseInteractiveListSymbol);
35 |
36 | if (!interactiveList) {
37 | throw new Error('CTabList must be used inside CTabs');
38 | }
39 |
40 | return {
41 | config: useComponentConfig('tabPanel'),
42 | tabs,
43 | interactiveList,
44 | };
45 | },
46 |
47 | render() {
48 | if (!this.tabs) return;
49 |
50 | const isActive = this.id === this.interactiveList.selection.value;
51 |
52 | if (!isActive) return null;
53 |
54 | const elementProps = {
55 | id: `${this.tabs.uid.id.value}-tabpanel-${this.id}`,
56 | role: 'tabpanel',
57 | 'aria-labelledby': `${this.tabs.uid.id.value}-tab-${this.id}`,
58 | tabindex: '0',
59 | ...generateConfigClass(this.config?.class, {
60 | ...this.$props,
61 | active: isActive,
62 | }),
63 | };
64 |
65 | return h('div', mergeProps(this.$attrs, elementProps), this.$slots);
66 | },
67 | });
68 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CTabs/CTabPanels.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 |
5 | import useComponentConfig from '../../composables/useComponentConfig';
6 |
7 | import { generateConfigClass } from '../../utils/components';
8 |
9 | export default defineComponent({
10 | name: 'CTabPanels',
11 |
12 | mixins: [componentMixin],
13 |
14 | setup() {
15 | return {
16 | config: useComponentConfig('tabPanels'),
17 | };
18 | },
19 |
20 | render() {
21 | const elementProps: Record = {
22 | ...generateConfigClass(this.config?.class, this.$props),
23 | };
24 |
25 | return h('div', mergeProps(this.$attrs, elementProps), this.$slots);
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CTabs/CTabs.ts:
--------------------------------------------------------------------------------
1 | import {
2 | InjectionKey,
3 | PropType,
4 | defineComponent,
5 | h,
6 | mergeProps,
7 | provide,
8 | watch,
9 | } from 'vue';
10 |
11 | import componentMixin from '../mixins/componentMixin';
12 |
13 | import useCachedUid, { UseCachedUid } from '../../composables/useCachedUid';
14 | import useComponentConfig from '../../composables/useComponentConfig';
15 | import useInteractiveList, {
16 | InteractiveItemId,
17 | InteractiveListRoles,
18 | } from '../../composables/useInteractiveList';
19 |
20 | import { generateConfigClass } from '../../utils/components';
21 |
22 | export const TabsSymbol: InjectionKey = Symbol('CTabs');
23 |
24 | export interface Tabs {
25 | uid: UseCachedUid;
26 | }
27 |
28 | export default defineComponent({
29 | name: 'CTabs',
30 |
31 | mixins: [componentMixin],
32 |
33 | props: {
34 | /**
35 | * Optionally bind the Tabs state with the parent component.
36 | *
37 | * @type {string|number}
38 | */
39 | modelValue: {
40 | type: [String, Number] as PropType,
41 | default: null,
42 | },
43 | /**
44 | * The id of the Tab to display by default. This value is ignored if `v-model` is used and **required** otherwise.
45 | *
46 | * @type {string|number}
47 | */
48 | defaultTab: {
49 | type: [String, Number] as PropType,
50 | default: null,
51 | },
52 | },
53 |
54 | emits: [
55 | /**
56 | * When the selected tab change.
57 | * @arg {string|number} modelValue The selected tab id.
58 | */
59 | 'update:modelValue',
60 | ],
61 |
62 | setup(props) {
63 | if (props.modelValue === null && props.defaultTab === null) {
64 | throw new Error(
65 | 'CTabs requires either a `v-model` or the `defaultTab` prop to be set.'
66 | );
67 | }
68 |
69 | const interactiveList = useInteractiveList({
70 | role: InteractiveListRoles.tablist,
71 | initialValue: props.modelValue ?? props.defaultTab ?? null,
72 | initialActiveItem: props.modelValue ?? props.defaultTab ?? null,
73 | loop: true,
74 | skipDisabled: true,
75 | autoSelect: true,
76 | });
77 |
78 | // Update selected tab when `v-model` changes
79 | watch(
80 | () => props.modelValue,
81 | (val, oldVal) => {
82 | if (val !== oldVal && ['string', 'number'].includes(typeof val)) {
83 | interactiveList.selectItem(val);
84 | }
85 | }
86 | );
87 |
88 | const tabs: Tabs = {
89 | uid: useCachedUid('chusho-tabs'),
90 | };
91 |
92 | provide(TabsSymbol, tabs);
93 |
94 | return {
95 | config: useComponentConfig('tabs'),
96 | tabs,
97 | interactiveList,
98 | };
99 | },
100 |
101 | render() {
102 | const elementProps: Record = {
103 | ...generateConfigClass(this.config?.class, this.$props),
104 | ...this.tabs.uid.cacheAttrs,
105 | };
106 |
107 | return h('div', mergeProps(this.$attrs, elementProps), this.$slots);
108 | },
109 | });
110 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CTabs/index.ts:
--------------------------------------------------------------------------------
1 | import CTab from './CTab';
2 | import CTabList from './CTabList';
3 | import CTabPanel from './CTabPanel';
4 | import CTabPanels from './CTabPanels';
5 | import CTabs from './CTabs';
6 |
7 | export { CTabs, CTabList, CTab, CTabPanels, CTabPanel };
8 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CTextField/CTextField.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 | import textFieldMixin from '../mixins/textFieldMixin';
5 |
6 | import useComponentConfig from '../../composables/useComponentConfig';
7 | import useFormGroup from '../../composables/useFormGroup';
8 |
9 | import { generateConfigClass } from '../../utils/components';
10 |
11 | export default defineComponent({
12 | name: 'CTextField',
13 |
14 | mixins: [componentMixin, textFieldMixin],
15 |
16 | props: {
17 | /**
18 | * Usual HTML input element type attribute for textual input (text, email, tel, url, …)
19 | */
20 | type: {
21 | type: String,
22 | default: 'text',
23 | },
24 | /**
25 | * Input value
26 | */
27 | modelValue: {
28 | type: [String, Number],
29 | default: null,
30 | },
31 | },
32 |
33 | emits: [
34 | /**
35 | * When the input value changes.
36 | * @arg {string} modelValue The input value.
37 | */
38 | 'update:modelValue',
39 | ],
40 |
41 | setup(props) {
42 | const { formGroup, flags } = useFormGroup(props, [
43 | 'required',
44 | 'disabled',
45 | 'readonly',
46 | ]);
47 |
48 | return {
49 | config: useComponentConfig('textField'),
50 | formGroup,
51 | flags,
52 | };
53 | },
54 |
55 | render() {
56 | const elementProps: Record = {
57 | ...generateConfigClass(this.config?.class, {
58 | ...this.$props,
59 | ...this.flags,
60 | }),
61 | type: this.type,
62 | value: this.modelValue,
63 | id: this.$attrs.id ?? this.formGroup?.ids.field,
64 | onInput: (e: KeyboardEvent) => {
65 | this.$emit('update:modelValue', (e.target as HTMLInputElement).value);
66 | },
67 | };
68 |
69 | return h('input', mergeProps(this.$attrs, elementProps, this.flags));
70 | },
71 | });
72 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CTextField/index.ts:
--------------------------------------------------------------------------------
1 | import CTextField from './CTextField';
2 |
3 | export { CTextField };
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CTextarea/CTextarea.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent, h, mergeProps } from 'vue';
2 |
3 | import componentMixin from '../mixins/componentMixin';
4 | import textFieldMixin from '../mixins/textFieldMixin';
5 |
6 | import useComponentConfig from '../../composables/useComponentConfig';
7 | import useFormGroup from '../../composables/useFormGroup';
8 |
9 | import { generateConfigClass } from '../../utils/components';
10 |
11 | export default defineComponent({
12 | name: 'CTextarea',
13 |
14 | mixins: [componentMixin, textFieldMixin],
15 |
16 | props: {
17 | /**
18 | * Textarea value
19 | */
20 | modelValue: {
21 | type: [String, Number],
22 | default: null,
23 | },
24 | },
25 |
26 | emits: [
27 | /**
28 | * When the textarea value changes.
29 | * @arg {string} modelValue The textarea value.
30 | */
31 | 'update:modelValue',
32 | ],
33 |
34 | setup(props) {
35 | const { formGroup, flags } = useFormGroup(props, [
36 | 'required',
37 | 'disabled',
38 | 'readonly',
39 | ]);
40 |
41 | return {
42 | config: useComponentConfig('textarea'),
43 | formGroup,
44 | flags,
45 | };
46 | },
47 |
48 | render() {
49 | const elementProps: Record = {
50 | ...generateConfigClass(this.config?.class, {
51 | ...this.$props,
52 | ...this.flags,
53 | }),
54 | value: this.modelValue,
55 | id: this.$attrs.id ?? this.formGroup?.ids.field,
56 | onInput: (e: KeyboardEvent) => {
57 | this.$emit('update:modelValue', (e.target as HTMLInputElement).value);
58 | },
59 | };
60 |
61 | return h('textarea', mergeProps(this.$attrs, elementProps, this.flags));
62 | },
63 | });
64 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/CTextarea/index.ts:
--------------------------------------------------------------------------------
1 | import CTextarea from './CTextarea';
2 |
3 | export { CTextarea };
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CAlert';
2 | export * from './CBtn';
3 | export * from './CCheckbox';
4 | export * from './CCollapse';
5 | export * from './CDialog';
6 | export * from './CFormGroup';
7 | export * from './CIcon';
8 | export * from './CLabel';
9 | export * from './CMenu';
10 | export * from './CPicture';
11 | export * from './CRadio';
12 | export * from './CSelect';
13 | export * from './CTabs';
14 | export * from './CTextarea';
15 | export * from './CTextField';
16 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/mixins/componentMixin.ts:
--------------------------------------------------------------------------------
1 | import { PropType, defineComponent } from 'vue';
2 |
3 | import { RawVariant } from '../../utils/components';
4 |
5 | export default defineComponent({
6 | inheritAttrs: false,
7 |
8 | props: {
9 | /**
10 | * Useful when used in the component config `class` option, to style it conditionally. See [styling components](/guide/styling-components).
11 | * @type {string|array|object}
12 | */
13 | variant: {
14 | type: [String, Array, Object] as PropType,
15 | default: undefined,
16 | },
17 |
18 | /**
19 | * Disable class inheritance from the component config. See [styling components](/guide/styling-components).
20 | */
21 | bare: {
22 | type: Boolean,
23 | default: false,
24 | },
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/mixins/fieldMixin.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue';
2 |
3 | export default defineComponent({
4 | props: {
5 | /**
6 | * Set the HTML disabled attribute
7 | */
8 | disabled: {
9 | type: Boolean,
10 | default: null,
11 | },
12 |
13 | /**
14 | * Set the HTML required attribute
15 | */
16 | required: {
17 | type: Boolean,
18 | default: null,
19 | },
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/mixins/textFieldMixin.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue';
2 |
3 | import fieldMixin from './fieldMixin';
4 |
5 | export default defineComponent({
6 | mixins: [fieldMixin],
7 |
8 | props: {
9 | /**
10 | * Set the HTML readonly attribute
11 | */
12 | readonly: {
13 | type: Boolean,
14 | default: null,
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/packages/chusho/lib/components/mixins/transitionMixin.ts:
--------------------------------------------------------------------------------
1 | import { PropType, TransitionProps, defineComponent } from 'vue';
2 |
3 | export default defineComponent({
4 | props: {
5 | /**
6 | * The object can contain any Vue built-in [transition component props](https://v3.vuejs.org/api/built-in-components.html#transition).
7 | *
8 | * For example: `{ name: "fade", mode: "out-in" }`.
9 | *
10 | * If you defined a default transition in the config and want to disable it, use `false`.
11 | */
12 | transition: {
13 | type: [Object, Boolean] as PropType,
14 | default: null,
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/useActiveElement.spec.js:
--------------------------------------------------------------------------------
1 | import useActiveElement from './useActiveElement';
2 |
3 | describe('useActiveElement', () => {
4 | let activeElement;
5 |
6 | beforeEach(() => {
7 | activeElement = useActiveElement();
8 | });
9 |
10 | it('has no element by default', () => {
11 | expect(activeElement.element.value).toBe(null);
12 | });
13 |
14 | it('saves and restores the same element', () => {
15 | const btn = document.createElement('button');
16 | document.body.appendChild(btn);
17 | btn.focus();
18 | activeElement.save();
19 |
20 | expect(activeElement.element.value).toBe(btn);
21 |
22 | btn.blur();
23 | expect(document.activeElement).toBe(document.body);
24 |
25 | activeElement.restore();
26 | expect(document.activeElement).toBe(btn);
27 | expect(activeElement.element.value).toBe(null);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/useActiveElement.ts:
--------------------------------------------------------------------------------
1 | import { Ref, computed, ref } from 'vue';
2 |
3 | import { isServer } from '../utils/ssr';
4 |
5 | /**
6 | * Generic logic to save and restore document.activeElement
7 | * (the element currently holding focus in a page)
8 | */
9 | export default function useActiveElement() {
10 | const savedElement: Ref = ref(null);
11 |
12 | function save() {
13 | if (isServer) return;
14 |
15 | savedElement.value = document?.activeElement as HTMLElement;
16 | }
17 |
18 | function restore() {
19 | if (isServer) return;
20 |
21 | if (savedElement.value?.focus) {
22 | savedElement.value.focus();
23 | }
24 | savedElement.value = null;
25 | }
26 |
27 | return {
28 | element: computed(() => savedElement.value),
29 | save,
30 | restore,
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/useCachedUid.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils';
2 | import { h } from 'vue';
3 |
4 | import { withSetup } from '../../test/utils';
5 |
6 | import useCachedUid from './useCachedUid';
7 |
8 | describe('useCachedUid', () => {
9 | it('returns a unique ID without prefix', () => {
10 | const { composable } = withSetup(() => useCachedUid());
11 |
12 | expect(composable.id.value).toBe('0');
13 | });
14 |
15 | it('returns a unique ID with prefix', () => {
16 | const { composable } = withSetup(() => useCachedUid('chusho'));
17 |
18 | expect(composable.id.value).toBe('chusho-0');
19 | });
20 |
21 | it('returns a ref and data-attribute for caching', () => {
22 | const { composable } = withSetup(() => useCachedUid('chusho'));
23 |
24 | expect(composable.cacheAttrs).toMatchInlineSnapshot(`
25 | {
26 | "data-chusho-ssr-uid": undefined,
27 | "ref": RefImpl {
28 | "__v_isRef": true,
29 | "__v_isShallow": false,
30 | "_rawValue": null,
31 | "_value": null,
32 | "dep": undefined,
33 | },
34 | }
35 | `);
36 | });
37 |
38 | it('set the uid based on the ref caching attribute on mount', () => {
39 | let uid = null;
40 |
41 | mount({
42 | setup() {
43 | uid = useCachedUid();
44 | return {
45 | uid,
46 | };
47 | },
48 | render() {
49 | return h('div', {
50 | ...this.uid.cacheAttrs,
51 | 'data-chusho-ssr-uid': 'ssr-id',
52 | });
53 | },
54 | });
55 |
56 | expect(uid.id.value).toEqual('ssr-id');
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/useCachedUid.ts:
--------------------------------------------------------------------------------
1 | import { ComponentPublicInstance, Ref, onMounted, readonly, ref } from 'vue';
2 |
3 | import { getElement } from '../utils/components';
4 | import { isServer } from '../utils/ssr';
5 | import uid from '../utils/uid';
6 |
7 | const UID_CACHE_ATTR = 'data-chusho-ssr-uid';
8 |
9 | type CacheElement = HTMLElement | SVGElement | ComponentPublicInstance | null;
10 |
11 | export interface UseCachedUid {
12 | id: Readonly[>;
13 | cacheAttrs: {
14 | ref: Ref;
15 | [UID_CACHE_ATTR]?: string;
16 | };
17 | }
18 |
19 | /**
20 | * When using SSR, we cannot just generate a random ID, or server and client values won’t match.
21 | * The idea here is to store the server generated ID on a dom node (target) and restore it when mounting the component client side.
22 | */
23 | export default function useCachedUid(prefix?: string): UseCachedUid {
24 | const id = ref(uid(prefix));
25 | const cacheElement = ref(null);
26 |
27 | onMounted(() => {
28 | if (cacheElement.value) {
29 | // It can happen that the element holding the cache reference is a Vue component
30 | // in this case we’ll use it’s root element instead ($el)
31 | const element = getElement(cacheElement);
32 | const serverId = element?.getAttribute(UID_CACHE_ATTR);
33 |
34 | if (serverId) {
35 | id.value = serverId;
36 | }
37 |
38 | /**
39 | * Remove the data attribute when we render on the client
40 | */
41 | element?.removeAttribute(UID_CACHE_ATTR);
42 | }
43 | });
44 |
45 | return {
46 | id: readonly(id),
47 | cacheAttrs: {
48 | ref: cacheElement,
49 | [UID_CACHE_ATTR]: isServer ? id.value : undefined,
50 | },
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/useComponentConfig.ts:
--------------------------------------------------------------------------------
1 | import { Ref, inject, ref, toRef } from 'vue';
2 |
3 | import { DollarChusho } from '../types';
4 |
5 | /**
6 | * Get the user config for a given component
7 | */
8 | export default function useComponentConfig<
9 | K extends keyof DollarChusho['options']['components']
10 | >(entry: K): Ref {
11 | const $chusho = inject('$chusho', null);
12 | return $chusho ? toRef($chusho.options.components, entry) : ref(undefined);
13 | }
14 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/useFormGroup.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils';
2 | import { defineComponent, h } from 'vue';
3 |
4 | import textFieldMixin from '../components/mixins/textFieldMixin';
5 |
6 | import CFormGroup from '../components/CFormGroup/CFormGroup';
7 |
8 | import useFormGroup from './useFormGroup';
9 |
10 | const ComponentWithinFormGroup = defineComponent({
11 | mixins: [textFieldMixin],
12 |
13 | setup(props) {
14 | return {
15 | formGroup: useFormGroup(props, ['required', 'disabled', 'readonly']),
16 | };
17 | },
18 |
19 | render() {
20 | return h('input');
21 | },
22 | });
23 |
24 | describe('useFormGroup', () => {
25 | let wrapper = null;
26 |
27 | beforeEach(() => {
28 | wrapper = mount(CFormGroup, {
29 | props: {
30 | required: false,
31 | disabled: false,
32 | readonly: false,
33 | },
34 | slots: {
35 | default: () => h(ComponentWithinFormGroup),
36 | },
37 | });
38 | });
39 |
40 | it('exposes the form group', () => {
41 | expect(
42 | wrapper.findComponent(ComponentWithinFormGroup).vm.formGroup
43 | ).toMatchObject({
44 | flags: {
45 | required: false,
46 | disabled: false,
47 | readonly: false,
48 | },
49 | formGroup: {
50 | // Exact value is not important, just that it's an object
51 | },
52 | });
53 | });
54 |
55 | it('has reactive flags', async () => {
56 | expect(
57 | wrapper.findComponent(ComponentWithinFormGroup).vm.formGroup.flags
58 | ).toMatchObject({
59 | required: false,
60 | disabled: false,
61 | readonly: false,
62 | });
63 |
64 | await wrapper.setProps({
65 | required: true,
66 | disabled: true,
67 | readonly: true,
68 | });
69 |
70 | expect(
71 | wrapper.findComponent(ComponentWithinFormGroup).vm.formGroup.flags
72 | ).toStrictEqual({
73 | required: true,
74 | disabled: true,
75 | readonly: true,
76 | });
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/useFormGroup.ts:
--------------------------------------------------------------------------------
1 | import { computed, inject, reactive } from 'vue';
2 |
3 | import { isNil } from '../utils/objects';
4 |
5 | import {
6 | FormGroupFlag,
7 | FormGroupSymbol,
8 | } from '../components/CFormGroup/CFormGroup';
9 |
10 | interface flags {
11 | required?: FormGroupFlag;
12 | disabled?: FormGroupFlag;
13 | readonly?: FormGroupFlag;
14 | }
15 |
16 | type flag = keyof flags;
17 |
18 | /**
19 | * Allow a child of CFormGroup to access its context
20 | */
21 | export default function useFormGroup(
22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
23 | props: Record,
24 | flagsToSet: Array
25 | ) {
26 | const formGroup = inject(FormGroupSymbol, null);
27 | const flags: flags = {};
28 |
29 | flagsToSet.forEach((flagToSet) => {
30 | if (isNil(props[flagToSet]) && formGroup) {
31 | flags[flagToSet] = formGroup[flagToSet];
32 | } else {
33 | flags[flagToSet] = computed(() => props[flagToSet]);
34 | }
35 | });
36 |
37 | return {
38 | formGroup,
39 | flags: reactive(flags),
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/useKeyboardListNavigation.ts:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue';
2 |
3 | import debounce from '../utils/debounce';
4 | import { calculateActiveIndex, getNextFocusByKey } from '../utils/keyboard';
5 |
6 | type NavigableItemData = {
7 | disabled?: boolean;
8 | text?: string;
9 | };
10 |
11 | interface NavigableItem {
12 | id: string | number;
13 | data?: NavigableItemData;
14 | }
15 |
16 | interface UseKeyboardListNavigationOptions {
17 | resolveItems: () => NavigableItem[];
18 | resolveActiveIndex: () => number;
19 | resolveDisabled: (item: NavigableItem) => boolean;
20 | loop?: boolean;
21 | }
22 |
23 | export default function useKeyboardListNavigation(
24 | handler: (e: KeyboardEvent, index: number | null) => void,
25 | {
26 | resolveItems,
27 | resolveActiveIndex,
28 | resolveDisabled,
29 | loop = false,
30 | }: UseKeyboardListNavigationOptions
31 | ): (e: KeyboardEvent) => void {
32 | const query = ref('');
33 | const searchIndex = ref(0);
34 | const prepareToResetQuery = debounce(() => (query.value = ''), 500);
35 |
36 | function findItemIndexToFocus(character: string): number | null {
37 | const items = resolveItems();
38 | const selectedItemIndex = resolveActiveIndex();
39 |
40 | if (!query.value && selectedItemIndex !== -1) {
41 | searchIndex.value = selectedItemIndex;
42 | }
43 |
44 | query.value += character.toLowerCase();
45 |
46 | prepareToResetQuery();
47 |
48 | let nextMatch = findMatchInRange(
49 | items,
50 | searchIndex.value + 1,
51 | items.length
52 | );
53 |
54 | if (!nextMatch) {
55 | nextMatch = findMatchInRange(items, 0, searchIndex.value);
56 | }
57 |
58 | return nextMatch;
59 | }
60 |
61 | function findMatchInRange(
62 | items: NavigableItem[],
63 | startIndex: number,
64 | endIndex: number
65 | ): number | null {
66 | for (let index = startIndex; index < endIndex; index++) {
67 | const item = items[index];
68 | const label = item.data?.text;
69 |
70 | if (!resolveDisabled(item) && label && label.indexOf(query.value) === 0) {
71 | return index;
72 | }
73 | }
74 | return null;
75 | }
76 |
77 | return function handleKeyboardListNavigation(e: KeyboardEvent) {
78 | const focus = getNextFocusByKey(e.key);
79 |
80 | let newIndex = null;
81 |
82 | if (focus === null) {
83 | newIndex = findItemIndexToFocus(e.key);
84 | } else {
85 | newIndex = calculateActiveIndex(
86 | focus,
87 | {
88 | resolveItems,
89 | resolveActiveIndex,
90 | resolveDisabled,
91 | },
92 | loop
93 | );
94 | }
95 |
96 | handler(e, newIndex);
97 | };
98 | }
99 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/usePopupBtn.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils';
2 | import { h, nextTick, ref } from 'vue';
3 |
4 | import usePopup, { PopupType } from './usePopup';
5 | import usePopupBtn from './usePopupBtn';
6 |
7 | let popup = null;
8 | let composable = null;
9 | let disabled = ref(false);
10 | const component = {
11 | setup() {
12 | popup = usePopup({
13 | disabled,
14 | type: PopupType.menu,
15 | });
16 |
17 | return () =>
18 | h({
19 | setup() {
20 | composable = usePopupBtn();
21 |
22 | return {
23 | composable,
24 | };
25 | },
26 |
27 | render() {
28 | return h('button', {
29 | ...this.composable.attrs,
30 | ...this.composable.events,
31 | });
32 | },
33 | });
34 | },
35 | };
36 |
37 | describe('usePopupBtn', () => {
38 | afterEach(() => {
39 | disabled.value = false;
40 | });
41 |
42 | it('provides aria-* and disabled attributes with correct values', () => {
43 | mount(component);
44 |
45 | expect(composable.attrs).toMatchInlineSnapshot(`
46 | {
47 | "aria-controls": "chusho-popup-0",
48 | "aria-expanded": "false",
49 | "aria-haspopup": "menu",
50 | "disabled": false,
51 | }
52 | `);
53 | });
54 |
55 | it('track changes in attributes', async () => {
56 | mount(component);
57 |
58 | popup.expand();
59 | await nextTick();
60 | expect(composable.attrs['aria-expanded']).toBe('true');
61 |
62 | disabled.value = true;
63 | await nextTick();
64 | expect(composable.attrs['disabled']).toBe(true);
65 | });
66 |
67 | it('toggles the popup on click', () => {
68 | const wrapper = mount(component);
69 |
70 | wrapper.find('button').trigger('click');
71 | expect(popup.expanded.value).toBe(true);
72 | expect(popup.trigger.value).toBe('Click');
73 |
74 | wrapper.find('button').trigger('click');
75 | expect(popup.expanded.value).toBe(false);
76 | expect(popup.trigger.value).toBe(null);
77 | });
78 |
79 | it.each(['ArrowDown', 'ArrowUp'])(
80 | 'expand the popup when pressing %s and updates the trigger',
81 | async (key) => {
82 | const wrapper = mount(component);
83 |
84 | await wrapper.find('button').trigger('keydown', { key });
85 | expect(popup.expanded.value).toBe(true);
86 | expect(popup.trigger.value).toBe(key);
87 | }
88 | );
89 | });
90 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/usePopupBtn.ts:
--------------------------------------------------------------------------------
1 | import { Ref, computed, inject, reactive } from 'vue';
2 |
3 | import { UsePopup, UsePopupSymbol } from './usePopup';
4 |
5 | export interface UsePopupBtn {
6 | ref: Ref;
7 | attrs: {
8 | 'aria-expanded': string;
9 | 'aria-controls': string;
10 | 'aria-haspopup'?: string;
11 | disabled: boolean;
12 | };
13 | events: {
14 | onClick: (e: MouseEvent) => void;
15 | onKeydown: (e: KeyboardEvent) => void;
16 | };
17 | popup: UsePopup;
18 | }
19 |
20 | export default function usePopupBtn(): UsePopupBtn {
21 | const popup = inject(UsePopupSymbol);
22 |
23 | if (!popup) {
24 | throw new Error('usePopupBtn must be used within usePopup');
25 | }
26 |
27 | const { expand, toggle, disabled, uid, expanded, type } = popup;
28 |
29 | function handleKeydown(e: KeyboardEvent) {
30 | if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
31 | e.preventDefault(); // Prevent scroll
32 | expand(e.key);
33 | }
34 | }
35 |
36 | function handleClick() {
37 | toggle('Click');
38 | }
39 |
40 | return {
41 | ref: popup.btnRef,
42 | attrs: reactive({
43 | 'aria-controls': uid.id,
44 | 'aria-expanded': computed(() => expanded.value.toString()),
45 | 'aria-haspopup': type,
46 | disabled,
47 | }),
48 | events: {
49 | onClick: handleClick,
50 | onKeydown: handleKeydown,
51 | },
52 | popup,
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/usePopupTarget.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils';
2 | import { h } from 'vue';
3 |
4 | import usePopup from './usePopup';
5 | import usePopupTarget from './usePopupTarget';
6 |
7 | let popup = null;
8 | let composable = null;
9 | const component = {
10 | setup() {
11 | popup = usePopup();
12 |
13 | return () =>
14 | h({
15 | setup() {
16 | composable = usePopupTarget();
17 |
18 | return {
19 | composable,
20 | };
21 | },
22 |
23 | render() {
24 | return h('div', {
25 | ...this.composable.attrs,
26 | ...this.composable.events,
27 | });
28 | },
29 | });
30 | },
31 | };
32 |
33 | describe('usePopupTarget', () => {
34 | it('provides id attribute with correct value', () => {
35 | mount(component);
36 |
37 | expect(composable.attrs.id).toBe('chusho-popup-0');
38 | });
39 |
40 | it.each(['Tab', 'Escape'])(
41 | 'collapses the popup when pressing %s',
42 | async (key) => {
43 | const wrapper = mount(component);
44 |
45 | popup.expand();
46 | expect(popup.expanded.value).toBe(true);
47 |
48 | await wrapper.trigger('keydown', { key });
49 | expect(popup.expanded.value).toBe(false);
50 | }
51 | );
52 | });
53 |
--------------------------------------------------------------------------------
/packages/chusho/lib/composables/usePopupTarget.ts:
--------------------------------------------------------------------------------
1 | import { inject, reactive } from 'vue';
2 |
3 | import { UsePopup, UsePopupSymbol } from './usePopup';
4 |
5 | export interface UsePopupTarget {
6 | attrs: {
7 | id: string;
8 | };
9 | events: {
10 | onKeydown: (e: KeyboardEvent) => void;
11 | };
12 | popup: UsePopup;
13 | }
14 |
15 | export default function usePopupTarget(): UsePopupTarget {
16 | const popup = inject(UsePopupSymbol);
17 |
18 | if (!popup) {
19 | throw new Error('usePopupTarget must be used within usePopup');
20 | }
21 |
22 | const { uid, collapse } = popup;
23 |
24 | function handleKeydown(e: KeyboardEvent) {
25 | if (e.key === 'Tab') {
26 | collapse({ restoreFocus: false });
27 | } else if (e.key === 'Escape') {
28 | collapse();
29 | }
30 | }
31 |
32 | return {
33 | attrs: reactive({ id: uid.id }),
34 | events: {
35 | onKeydown: handleKeydown,
36 | },
37 | popup,
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/packages/chusho/lib/directives/clickOutside/clickOutside.ts:
--------------------------------------------------------------------------------
1 | import { ObjectDirective } from 'vue';
2 |
3 | import { getElement } from '../../utils/components';
4 | import { warn } from '../../utils/debug';
5 |
6 | type ClickOutsideHandler = (e: MouseEvent) => void;
7 |
8 | interface ClickOutsideOptions {
9 | ignore?: Array;
10 | }
11 |
12 | type ClickOutsideBinding =
13 | | ClickOutsideHandler
14 | | { handler: ClickOutsideHandler; options: ClickOutsideOptions };
15 |
16 | const handlers = new WeakMap();
17 |
18 | function handleClick(
19 | e: MouseEvent,
20 | el: HTMLElement,
21 | handler: ClickOutsideHandler,
22 | options?: ClickOutsideOptions
23 | ) {
24 | const composedPath = e.composedPath();
25 |
26 | if (!el || el === e.target || composedPath.includes(el)) {
27 | return;
28 | }
29 |
30 | if (options?.ignore?.length) {
31 | if (
32 | options.ignore.some((target) => {
33 | const el = getElement(target);
34 | return e.target === el || composedPath.includes(el);
35 | })
36 | ) {
37 | return;
38 | }
39 | }
40 |
41 | handler(e);
42 | }
43 |
44 | export default {
45 | name: 'clickOutside',
46 |
47 | mounted(el, binding): void {
48 | handlers.set(el, (e: MouseEvent) => {
49 | if (typeof binding.value === 'function') {
50 | handleClick(e, el, binding.value);
51 | } else if (typeof binding.value?.handler === 'function') {
52 | handleClick(e, el, binding.value.handler, binding.value.options);
53 | } else {
54 | warn('clickOutside handler must be a Function.');
55 | }
56 | });
57 |
58 | document.addEventListener('click', handlers.get(el), {
59 | passive: true,
60 | });
61 | },
62 |
63 | beforeUnmount(el): void {
64 | const handler = handlers.get(el);
65 | if (handler) {
66 | document.removeEventListener('click', handler);
67 | }
68 | },
69 | } as ObjectDirective;
70 |
--------------------------------------------------------------------------------
/packages/chusho/lib/directives/clickOutside/index.ts:
--------------------------------------------------------------------------------
1 | import clickOutside from './clickOutside';
2 |
3 | export { clickOutside };
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/directives/index.ts:
--------------------------------------------------------------------------------
1 | export * from './clickOutside';
2 |
--------------------------------------------------------------------------------
/packages/chusho/lib/types/utils.ts:
--------------------------------------------------------------------------------
1 | import { Ref } from 'vue';
2 |
3 | export type MaybeRef = T | Ref;
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/utils/__mocks__/uid.ts:
--------------------------------------------------------------------------------
1 | let currentuid = 0;
2 |
3 | export default vi.fn().mockImplementation((prefix = '') => {
4 | const id = currentuid++;
5 | return prefix ? `${prefix}-${id}` : id.toString();
6 | });
7 |
8 | export function reset(): void {
9 | currentuid = 0;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/chusho/lib/utils/arrays.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Put the given value into an array if it’s not already one.
3 | */
4 | export function ensureArray(value: T | T[]): T[] {
5 | return Array.isArray(value) ? value : [value];
6 | }
7 |
8 | /**
9 | * Return the array value at the given index
10 | * Accepts negative indexes, returning the value starting from the end of the array
11 | * Returns undefined if the index is out of bounds
12 | *
13 | * Example:
14 | * 2 => [0, 1, 2, 3, 4] => 2
15 | * -2 => [0, 1, 2, 3, 4] => 3
16 | */
17 | export function getAtIndex(array: T[], index: number): T | undefined {
18 | if (!Number.isInteger(index)) {
19 | return;
20 | }
21 |
22 | if (index < 0) {
23 | index += array.length;
24 | }
25 |
26 | if (index < 0 || index >= array.length) {
27 | return;
28 | }
29 |
30 | return array[index];
31 | }
32 |
--------------------------------------------------------------------------------
/packages/chusho/lib/utils/debounce.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Debounce a function call
3 | * Inspired by https://davidwalsh.name/javascript-debounce-function
4 | */
5 | export default function debounce(
6 | fn: (...args: T) => void,
7 | wait: number,
8 | immediate = false
9 | ) {
10 | let timeout: ReturnType | null;
11 |
12 | return function caller(this: unknown, ...args: T) {
13 | const callNow = immediate && !timeout;
14 |
15 | timeout && clearTimeout(timeout);
16 |
17 | timeout = setTimeout(() => {
18 | timeout = null;
19 |
20 | if (!immediate) {
21 | fn.apply(this, args);
22 | }
23 | }, wait);
24 |
25 | if (callNow) {
26 | fn.apply(this, args);
27 | }
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/packages/chusho/lib/utils/debug.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
2 | export let warn = (msg: string): void => {};
3 |
4 | if (process.env.NODE_ENV !== 'production') {
5 | warn = (msg: string): void => {
6 | // eslint-disable-next-line no-console
7 | console?.warn(`[Chūshō warn] ${msg}`);
8 | };
9 | }
10 |
--------------------------------------------------------------------------------
/packages/chusho/lib/utils/objects.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
2 | export function isPlainObject(value: unknown): value is Record {
3 | return Object.prototype.toString.call(value) === '[object Object]';
4 | }
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
7 | export function isObject(obj: unknown): obj is Record {
8 | return obj !== null && typeof obj === 'object';
9 | }
10 |
11 | export function isPrimitive(
12 | value: unknown
13 | ): value is undefined | boolean | number | string | bigint | symbol {
14 | return [
15 | 'undefined',
16 | 'boolean',
17 | 'number',
18 | 'string',
19 | 'bigint',
20 | 'symbol',
21 | ].includes(typeof value);
22 | }
23 |
24 | export function isNil(value: unknown): value is null | undefined {
25 | return value === undefined || value === null;
26 | }
27 |
28 | export function mergeDeep(
29 | /* eslint-disable @typescript-eslint/no-explicit-any */
30 | source: Record = {},
31 | target: Record = {}
32 | ): Record {
33 | /* eslint-enable @typescript-eslint/no-explicit-any */
34 | for (const key in target) {
35 | const sourceProperty = source[key];
36 | const targetProperty = target[key];
37 |
38 | if (isObject(sourceProperty) && isObject(targetProperty)) {
39 | source[key] = mergeDeep(sourceProperty, targetProperty);
40 | continue;
41 | }
42 |
43 | source[key] = targetProperty;
44 | }
45 |
46 | return source;
47 | }
48 |
--------------------------------------------------------------------------------
/packages/chusho/lib/utils/ssr.ts:
--------------------------------------------------------------------------------
1 | export const isClient = typeof window !== 'undefined';
2 |
3 | export const isServer = !isClient;
4 |
--------------------------------------------------------------------------------
/packages/chusho/lib/utils/tests/arrays.spec.js:
--------------------------------------------------------------------------------
1 | import { ensureArray, getAtIndex } from '../arrays';
2 |
3 | describe('ensureArray', () => {
4 | it('return by reference if the provided argument is an array', () => {
5 | const array = [0, 1];
6 |
7 | expect(ensureArray(array)).toBe(array);
8 | });
9 |
10 | it('return a new array if the provided argument is not an array', () => {
11 | expect(ensureArray(0)).toEqual([0]);
12 | expect(ensureArray('foo')).toEqual(['foo']);
13 | expect(ensureArray({ foo: 'bar' })).toEqual([{ foo: 'bar' }]);
14 | expect(ensureArray({ foo: 'bar' })).toEqual([{ foo: 'bar' }]);
15 | });
16 | });
17 |
18 | describe('getAtIndex', () => {
19 | it('return the element at the provided positive index', () => {
20 | const array = ['0', '1', '2'];
21 |
22 | expect(getAtIndex(array, 0)).toEqual('0');
23 | expect(getAtIndex(array, 1)).toEqual('1');
24 | expect(getAtIndex(array, 2)).toEqual('2');
25 | });
26 |
27 | it('return the element at the provided negative index, counting from the last element in the array', () => {
28 | const array = ['0', '1', '2'];
29 |
30 | expect(getAtIndex(array, -1)).toEqual('2');
31 | expect(getAtIndex(array, -2)).toEqual('1');
32 | expect(getAtIndex(array, -3)).toEqual('0');
33 | });
34 |
35 | it('return `undefined` if index is not part of the array or not not a number', () => {
36 | const array = ['0', '1', '2'];
37 |
38 | expect(getAtIndex(array, 10)).toEqual(undefined);
39 | expect(getAtIndex(array, -10)).toEqual(undefined);
40 | expect(getAtIndex(array, NaN)).toEqual(undefined);
41 | expect(getAtIndex(array, {})).toEqual(undefined);
42 | expect(getAtIndex(array, [])).toEqual(undefined);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/packages/chusho/lib/utils/tests/debounce.spec.js:
--------------------------------------------------------------------------------
1 | import debounce from '../debounce';
2 |
3 | afterAll(() => {
4 | vi.useRealTimers();
5 | });
6 |
7 | describe('debounce', () => {
8 | const time = 500;
9 |
10 | it('call the function only once after the given duration', () => {
11 | vi.useFakeTimers();
12 |
13 | const callback = vi.fn();
14 |
15 | const debounced = debounce(callback, time);
16 |
17 | debounced();
18 | debounced();
19 |
20 | expect(callback).not.toBeCalled();
21 |
22 | vi.advanceTimersByTime(time);
23 |
24 | expect(callback).toHaveBeenCalledTimes(1);
25 | });
26 |
27 | it('recall the function only once after the time has elapsed', () => {
28 | vi.useFakeTimers();
29 |
30 | const callback = vi.fn();
31 |
32 | const debounced = debounce(callback, time);
33 |
34 | debounced();
35 | debounced();
36 |
37 | vi.advanceTimersByTime(time);
38 |
39 | debounced();
40 | debounced();
41 |
42 | expect(callback).toHaveBeenCalledTimes(1);
43 |
44 | vi.advanceTimersByTime(time);
45 |
46 | expect(callback).toHaveBeenCalledTimes(2);
47 | });
48 |
49 | it('call the function only once immediately if requested', () => {
50 | vi.useFakeTimers();
51 |
52 | const callback = vi.fn();
53 |
54 | const debounced = debounce(callback, time, true);
55 |
56 | debounced();
57 | debounced();
58 |
59 | expect(callback).toHaveBeenCalledTimes(1);
60 | });
61 |
62 | it('recall the function only once after the time has elapsed after immediate invocation ', () => {
63 | vi.useFakeTimers();
64 |
65 | const callback = vi.fn();
66 |
67 | const debounced = debounce(callback, time, true);
68 |
69 | debounced();
70 | debounced();
71 |
72 | expect(callback).toHaveBeenCalledTimes(1);
73 |
74 | vi.advanceTimersByTime(time);
75 |
76 | debounced();
77 | debounced();
78 |
79 | expect(callback).toHaveBeenCalledTimes(2);
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/packages/chusho/lib/utils/tests/uid.spec.js:
--------------------------------------------------------------------------------
1 | describe('uid', async () => {
2 | const { default: uid } = await vi.importActual('../uid');
3 |
4 | it('should generate unique Ids', async () => {
5 | expect(typeof uid()).toBe('string');
6 | expect(uid('prefix')).toMatch('prefix-');
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/packages/chusho/lib/utils/uid.ts:
--------------------------------------------------------------------------------
1 | export default function uid(prefix = ''): string {
2 | const id = Date.now().toString(36) + Math.random().toString(36).substring(2);
3 | return prefix ? `${prefix}-${id}` : id;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/chusho/nyc.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'report-dir': 'coverage/cypress',
3 | reporter: ['lcov'],
4 | };
5 |
--------------------------------------------------------------------------------
/packages/chusho/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chusho",
3 | "version": "0.6.1",
4 | "description": "A library of bare accessible Vue components and tools.",
5 | "author": {
6 | "name": "Liip",
7 | "url": "https://www.liip.ch/"
8 | },
9 | "scripts": {
10 | "start": "node server",
11 | "build": "npm run build:lib && npm run build:types",
12 | "build:lib": "vite build",
13 | "build:types": "tsc -p tsconfig.prod.json --declaration --emitDeclarationOnly",
14 | "prepublishOnly": "npm run build",
15 | "test": "npm run build && npm run test:unit -- --run --coverage && npm run test:e2e",
16 | "test:unit": "vitest --config ./vitest.config.js",
17 | "test:e2e": "cypress run --component --browser chrome",
18 | "test:e2e:dev": "cypress open --component --browser chrome"
19 | },
20 | "main": "dist/chusho.cjs.js",
21 | "module": "dist/esm/chusho.js",
22 | "umd:main": "dist/chusho.umd.js",
23 | "types": "dist/types/chusho.d.ts",
24 | "jsdelivr": "dist/chusho.umd.js",
25 | "unpkg": "dist/chusho.umd.js",
26 | "files": [
27 | "dist/"
28 | ],
29 | "devDependencies": {
30 | "@cypress/code-coverage": "^3.10.0",
31 | "@types/lodash": "^4.14.191",
32 | "@vitejs/plugin-vue": "^3.2.0",
33 | "@vitejs/plugin-vue-jsx": "^2.1.1",
34 | "@vitest/coverage-c8": "^0.25.3",
35 | "@vue/test-utils": "^2.2.8",
36 | "autoprefixer": "^10.4.13",
37 | "c8": "^7.12.0",
38 | "cypress": "^12.2.0",
39 | "cypress-real-events": "^1.7.4",
40 | "express": "^4.18.2",
41 | "highlight.js": "^11.7.0",
42 | "jsdom": "^20.0.3",
43 | "lodash": "^4.17.21",
44 | "tailwindcss": "^3.2.4",
45 | "typescript": "^4.9.5",
46 | "vee-validate": "^4.7.3",
47 | "vite": "^4.1.2",
48 | "vite-plugin-istanbul": "^3.0.4",
49 | "vitest": "^0.26.2",
50 | "vue": "^3.2.45",
51 | "vue-router": "^4.1.6"
52 | },
53 | "peerDependencies": {
54 | "vue": "3.x"
55 | },
56 | "bugs": {
57 | "url": "https://github.com/liip/chusho/issues"
58 | },
59 | "homepage": "https://www.chusho.dev/",
60 | "keywords": [
61 | "vue",
62 | "components",
63 | "accessibility",
64 | "wcag"
65 | ],
66 | "license": "MIT",
67 | "repository": {
68 | "type": "git",
69 | "url": "https://github.com/liip/chusho.git",
70 | "directory": "packages/chusho"
71 | },
72 | "sideEffects": false
73 | }
74 |
--------------------------------------------------------------------------------
/packages/chusho/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require('tailwindcss'), require('autoprefixer')],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/chusho/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liip/chusho/96eeddf94fdb9ab5ee7f05d58baed134a22b9813/packages/chusho/public/favicon.ico
--------------------------------------------------------------------------------
/packages/chusho/public/highlightWorker.js:
--------------------------------------------------------------------------------
1 | self.importScripts(
2 | 'https://unpkg.com/prettier@2.2.1/standalone.js',
3 | 'https://unpkg.com/prettier@2.2.1/parser-html.js',
4 | 'https://unpkg.com/@highlightjs/cdn-assets@11.6.0/highlight.min.js',
5 | 'https://unpkg.com/@highlightjs/cdn-assets@11.6.0/languages/xml.min.js'
6 | );
7 |
8 | self.addEventListener('message', (e) => {
9 | const code = self.hljs.highlight(
10 | self.prettier.format(e.data, {
11 | parser: 'html',
12 | plugins: self.prettierPlugins,
13 | htmlWhitespaceSensitivity: 'ignore',
14 | }),
15 | {
16 | language: 'xml',
17 | }
18 | ).value;
19 |
20 | self.postMessage(code);
21 | });
22 |
--------------------------------------------------------------------------------
/packages/chusho/public/icons.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/packages/chusho/server.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const express = require('express');
4 | const { createServer: createViteServer } = require('vite');
5 |
6 | const PORT = 3000;
7 |
8 | async function createServer() {
9 | const app = express();
10 | const vite = await createViteServer({
11 | server: { middlewareMode: true },
12 | appType: 'custom',
13 | });
14 |
15 | app.use(vite.middlewares);
16 |
17 | app.use('*', async (req, res, next) => {
18 | const url = req.originalUrl;
19 |
20 | try {
21 | let template = fs.readFileSync(
22 | path.resolve(__dirname, 'index.html'),
23 | 'utf-8'
24 | );
25 |
26 | template = await vite.transformIndexHtml(url, template);
27 |
28 | const { render } = await vite.ssrLoadModule('/src/entry-server.js');
29 | const [appHtml, teleports] = await render(url);
30 |
31 | let html = template.replace(``, appHtml);
32 |
33 | if (teleports) {
34 | Object.keys(teleports).forEach((id) => {
35 | html = html.replace(``, teleports[id]);
36 | });
37 | }
38 |
39 | res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
40 | } catch (e) {
41 | vite.ssrFixStacktrace(e);
42 | next(e);
43 | }
44 | });
45 |
46 | app.listen(PORT);
47 |
48 | // eslint-disable-next-line no-console
49 | console.info(`Server started, go to http://localhost:${PORT}`);
50 | }
51 |
52 | createServer();
53 |
--------------------------------------------------------------------------------
/packages/chusho/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | ]
4 |
5 | Route {{ route.path }} does not exist.
6 |
7 |
8 |
9 |
10 |
35 |
--------------------------------------------------------------------------------
/packages/chusho/src/assets/images/building.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liip/chusho/96eeddf94fdb9ab5ee7f05d58baed134a22b9813/packages/chusho/src/assets/images/building.jpg
--------------------------------------------------------------------------------
/packages/chusho/src/assets/images/building.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liip/chusho/96eeddf94fdb9ab5ee7f05d58baed134a22b9813/packages/chusho/src/assets/images/building.webp
--------------------------------------------------------------------------------
/packages/chusho/src/assets/images/building@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liip/chusho/96eeddf94fdb9ab5ee7f05d58baed134a22b9813/packages/chusho/src/assets/images/building@2x.jpg
--------------------------------------------------------------------------------
/packages/chusho/src/assets/images/building@2x.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liip/chusho/96eeddf94fdb9ab5ee7f05d58baed134a22b9813/packages/chusho/src/assets/images/building@2x.webp
--------------------------------------------------------------------------------
/packages/chusho/src/assets/images/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.jpg';
2 | declare module '*.webp';
3 |
--------------------------------------------------------------------------------
/packages/chusho/src/assets/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | /*
6 | * Transitions
7 | */
8 |
9 | .fade-enter-active,
10 | .fade-leave-active {
11 | transition: opacity 0.25s;
12 | }
13 | .fade-enter-from,
14 | .fade-leave-to {
15 | opacity: 0;
16 | }
17 |
18 | .appear-enter-active,
19 | .appear-leave-active {
20 | transition: opacity 0.15s ease-in, transform 0.15s ease-in;
21 | }
22 | .appear-enter-from,
23 | .appear-leave-to {
24 | opacity: 0;
25 |
26 | transform: scale(0.98) translateY(-0.25rem);
27 | }
28 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/Examples.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
43 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/alert/Default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | This is an error message you should probably care about.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/btn/AsLink.vue:
--------------------------------------------------------------------------------
1 |
2 | Click me
3 |
4 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/btn/AsRouterLink.vue:
--------------------------------------------------------------------------------
1 |
2 | Click me
3 |
4 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/btn/Bare.vue:
--------------------------------------------------------------------------------
1 |
2 | Click me
3 |
4 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/btn/Default.vue:
--------------------------------------------------------------------------------
1 |
2 | Click me
3 |
4 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/btn/Disabled.vue:
--------------------------------------------------------------------------------
1 |
2 | Click me
3 |
4 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/btn/TypeSubmit.vue:
--------------------------------------------------------------------------------
1 |
2 | Click me
3 |
4 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/btn/WithVariant.vue:
--------------------------------------------------------------------------------
1 |
2 | Click me
3 |
4 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/checkbox/Controlled.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Check me if you can
5 |
6 |
7 |
8 |
17 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/collapse/Controlled.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ collapseOpen ? 'Close' : 'Open' }}
5 |
6 |
11 | Lorem ipsum dolor sit amet consectetur adipisicing elit.
12 |
13 |
14 |
15 |
16 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Eum, vitae autem
17 | adipisci necessitatibus cum quae quam? Dignissimos nesciunt nemo voluptate,
18 | animi reiciendis aperiam a vel amet earum, iure reprehenderit nulla. Quaerat
19 | iusto aspernatur perspiciatis ad temporibus. Nisi ipsam eaque consectetur!
20 |
21 |
22 |
27 | Toggle v-model
28 |
29 |
30 |
31 |
40 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/collapse/Default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Toggle
4 |
5 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam in,
6 | iste id nobis dolor excepturi dolore expedita vero quae. Nobis fuga
7 | cupiditate suscipit blanditiis, aliquid minima harum molestias pariatur
8 | tempora ab, libero quo maiores sapiente doloribus nihil commodi eaque
9 | accusantium praesentium! Nobis natus qui voluptate inventore molestias
10 | quisquam, consequuntur harum?
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/dialog/Default.vue:
--------------------------------------------------------------------------------
1 |
2 | Open dialog
3 |
4 |
11 |
12 |
13 |
Dialog title
14 |
15 |
16 |
23 | ✗
24 |
25 |
26 |
27 |
28 | Lorem ipsum dolor sit amet.
29 |
30 |
36 |
37 |
38 |
39 |
49 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/dialog/DynamicContent.vue:
--------------------------------------------------------------------------------
1 |
2 | Open dialog
3 |
4 |
11 |
12 |
13 |
Dialog title
14 |
15 |
16 |
23 | ✗
24 |
25 |
26 |
27 |
28 | Lorem ipsum dolor sit amet.
29 |
30 |
47 |
48 |
49 |
50 |
70 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/dialog/Nested.vue:
--------------------------------------------------------------------------------
1 |
2 | Open dialog
3 |
4 |
11 |
12 |
13 |
Dialog title
14 |
15 |
16 |
23 | ✗
24 |
25 |
26 |
27 |
28 | Lorem ipsum dolor sit amet.
29 |
30 |
31 | Open Child Dialog
34 |
35 |
36 |
37 |
46 |
47 | Hello, I’m a nested Dialog with different styling, nice to meet you.
48 |
49 |
50 | Cancel
53 |
54 |
55 |
56 |
66 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/dialog/OpenByDefault.vue:
--------------------------------------------------------------------------------
1 |
2 | Open dialog
3 |
4 |
11 |
12 |
13 |
Dialog title
14 |
15 |
16 |
23 | ✗
24 |
25 |
26 |
27 |
28 | Lorem ipsum dolor sit amet.
29 |
30 |
31 |
32 |
41 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/dialog/WithTransition.vue:
--------------------------------------------------------------------------------
1 |
2 | Open dialog
3 |
4 |
12 |
13 |
14 |
Dialog title
15 |
16 |
17 |
24 | ✗
25 |
26 |
27 |
28 |
29 | Lorem ipsum dolor sit amet.
30 |
31 |
32 |
33 |
42 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/formGroup/AsDiv.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Label
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/formGroup/MultipleNested.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Fruits
4 |
5 |
6 |
7 |
8 | {{ fruit }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
26 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/formGroup/Renderless.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Label
5 | *
6 |
7 |
8 |
9 |
10 |
11 |
16 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/formGroup/WithHelp.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Label
4 |
5 | This text describe the field above.
6 |
7 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/formGroup/WithSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Label
4 |
5 |
6 |
7 |
8 |
17 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/icon/CustomScale.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/icon/Default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/icon/WithAlternateText.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/label/Default.vue:
--------------------------------------------------------------------------------
1 |
2 | Label
3 |
4 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/label/WithCheckbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Label
4 |
5 |
6 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/label/WithTextField.vue:
--------------------------------------------------------------------------------
1 |
2 | Label
3 |
4 |
5 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/menu/Controlled.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Controlled menu
5 |
11 |
12 |
13 | Do laborum
14 | Voluptate aute
15 | Consectetur et ex commodo
16 |
17 | Aliquip veniam
18 | Laboris do
19 |
20 |
21 |
22 |
23 |
35 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/menu/Default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Default menu
5 |
11 |
12 |
13 | Do laborum
14 | Voluptate aute
15 | Consectetur et ex commodo
16 |
17 | Aliquip veniam
18 | Laboris do
19 |
20 |
21 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/menu/Disabled.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Disabled menu
5 |
11 |
12 |
13 | Do laborum
14 | Voluptate aute
15 | Consectetur et ex commodo
16 |
17 | Aliquip veniam
18 | Laboris do
19 |
20 |
21 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/menu/MenuLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Standard links
5 |
11 |
12 |
13 |
14 | Laboris do
15 |
16 |
17 | Laboris eu mollit
18 |
19 |
20 |
21 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/menu/MultiSelectable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Selectable items
5 |
11 |
12 |
13 |
19 | {{ item.label }}
20 |
21 |
22 |
23 |
24 |
25 |
59 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/menu/MultiSelectableObject.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Selectable items
5 |
11 |
12 |
13 |
19 | {{ item.label }}
20 |
21 |
22 |
23 |
24 |
25 |
74 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/menu/RouterLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Router links
5 |
11 |
12 |
13 |
14 | Voluptate aute
15 |
16 |
17 | Consectetur et ex commodo
18 |
19 |
20 |
21 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/menu/Selectable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Selectable items
5 |
11 |
12 |
13 |
14 | {{ item.label }}
15 |
16 |
17 |
18 |
19 |
20 |
53 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/menu/SelectableObject.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Selectable object items
5 |
11 |
12 |
13 |
14 | {{ item.label }}
15 |
16 |
17 |
18 |
19 |
20 |
68 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/picture/Default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
16 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/picture/WithSources.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
36 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/radio/Controlled.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 | True
13 |
14 |
15 |
23 | False
24 |
25 |
26 |
34 | Other
35 |
36 |
37 |
38 |
39 |
44 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/select/Disabled.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ value.label }}
5 |
6 |
7 |
8 |
9 |
15 | {{ item.label }}
16 |
17 |
18 |
19 |
20 |
21 |
46 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/select/OptionsGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ value.label }}
5 | Select a value…
6 |
12 |
13 |
14 |
15 |
16 | {{ group.label }}
17 |
22 |
28 | {{ option.label }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
78 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/select/WithValidation.vue:
--------------------------------------------------------------------------------
1 |
2 | {}">
3 |
4 |
11 |
12 | {{ color?.label }}
13 | Select a value…
14 |
20 |
21 |
22 |
23 |
24 |
30 | {{ item.label }}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
63 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/tabs/Controlled.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tab 1
5 | Tab 2
6 | Tab 3
7 |
8 |
9 |
10 |
11 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam in,
12 | iste id nobis dolor excepturi dolore expedita vero quae. Nobis fuga
13 | cupiditate suscipit blanditiis, aliquid minima harum molestias pariatur
14 | tempora ab.
15 |
16 |
17 | Nobis fuga cupiditate suscipit blanditiis, aliquid minima harum
18 | molestias pariatur tempora ab, libero quo maiores sapiente doloribus
19 | nihil commodi eaque accusantium praesentium! Nobis natus qui voluptate
20 | inventore molestias quisquam, consequuntur harum?
21 |
22 |
23 | Laboriosam in, iste id nobis dolor excepturi dolore expedita vero quae.
24 | Nobis natus qui voluptate inventore molestias quisquam, consequuntur
25 | harum?
26 |
27 |
28 |
29 |
30 |
31 |
40 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/tabs/Default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tab 1
5 | Tab 2
6 | Tab 3
7 |
8 |
9 |
10 |
11 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam in,
12 | iste id nobis dolor excepturi dolore expedita vero quae. Nobis fuga
13 | cupiditate suscipit blanditiis, aliquid minima harum molestias pariatur
14 | tempora ab.
15 |
16 |
17 | Nobis fuga cupiditate suscipit blanditiis, aliquid minima harum
18 | molestias pariatur tempora ab, libero quo maiores sapiente doloribus
19 | nihil commodi eaque accusantium praesentium! Nobis natus qui voluptate
20 | inventore molestias quisquam, consequuntur harum?
21 |
22 |
23 | Laboriosam in, iste id nobis dolor excepturi dolore expedita vero quae.
24 | Nobis natus qui voluptate inventore molestias quisquam, consequuntur
25 | harum?
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/tabs/Dynamic.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tab {{ tab }}
11 |
12 |
13 |
14 |
20 | Tab {{ tab }} content
21 |
22 |
23 |
24 |
25 |
26 | Add tab
27 | Remove tab
28 |
29 |
30 |
31 |
50 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/tabs/OverrideStyle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 | Tab {{ num }}
22 |
23 |
24 |
25 |
26 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam in,
27 | iste id nobis dolor excepturi dolore expedita vero quae. Nobis fuga
28 | cupiditate suscipit blanditiis, aliquid minima harum molestias pariatur
29 | tempora ab.
30 |
31 |
32 | Nobis fuga cupiditate suscipit blanditiis, aliquid minima harum
33 | molestias pariatur tempora ab, libero quo maiores sapiente doloribus
34 | nihil commodi eaque accusantium praesentium! Nobis natus qui voluptate
35 | inventore molestias quisquam, consequuntur harum?
36 |
37 |
38 | Laboriosam in, iste id nobis dolor excepturi dolore expedita vero quae.
39 | Nobis natus qui voluptate inventore molestias quisquam, consequuntur
40 | harum?
41 |
42 |
43 |
44 |
45 |
46 |
55 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/textField/Controlled.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
15 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/textField/WithValidation.vue:
--------------------------------------------------------------------------------
1 |
2 | {}">
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
33 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/components/textarea/Controlled.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/directives/click-outside/Default.vue:
--------------------------------------------------------------------------------
1 |
2 | Click inside
3 |
4 |
5 |
18 |
--------------------------------------------------------------------------------
/packages/chusho/src/components/examples/directives/click-outside/IgnoringElements.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Toggle spoiler
4 |
5 |
6 |
15 | Spoiler alert!
16 |
17 |
18 |
19 |
25 |
--------------------------------------------------------------------------------
/packages/chusho/src/entry-client.js:
--------------------------------------------------------------------------------
1 | import { createApp } from './main';
2 |
3 | const { app, router } = createApp();
4 |
5 | router.isReady().then(() => {
6 | app.mount('#app');
7 | });
8 |
--------------------------------------------------------------------------------
/packages/chusho/src/entry-server.js:
--------------------------------------------------------------------------------
1 | import { renderToString } from 'vue/server-renderer';
2 |
3 | import { createApp } from './main';
4 |
5 | export async function render(url) {
6 | const { app, router } = createApp();
7 |
8 | router.push(url);
9 | await router.isReady();
10 |
11 | const ctx = {};
12 | const html = await renderToString(app, ctx);
13 |
14 | return [html, ctx.teleports];
15 | }
16 |
--------------------------------------------------------------------------------
/packages/chusho/src/main.js:
--------------------------------------------------------------------------------
1 | import Chusho, { $chusho, components, directives, mergeDeep } from 'chusho';
2 | import { createSSRApp } from 'vue';
3 |
4 | import App from './App.vue';
5 | import './assets/tailwind.css';
6 | import chushoConfig from './chusho.config.ts';
7 | import { createRouter } from './router';
8 |
9 | export function createApp() {
10 | const app = createSSRApp(App);
11 | const router = createRouter();
12 |
13 | app.use(router);
14 | app.use(Chusho, chushoConfig);
15 |
16 | /**
17 | * Register all components & directives globally
18 | */
19 | Object.values(components).forEach((component) => {
20 | app.component(component.name, component);
21 | });
22 |
23 | Object.values(directives).forEach((directive) => {
24 | app.directive(directive.name, directive);
25 | });
26 |
27 | return { app, router };
28 | }
29 |
30 | if (import.meta.hot) {
31 | import.meta.hot.accept('./chusho.config.ts', (newConfig) => {
32 | mergeDeep($chusho.options, newConfig.default);
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/packages/chusho/src/router.js:
--------------------------------------------------------------------------------
1 | import startCase from 'lodash/startCase';
2 | import {
3 | createRouter as _createRouter,
4 | createMemoryHistory,
5 | createWebHistory,
6 | } from 'vue-router';
7 |
8 | import Examples from './components/Examples.vue';
9 | import Playground from './components/Playground.vue';
10 |
11 | const examplesComponents = import.meta.glob('./components/examples/**/*.vue');
12 | const examples = [];
13 |
14 | for (const path in examplesComponents) {
15 | const shortPath = path.replace('./components/examples/', '');
16 | const [type, item, variant] = shortPath.split('/');
17 | const name = startCase(variant.replace('.vue', ''));
18 |
19 | examples.push({
20 | path: `${type}/${item}/${name}`.toLowerCase().replaceAll(' ', '-'),
21 | component: examplesComponents[path],
22 | meta: {
23 | category: { id: type, label: startCase(type) },
24 | group: { id: item, label: startCase(item) },
25 | label: startCase(name),
26 | },
27 | });
28 | }
29 |
30 | export function createRouter() {
31 | return _createRouter({
32 | history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
33 | routes: [
34 | {
35 | path: '/',
36 | name: 'playground',
37 | component: Playground,
38 | },
39 | {
40 | path: '/examples',
41 | name: 'examples',
42 | component: Examples,
43 | children: examples,
44 | },
45 | ],
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/packages/chusho/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors');
2 |
3 | module.exports = {
4 | content: ['./src/chusho.config.ts', './index.html', './src/**/*.{js,ts,vue}'],
5 | theme: {
6 | extend: {
7 | colors: {
8 | gray: colors.slate,
9 | accent: {
10 | 100: '#FEF2DD',
11 | 200: '#FCDEAC',
12 | 300: '#F9CB7B',
13 | 400: '#F7B84A',
14 | 500: '#F5A519',
15 | 600: '#D38909',
16 | 700: '#A26907',
17 | 800: '#714905',
18 | 900: '#402903',
19 | },
20 | },
21 | },
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/packages/chusho/test/setup.js:
--------------------------------------------------------------------------------
1 | import uid, { reset } from '../lib/utils/uid';
2 |
3 | /*----------------------------------------*\
4 | Spy on console.warn to assert on it
5 | \*----------------------------------------*/
6 |
7 | expect.extend({
8 | toHaveBeenWarned(received) {
9 | asserted.add(received);
10 |
11 | const passed = warn.mock.calls.some(
12 | (args) => args[0].indexOf(received) > -1
13 | );
14 |
15 | if (passed) {
16 | return {
17 | pass: true,
18 | message: () => `expected "${received}" not to have been warned.`,
19 | };
20 | } else {
21 | const msgs = warn.mock.calls.map((args) => args[0]).join('\n - ');
22 | return {
23 | pass: false,
24 | message: () =>
25 | `expected "${received}" to have been warned.\n\nActual messages:\n\n - ${msgs}`,
26 | };
27 | }
28 | },
29 | });
30 |
31 | let warn;
32 | const asserted = new Set();
33 |
34 | beforeEach(() => {
35 | asserted.clear();
36 | warn = vi.spyOn(console, 'warn');
37 | warn.mockImplementation(() => {
38 | // noop
39 | });
40 | });
41 |
42 | afterEach(() => {
43 | const assertedArray = Array.from(asserted);
44 | const nonAssertedWarnings = warn.mock.calls
45 | .map((args) => args[0])
46 | .filter((received) => {
47 | return !assertedArray.some((assertedMsg) => {
48 | return received.indexOf(assertedMsg) > -1;
49 | });
50 | });
51 | warn.mockRestore();
52 | if (nonAssertedWarnings.length) {
53 | nonAssertedWarnings.forEach((warning) => {
54 | // eslint-disable-next-line no-console
55 | console.error(warning);
56 | });
57 | }
58 | });
59 |
60 | /*----------------------------------------*\
61 | Keep the same UID sequence for each test
62 | \*----------------------------------------*/
63 |
64 | vi.mock('../lib/utils/uid');
65 |
66 | afterEach(() => {
67 | uid.mockClear();
68 | reset();
69 | });
70 |
--------------------------------------------------------------------------------
/packages/chusho/test/utils.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils';
2 |
3 | /**
4 | * Wrap a composable into a component setup
5 | * Required for composables using lifecycle hooks
6 | */
7 | export function withSetup(composableFn, options = {}) {
8 | let composable;
9 |
10 | const wrapper = mount({
11 | ...options,
12 |
13 | setup() {
14 | composable = composableFn();
15 | // Suppress missing template warning
16 | return () => {
17 | // noop
18 | };
19 | },
20 | });
21 |
22 | return { wrapper, composable };
23 | }
24 |
--------------------------------------------------------------------------------
/packages/chusho/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "esnext",
5 | "declaration": true,
6 | "declarationDir": "./dist/types",
7 | "strict": true,
8 | "strictPropertyInitialization": false,
9 | "jsx": "preserve",
10 | "importHelpers": true,
11 | "moduleResolution": "node",
12 | "noImplicitAny": true,
13 | "experimentalDecorators": true,
14 | "esModuleInterop": true,
15 | "allowSyntheticDefaultImports": true,
16 | "sourceMap": true,
17 | "baseUrl": ".",
18 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
19 | "isolatedModules": true,
20 | "skipLibCheck": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/chusho/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "types": ["vitest/globals", "node", "cypress-real-events", "lodash"]
5 | },
6 | "include": ["lib/**/*", "src/**/*", "cypress/**/*", "types/**/*"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/chusho/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "include": ["lib/**/*"],
4 | "exclude": ["node_modules", "**/*.cy.tsx", "**/__mocks__/**"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/chusho/types/vue-shims.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue';
3 | export default Vue;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/chusho/vite.config.js:
--------------------------------------------------------------------------------
1 | import vue from '@vitejs/plugin-vue';
2 | import vueJsx from '@vitejs/plugin-vue-jsx';
3 | import path from 'path';
4 | import { defineConfig } from 'vite';
5 | import istanbul from 'vite-plugin-istanbul';
6 |
7 | const libraryName = 'Chusho';
8 | const builds = [
9 | {
10 | output: {
11 | format: 'cjs',
12 | name: libraryName,
13 | },
14 | },
15 | {
16 | output: {
17 | format: 'umd',
18 | name: libraryName,
19 | },
20 | },
21 | {
22 | output: {
23 | format: 'es',
24 | dir: 'dist/esm',
25 | preserveModules: true,
26 | preserveModulesRoot: 'src',
27 | },
28 | },
29 | ];
30 |
31 | export default defineConfig(({ mode }) => {
32 | const isProd = mode === 'production';
33 |
34 | const config = {
35 | resolve: {
36 | alias: {
37 | '@': path.resolve(__dirname, 'src'),
38 | chusho: path.resolve(__dirname, 'lib/chusho.ts'),
39 | },
40 | },
41 |
42 | build: {
43 | minify: false,
44 | lib: {
45 | entry: path.resolve(__dirname, 'lib/chusho.ts'),
46 | name: libraryName,
47 | fileName: (format) => {
48 | return format === 'es' ? `[name].js` : `chusho.${format}.js`;
49 | },
50 | },
51 | rollupOptions: {
52 | external: ['vue'],
53 | output: builds.map((build) => {
54 | return {
55 | ...build.output,
56 | exports: 'named',
57 | globals: {
58 | vue: 'Vue',
59 | },
60 | };
61 | }),
62 | },
63 | },
64 |
65 | plugins: [vue(), vueJsx()],
66 | };
67 |
68 | if (isProd) {
69 | // The library emits warnings and other helpful messages for users in development.
70 | // They are wrapped in NODE_ENV conditions to be removed from production bundles.
71 | // This prevents them from being replaced by Vite now and kept in our bundles.
72 | config.define = {
73 | 'process.env.NODE_ENV': 'process.env.NODE_ENV',
74 | };
75 |
76 | // Do not copy playground public files (like favicon.ico) to the library dist folder
77 | config.publicDir = false;
78 | } else {
79 | if (process.env.CYPRESS) {
80 | config.build.sourcemap = true;
81 | config.plugins.push(
82 | istanbul({
83 | include: ['lib/**/*'],
84 | extension: ['.ts'],
85 | })
86 | );
87 | }
88 | }
89 |
90 | return config;
91 | });
92 |
--------------------------------------------------------------------------------
/packages/chusho/vitest.config.js:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path';
2 | import { defineConfig } from 'vitest/config';
3 |
4 | export default defineConfig({
5 | test: {
6 | environment: 'jsdom',
7 | globals: true,
8 | setupFiles: [resolve(__dirname, 'test/setup.js')],
9 | coverage: {
10 | reporter: ['lcov'],
11 | include: ['lib/**/*.ts'],
12 | exclude: ['**/__mocks__/**', 'lib/components/**'],
13 | reportsDirectory: 'coverage/vitest',
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/packages/docs/.vitepress/theme/index.js:
--------------------------------------------------------------------------------
1 | import Chusho, { $chusho, components, mergeDeep } from 'chusho';
2 | import DefaultTheme from 'vitepress/theme';
3 | import { defineAsyncComponent } from 'vue';
4 |
5 | import Showcase from '../../components/Showcase.vue';
6 |
7 | import chushoConfig from '../../chusho.config.ts';
8 | import './custom.css';
9 |
10 | if (import.meta.hot) {
11 | import.meta.hot.accept('../../chusho.config.ts', (newConfig) => {
12 | mergeDeep($chusho.options, newConfig.default);
13 | });
14 | }
15 |
16 | export default {
17 | ...DefaultTheme,
18 |
19 | enhanceApp(ctx) {
20 | DefaultTheme.enhanceApp(ctx);
21 |
22 | /**
23 | * Install and configure Chūshō
24 | */
25 | ctx.app.use(Chusho, chushoConfig);
26 |
27 | Object.values(components).forEach((component) => {
28 | if (component.name) {
29 | ctx.app.component(component.name, component);
30 | }
31 | });
32 |
33 | /**
34 | * Extra components for documentation
35 | */
36 | ctx.app.component('Showcase', Showcase);
37 | ctx.app.component(
38 | 'Docgen',
39 | defineAsyncComponent(() => import('../../components/Docgen.vue'))
40 | );
41 |
42 | /**
43 | * Example components
44 | */
45 | ctx.app.component(
46 | 'ExampleCheckbox',
47 | defineAsyncComponent(() =>
48 | import('../../components/Example/Checkbox.vue')
49 | )
50 | );
51 | ctx.app.component(
52 | 'ExampleRadio',
53 | defineAsyncComponent(() => import('../../components/Example/Radio.vue'))
54 | );
55 | ctx.app.component(
56 | 'ExampleSelect',
57 | defineAsyncComponent(() => import('../../components/Example/Select.vue'))
58 | );
59 | ctx.app.component(
60 | 'ExampleDialog',
61 | defineAsyncComponent(() => import('../../components/Example/Dialog.vue'))
62 | );
63 | },
64 | };
65 |
--------------------------------------------------------------------------------
/packages/docs/README.md:
--------------------------------------------------------------------------------
1 | # Chūshō docs
2 |
3 | Documentation powered by [VitePress](https://vitepress.vuejs.org/) and deployed to [chusho.dev](https://www.chusho.dev/) from `main` branch.
4 |
5 | ## Useful commands
6 |
7 | Start the development server:
8 |
9 | ```bash
10 | npm start
11 | ```
12 |
13 | Build a static version into `.vitepress/dist`
14 |
15 | ```bash
16 | npm run build
17 | ```
18 |
--------------------------------------------------------------------------------
/packages/docs/assets/showcase.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .appear-enter-active,
6 | .appear-leave-active {
7 | transition: opacity 0.15s ease-in, transform 0.15s ease-in;
8 | }
9 |
10 | .appear-enter-from,
11 | .appear-leave-to {
12 | opacity: 0;
13 | transform: scale(0.98) translateY(-0.25rem);
14 | }
15 |
16 | .dialog-enter-active,
17 | .dialog-leave-active {
18 | transition: opacity 0.2s;
19 |
20 | .dialog {
21 | transition-duration: 0.3s;
22 | transition-property: opacity transform;
23 | transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.4);
24 | }
25 | }
26 |
27 | .dialog-enter-from,
28 | .dialog-leave-to {
29 | opacity: 0;
30 |
31 | .dialog {
32 | opacity: 0;
33 | transform: scale(0.9);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/docs/components/ComponentSpecs.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
39 |
--------------------------------------------------------------------------------
/packages/docs/components/Docgen.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 | {{ doc.displayName }}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
77 |
78 |
106 |
--------------------------------------------------------------------------------
/packages/docs/components/Example/Checkbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Check me if you can
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/packages/docs/components/Example/Dialog.vue:
--------------------------------------------------------------------------------
1 |
2 | Open Modal
3 |
4 |
10 |
11 |
Are you sure?
12 |
13 |
14 | You’re about to delete the file “cute-kitten.jpg”, are
15 | you sure? This action cannot be reverted.
16 |
17 |
18 |
19 |
23 |
24 |
25 |
26 |
35 |
--------------------------------------------------------------------------------
/packages/docs/components/Example/Radio.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Cats
6 |
7 |
8 |
9 | Dogs
10 |
11 |
12 |
13 | Both
14 |
15 |
16 |
17 |
18 |
23 |
--------------------------------------------------------------------------------
/packages/docs/components/Showcase.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/packages/docs/guide/browsers-support.md:
--------------------------------------------------------------------------------
1 | # Browsers support
2 |
3 | If you’re using Vite, Webpack, Rollup or another modern bundler, it’s probably going to import the ES Module described above. This is great as it will automatically enable [tree-shaking](https://en.wikipedia.org/wiki/Tree_shaking) and remove unused code from your app bundle.
4 |
5 | However this version targets modern browsers (supporting ES2019 features), therefor if you need to support older browsers, you’ll have to transpile Chūshō’s code.
6 |
7 | ### Vite
8 |
9 | See [@vitejs/plugin-legacy](https://github.com/vitejs/vite/tree/main/packages/plugin-legacy).
10 |
11 | ### Vue CLI
12 |
13 | To transpile for older browsers using Vue CLI, you need to add it to the `transpileDependencies` array in [`vue.config.js`](https://cli.vuejs.org/config/#vue-config-js):
14 |
15 | ```js{4}
16 | // vue.config.js
17 |
18 | module.exports = {
19 | transpileDependencies: ['chusho']
20 | }
21 | ```
22 |
23 | ### Webpack & Babel
24 |
25 | To transpile for older browsers using a custom Webpack setup, you need to adapt your JavaScript loader configuration to include Chūshō. Usually it’s configured like this:
26 |
27 | ```js{10}
28 | // webpack.config.js
29 |
30 | module.exports = {
31 | // ...
32 | module: {
33 | rules: [
34 | {
35 | test: /\.js$/,
36 | loader: 'babel-loader',
37 | exclude: /node_modules/,
38 | },s
39 | ],
40 | },
41 | // ...
42 | };
43 | ```
44 |
45 | Instead of excluding all `node_modules`, adapt the Regex to include Chūshō:
46 |
47 | ```js{5}
48 | // ...
49 | exclude: /node_modules\/(?!chusho)/,
50 | // ...
51 | ```
52 |
--------------------------------------------------------------------------------
/packages/docs/guide/components/alert.md:
--------------------------------------------------------------------------------
1 | # Alert
2 |
3 | Announce important messages.
4 |
5 |
6 |
7 |
8 | Oops! Something went wrong.
9 |
10 |
11 | Hey! You should pay attention to this message.
12 |
13 |
14 | Yay! Your message was sent.
15 |
16 |
17 | Hey! Did you know?
18 |
19 |
20 |
21 |
22 | ## Usage
23 |
24 | See [using components](/guide/using-components) for detailed instructions.
25 |
26 | ```js
27 | import { CAlert } from 'chusho';
28 | ```
29 |
30 | ## Config
31 |
32 | The options below are to be set in the [global configuration](/guide/config.html) at the following location:
33 |
34 | ```js
35 | {
36 | components: {
37 | alert: {
38 | class: ({ variant }) => {},
39 | },
40 | },
41 | }
42 | ```
43 |
44 | ### class
45 |
46 | Classes applied to the component root element, except when the prop `bare` is set to `true`. See [styling components](/guide/styling-components).
47 |
48 | - **type:** `Array | Object | String | (props: Object) => {}`
49 | - **default:** `null`
50 |
51 | #### Example
52 |
53 | ```js
54 | class({ variant }) {
55 | return ['alert', {
56 | 'alert--error': variant?.error,
57 | 'alert--warning': variant?.warning,
58 | }]
59 | }
60 | ```
61 |
62 | ## API
63 |
64 |
65 |
66 | ## Examples
67 |
68 | ### Simplest
69 |
70 | ```vue-html
71 | An important message.
72 | ```
73 |
74 | ### With variant
75 |
76 | ```vue-html
77 | An important message.
78 | ```
79 |
--------------------------------------------------------------------------------
/packages/docs/guide/components/checkbox.md:
--------------------------------------------------------------------------------
1 | # Checkbox
2 |
3 | Augmented form field for boolean input.
4 |
5 |
6 |
7 |
8 |
9 | ## Usage
10 |
11 | See [using components](/guide/using-components) for detailed instructions.
12 |
13 | ```js
14 | import { CCheckbox } from 'chusho';
15 | ```
16 |
17 | ## Config
18 |
19 | The options below are to be set in the [global configuration](/guide/config.html) at the following location:
20 |
21 | ```js
22 | {
23 | components: {
24 | checkbox: {
25 | class: ({ checked, required, disabled, modelValue, trueValue, falseValue, variant }) => {},
26 | },
27 | },
28 | }
29 | ```
30 |
31 | ### class
32 |
33 | Classes applied to the input element, except when the prop `bare` is set to `true`. See [styling components](/guide/styling-components).
34 |
35 | - **type:** `Array | Object | String | (props: Object) => {}`
36 | - **default:** `null`
37 |
38 | #### Example
39 |
40 | ```js
41 | class({ checked }) {
42 | return ['checkbox', {
43 | 'checkbox--checked': checked,
44 | }]
45 | }
46 | ```
47 |
48 | ## API
49 |
50 |
51 |
52 | ## Examples
53 |
54 | ### Controlled
55 |
56 | ```vue
57 |
58 |
59 |
60 |
61 |
70 | ```
71 |
72 | ### Controlled with custom values
73 |
74 | ```vue
75 |
76 |
77 |
78 |
79 |
88 | ```
89 |
--------------------------------------------------------------------------------
/packages/docs/guide/components/index.md:
--------------------------------------------------------------------------------
1 | # Components
2 |
3 | - [Alert](alert.html)
4 | - [Button](button.html)
5 | - [Checkbox](checkbox.html)
6 | - [Collapse](collapse.html)
7 | - [Dialog](dialog.html)
8 | - [FormGroup](formgroup.html)
9 | - [Icon](icon.html)
10 | - [Label](label.html)
11 | - [Menu](menu.html)
12 | - [Picture](picture.html)
13 | - [Radio](radio.html)
14 | - [Select](select.html)
15 | - [Tabs](tabs.html)
16 | - [Textarea](textarea.html)
17 | - [TextField](textfield.html)
18 |
--------------------------------------------------------------------------------
/packages/docs/guide/components/label.md:
--------------------------------------------------------------------------------
1 | # Label
2 |
3 | Just like a regular ``
4 |
5 |
6 |
7 | I’m a label!
8 |
9 |
10 |
11 | ## Usage
12 |
13 | See [using components](/guide/using-components) for detailed instructions.
14 |
15 | ```js
16 | import { CLabel } from 'chusho';
17 | ```
18 |
19 | ## Config
20 |
21 | The options below are to be set in the [global configuration](/guide/config.html) at the following location:
22 |
23 | ```js
24 | {
25 | components: {
26 | label: {
27 | class: ({ variant }) => {},
28 | },
29 | },
30 | }
31 | ```
32 |
33 | ### class
34 |
35 | Classes applied to the label element, except when the prop `bare` is set to `true`. See [styling components](/guide/styling-components).
36 |
37 | - **type:** `Array | Object | String | (props: Object) => {}`
38 | - **default:** `null`
39 |
40 | #### Example
41 |
42 | ```js
43 | class: 'label'
44 | ```
45 |
46 | ## API
47 |
48 |
49 |
50 | ## Examples
51 |
52 | ### Simplest
53 |
54 | ```vue-html
55 | Label
56 | ```
57 |
58 | ### Linked to a field
59 |
60 | ```vue-html
61 |
62 | Something labelling the checkbox
63 |
64 | ```
65 |
--------------------------------------------------------------------------------
/packages/docs/guide/components/picture.md:
--------------------------------------------------------------------------------
1 | # Picture
2 |
3 | Easily generate responsive images.
4 |
5 |
6 |
12 |
13 |
14 | ## Usage
15 |
16 | See [using components](/guide/using-components) for detailed instructions.
17 |
18 | ```js
19 | import { CPicture } from 'chusho';
20 | ```
21 |
22 | ## Config
23 |
24 | The options below are to be set in the [global configuration](/guide/config.html) at the following location:
25 |
26 | ```js
27 | {
28 | components: {
29 | picture: {
30 | class: ({ src, alt, sources, variant }) => {},
31 | },
32 | },
33 | }
34 | ```
35 |
36 | ### class
37 |
38 | Classes applied to the component `img` element, except when the prop `bare` is set to `true`. See [styling components](/guide/styling-components).
39 |
40 | - **type:** `Array | Object | String | (props: Object) => {}`
41 | - **default:** `null`
42 |
43 | #### Example
44 |
45 | ```js
46 | class: 'img-responsive'
47 | ```
48 |
49 | ## API
50 |
51 |
52 |
53 | ## Examples
54 |
55 | ### Simplest
56 |
57 | ```vue-html
58 |
59 | ```
60 |
61 | ### With sources
62 |
63 | ```vue-html
64 |
77 | ```
78 |
79 | ### With additional attributes
80 |
81 | Attributes are not applied to the `picture` element but to the `img` element.
82 |
83 | ```vue-html
84 |
85 | ```
86 |
--------------------------------------------------------------------------------
/packages/docs/guide/components/radio.md:
--------------------------------------------------------------------------------
1 | # Radio
2 |
3 | Augmented form field for choice input.
4 |
5 |
6 |
7 |
8 |
9 | ## Usage
10 |
11 | See [using components](/guide/using-components) for detailed instructions.
12 |
13 | ```js
14 | import { CRadio } from 'chusho';
15 | ```
16 |
17 | ## Config
18 |
19 | The options below are to be set in the [global configuration](/guide/config.html) at the following location:
20 |
21 | ```js
22 | {
23 | components: {
24 | radio: {
25 | class: ({ checked, modelValue, value, required, disabled, variant }) => {},
26 | },
27 | },
28 | }
29 | ```
30 |
31 | ### class
32 |
33 | Classes applied to the input element, except when the prop `bare` is set to `true`. See [styling components](/guide/styling-components).
34 |
35 | - **type:** `Array | Object | String | (props: Object) => {}`
36 | - **default:** `null`
37 |
38 | #### Example
39 |
40 | ```js
41 | class({ checked }) {
42 | return ['radio', {
43 | 'radio--checked': checked,
44 | }]
45 | }
46 | ```
47 |
48 | ## API
49 |
50 |
51 |
52 | ## Examples
53 |
54 | ### Controlled
55 |
56 | ```vue
57 |
58 |
59 |
60 |
61 |
62 |
63 |
72 | ```
73 |
--------------------------------------------------------------------------------
/packages/docs/guide/components/textarea.md:
--------------------------------------------------------------------------------
1 | # Textarea
2 |
3 | Augmented textarea form field.
4 |
5 |
6 |
7 |
8 |
9 | ## Usage
10 |
11 | See [using components](/guide/using-components) for detailed instructions.
12 |
13 | ```js
14 | import { CTextarea } from 'chusho';
15 | ```
16 |
17 | ## Config
18 |
19 | The options below are to be set in the [global configuration](/guide/config.html) at the following location:
20 |
21 | ```js
22 | {
23 | components: {
24 | textarea: {
25 | class({ required, disabled, readonly, modelValue, variant }) => {},
26 | },
27 | },
28 | }
29 | ```
30 |
31 | ### class
32 |
33 | Classes applied to the textarea element, except when the prop `bare` is set to `true`. See [styling components](/guide/styling-components).
34 |
35 | - **type:** `Array | Object | String | (props: Object) => {}`
36 | - **default:** `null`
37 |
38 | #### Example
39 |
40 | ```js
41 | class: 'field field--textarea'
42 | ```
43 |
44 | ## API
45 |
46 |
47 |
48 | ## Examples
49 |
50 | ### Controlled
51 |
52 | ```vue
53 |
54 |
55 |
56 |
57 |
66 | ```
67 |
--------------------------------------------------------------------------------
/packages/docs/guide/components/textfield.md:
--------------------------------------------------------------------------------
1 | # TextField
2 |
3 | Augmented form field for text input.
4 |
5 |
6 |
7 |
8 |
9 | ## Usage
10 |
11 | See [using components](/guide/using-components) for detailed instructions.
12 |
13 | ```js
14 | import { CTextField } from 'chusho';
15 | ```
16 |
17 | ## Config
18 |
19 | The options below are to be set in the [global configuration](/guide/config.html) at the following location:
20 |
21 | ```js
22 | {
23 | components: {
24 | textField: {
25 | class({ required, disabled, readonly, type, modelValue, variant }) => {},
26 | },
27 | },
28 | }
29 | ```
30 |
31 | ### class
32 |
33 | Classes applied to the input element, except when the prop `bare` is set to `true`. See [styling components](/guide/styling-components).
34 |
35 | - **type:** `Array | Object | String | (props: Object) => {}`
36 | - **default:** `null`
37 |
38 | #### Example
39 |
40 | ```js
41 | class({ type }) {
42 | return ['field', `field--${type}`]
43 | }
44 | ```
45 |
46 | ## API
47 |
48 |
49 |
50 | ## Examples
51 |
52 | ### Controlled
53 |
54 | ```vue
55 |
56 |
57 |
58 |
59 |
68 | ```
69 |
70 | ### With type
71 |
72 | ```vue
73 |
74 |
75 |
76 |
77 |
78 | ```
79 |
--------------------------------------------------------------------------------
/packages/docs/guide/directives/click-outside.md:
--------------------------------------------------------------------------------
1 | # Click Outside
2 |
3 | Trigger a function when a click happens outside the element with the said directive applied.
4 |
5 | ## Examples
6 |
7 | ### Simple
8 |
9 | ```vue
10 |
11 | Label
12 |
13 |
14 |
19 | ```
20 |
21 | ### Ignoring elements
22 |
23 | This is useful in the case of dynamically rendered elements. To avoid the click on the trigger from immediately removing the target due to event bubbling, the triggers must be ignored.
24 |
25 | ```vue
26 |
27 |
28 | Toggle spoiler
29 |
30 |
31 |
40 | Spoiler alert!
41 |
42 |
43 |
44 |
50 | ```
51 |
52 | ## API
53 |
54 | - **Expects:** `Function | { handler: Function, options: ClickOutsideOptions }`
55 |
56 | ```ts
57 | interface ClickOutsideOptions {
58 | // Click on or within ignored elements will not trigger the handler
59 | ignore?: Array;
60 | }
61 | ```
62 |
63 | The function receive the original click Event as the first argument.
64 |
--------------------------------------------------------------------------------
/packages/docs/guide/directives/index.md:
--------------------------------------------------------------------------------
1 | # Directives
2 |
3 | - [Click Outside](click-outside.html)
4 |
--------------------------------------------------------------------------------
/packages/docs/guide/index.md:
--------------------------------------------------------------------------------
1 | # Getting started
2 |
3 | :::: tip NOTICE
4 | Chūshō requires Vue.js 3
5 | ::::
6 |
7 | ## Installation
8 |
9 | Install Chūshō with your favorite package manager:
10 |
11 | ::: code-group
12 |
13 | ```bash [npm]
14 | npm install chusho
15 | ```
16 |
17 | ```bash [yarn]
18 | yarn add chusho
19 | ```
20 |
21 | ```bash [pnpm]
22 | pnpm add chusho
23 | ```
24 |
25 | :::
26 |
27 | ## Setup
28 |
29 | In your main entry point, enable Chūshō with:
30 |
31 | ```js
32 | import Chusho from 'chusho';
33 | import { createApp } from 'vue';
34 |
35 | const app = createApp(App);
36 |
37 | app.use(Chusho, {
38 | // Here goes the config
39 | });
40 | ```
41 |
42 | See the [configuration](/guide/config.html) for available options.
43 |
--------------------------------------------------------------------------------
/packages/docs/guide/using-components.md:
--------------------------------------------------------------------------------
1 | # Using components
2 |
3 | Components aren’t registered automatically to take advantage of bundler’s [tree-shaking](https://en.wikipedia.org/wiki/Tree_shaking) optimizations by default.
4 |
5 | You need to register Chūshō’s components you want to use either globally, for example in your main entry point:
6 |
7 | ```js{2,8-9}
8 | import { createApp } from 'vue';
9 | import { CBtn, CIcon } from 'chusho';
10 |
11 | const app = createApp(App);
12 |
13 | app.use(Chusho, { ... });
14 |
15 | app.component('CBtn', CBtn);
16 | app.component('CIcon', CIcon);
17 | ```
18 |
19 | Or locally in the components they are used:
20 |
21 | ```vue{11,15-17}
22 |
23 |
24 | ...
25 |
26 |
27 |
28 |
29 |
30 |
31 |
42 | ```
43 |
44 | You can also use a plugin like [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components/) to auto-import components on-demand.
45 |
--------------------------------------------------------------------------------
/packages/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 | hero:
4 | name: Chūshō
5 | text: Unstyled accessible UI components
6 | tagline: Styling interfaces easily without compromising on accessibility, for Vue.js 3.
7 | image:
8 | light: /logo.svg
9 | dark: /logo-dark.svg
10 | actions:
11 | - text: Get Started →
12 | link: /guide/
13 | type: primary
14 | features:
15 | - icon: 🧏
16 | title: Accessible
17 | details: All components are following the Web Content Accessibility Guidelines (WCAG) recommendations.
18 | - icon: 🖤
19 | title: Unstyled
20 | details: No built-in CSS whatsoever, you have total control over which class is applied to which element.
21 | - icon: ⚙️
22 | title: Configurable globally
23 | details: Define classes of every component globally and conditionally based on props, override locally when necessary.
24 | footer: MIT Licensed · © 2020-present, Liip
25 | ---
26 |
--------------------------------------------------------------------------------
/packages/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@chusho/docs",
3 | "version": "0.6.1",
4 | "private": true,
5 | "license": "MIT",
6 | "description": "Chūshō documentation",
7 | "author": {
8 | "name": "Liip",
9 | "url": "https://www.liip.ch/"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/liip/chusho.git",
14 | "directory": "packages/docs"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/liip/chusho/issues"
18 | },
19 | "homepage": "https://www.chusho.dev/",
20 | "scripts": {
21 | "start": "concurrently --kill-others 'npm run dev' 'npm run tailwind:watch'",
22 | "dev": "vitepress dev",
23 | "preview": "vitepress preview",
24 | "tailwind": "tailwindcss -i assets/showcase.css -o ./public/dist/showcase.css",
25 | "tailwind:watch": "npm run tailwind -- --watch",
26 | "build": "npm run tailwind && vitepress build"
27 | },
28 | "devDependencies": {
29 | "concurrently": "^7.6.0",
30 | "postcss": "^8.4.20",
31 | "sass": "^1.56.1",
32 | "vitepress": "^1.0.0-alpha.45",
33 | "vue": "^3.2.47",
34 | "vue-docgen-api": "^4.54.2"
35 | },
36 | "dependencies": {
37 | "chusho": "^0.6.1",
38 | "markdown-it": "^13.0.1",
39 | "tailwindcss": "^3.2.4"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/docs/plugins/docGen.js:
--------------------------------------------------------------------------------
1 | import glob from 'glob';
2 | import path from 'path';
3 | import { parse } from 'vue-docgen-api';
4 |
5 | async function parseComponents() {
6 | const componentsDocgen = {};
7 | const components = glob.sync('../../chusho/lib/components/C*/C*.ts', {
8 | cwd: path.resolve(__dirname),
9 | });
10 |
11 | await Promise.all(
12 | components.map((component) => {
13 | return parse(path.resolve(__dirname, component)).then((parsed) => {
14 | componentsDocgen[parsed.displayName] = parsed;
15 | });
16 | })
17 | );
18 |
19 | return componentsDocgen;
20 | }
21 |
22 | export default function docGen() {
23 | const virtualModuleId = 'virtual:docgen';
24 | const resolvedVirtualModuleId = '\0' + virtualModuleId;
25 |
26 | return {
27 | name: 'docGen',
28 |
29 | enforce: 'pre',
30 |
31 | resolveId(id) {
32 | if (id === virtualModuleId) {
33 | return resolvedVirtualModuleId;
34 | }
35 | },
36 |
37 | /**
38 | * Initial load of the virtual module
39 | */
40 | async load(id) {
41 | if (id === resolvedVirtualModuleId) {
42 | const componentsDocgen = await parseComponents();
43 | return `export default ${JSON.stringify(componentsDocgen)}`;
44 | }
45 | },
46 |
47 | configureServer(server) {
48 | const components = path.resolve(__dirname, '../chusho');
49 | server.watcher.add(components);
50 | },
51 |
52 | async handleHotUpdate({ file, server }) {
53 | if (file.includes('chusho/lib/components')) {
54 | const module = server.moduleGraph.getModuleById(
55 | resolvedVirtualModuleId
56 | );
57 | server.moduleGraph.invalidateModule(module);
58 |
59 | const data = await parseComponents();
60 | server.ws.send({
61 | type: 'custom',
62 | event: 'docgen-changed',
63 | data,
64 | });
65 | }
66 | },
67 | };
68 | }
69 |
--------------------------------------------------------------------------------
/packages/docs/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('tailwindcss/nesting'),
4 | require('tailwindcss'),
5 | require('autoprefixer'),
6 | ],
7 | };
8 |
--------------------------------------------------------------------------------
/packages/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liip/chusho/96eeddf94fdb9ab5ee7f05d58baed134a22b9813/packages/docs/public/favicon.ico
--------------------------------------------------------------------------------
/packages/docs/public/logo-dark.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/packages/docs/public/logo.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/packages/docs/public/showcase.js:
--------------------------------------------------------------------------------
1 | class ShowcaseRoot extends HTMLElement {
2 | constructor() {
3 | super();
4 | this.attachShadow({ mode: 'open' });
5 |
6 | // Inject the stylesheets within the shadow root
7 | // so that it doesn't leak outside of the component.
8 | const style = document.createElement('link');
9 | style.rel = 'stylesheet';
10 | style.href = '/dist/showcase.css';
11 | this.shadowRoot.appendChild(style);
12 | }
13 |
14 | connectedCallback() {
15 | // The slot content must be moved to the shadow root
16 | // for the scoped style above to be applied.
17 | this.shadowRoot.append(...this.childNodes);
18 | }
19 | }
20 |
21 | customElements.define('showcase-root', ShowcaseRoot);
22 |
--------------------------------------------------------------------------------
/packages/docs/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./chusho.config.ts', './components/**/*.vue', './guide/**/*.md'],
3 | theme: {
4 | extend: {
5 | colors: {
6 | gold: {
7 | 100: 'var(--gold-100)',
8 | 200: 'var(--gold-200)',
9 | 300: 'var(--gold-300)',
10 | 400: 'var(--gold-400)',
11 | 500: 'var(--gold-500)',
12 | 600: 'var(--gold-600)',
13 | 700: 'var(--gold-700)',
14 | 800: 'var(--gold-800)',
15 | 900: 'var(--gold-900)',
16 | },
17 | gray: {
18 | 50: 'var(--gray-50)',
19 | 100: 'var(--gray-100)',
20 | 200: 'var(--gray-200)',
21 | 300: 'var(--gray-300)',
22 | 400: 'var(--gray-400)',
23 | 500: 'var(--gray-500)',
24 | 600: 'var(--gray-600)',
25 | 700: 'var(--gray-700)',
26 | 800: 'var(--gray-800)',
27 | 900: 'var(--gray-900)',
28 | },
29 | },
30 | },
31 | },
32 | plugins: [],
33 | };
34 |
--------------------------------------------------------------------------------
/packages/docs/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 |
3 | import docGen from './plugins/docGen';
4 |
5 | export default defineConfig({
6 | plugins: [docGen()],
7 |
8 | server: {
9 | port: 8080,
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | singleQuote: true,
3 | trailingComma: 'es5',
4 | importOrder: [
5 | '/types',
6 | '/mixins',
7 | '/composables',
8 | '/utils',
9 | '/components',
10 | '/directives',
11 | '^[./]',
12 | ],
13 | importOrderSeparation: true,
14 | importOrderSortSpecifiers: true,
15 | };
16 |
--------------------------------------------------------------------------------
/vetur.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | projects: ['./packages/chusho', './packages/docs'],
3 | };
4 |
--------------------------------------------------------------------------------