Compare commits

..

105 Commits

Author SHA1 Message Date
Benjamin Canac
0af5184c70 chore(release): 2.2.1 2023-05-27 12:27:53 +02:00
Benjamin Canac
44c3e2c46a chore(forms): remove required on Input, Select and Textarea name
Resolves #236
2023-05-27 12:03:29 +02:00
Benjamin Canac
a96dc19215 fix(FormGroup): missing h import from vue
Resolves #236
2023-05-27 12:02:51 +02:00
Benjamin Canac
aa881a8d00 chore(release): 2.2.0 2023-05-26 23:19:53 +02:00
Benjamin Canac
08413f198b scripts: update to pnpm 2023-05-26 22:46:17 +02:00
Benjamin Canac
75ab1d2ed5 chore(deps): bump 2023-05-26 22:25:58 +02:00
Sumit Kolhe
2d6ce654f4 docs: add close button to Slideover example (#211)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-05-26 22:20:34 +02:00
Benjamin Canac
9ce531a06f feat!: handle color states on form elements (#234) 2023-05-26 22:07:49 +02:00
Benjamin Canac
1a9dc5c980 fix(Notification): remove default color on icon 2023-05-26 18:28:52 +02:00
Benjamin Canac
589f86ef1b chore(Avatar): dark variant for chip background color 2023-05-26 18:28:32 +02:00
Benjamin Canac
1b61ec72e2 chore(Notification)!: rename progressColor to color and style icon
This also removes `progressVariant` prop
2023-05-26 18:03:54 +02:00
Benjamin Canac
1f22f84360 chore(Avatar)!: remove chipVariant prop 2023-05-26 18:02:48 +02:00
Benjamin Canac
2c6db975f9 chore(deps): switch to pnpm (#228) 2023-05-26 17:41:07 +02:00
Benjamin Canac
b7099aa0d3 chore(SelectMenu): add searchablePlaceholder prop
Resolves #231
2023-05-26 15:02:21 +02:00
Benjamin Canac
36b0869bc2 docs: fix prev card gap on first page 2023-05-23 16:57:42 +02:00
Benjamin Canac
28167e41ff docs: add VerticalNavigation tailwind example 2023-05-23 15:27:13 +02:00
Benjamin Canac
19923cbf1e chore(VerticalNavigation)!: split preset 2023-05-23 15:26:47 +02:00
Benjamin Canac
1210e99ec1 chore(VerticalNavigation): improve types import 2023-05-23 15:25:28 +02:00
Benjamin Canac
fc894bc1ae chore(Dropdown): improve types import 2023-05-23 15:25:12 +02:00
Benjamin Canac
9491ac7172 chore(CommandPalette): improve types import 2023-05-23 15:25:00 +02:00
Benjamin Canac
32dc2264d8 chore(types): export button 2023-05-23 15:24:41 +02:00
Benjamin Canac
45ba3b26da chore(Notification): improve types 2023-05-23 15:24:32 +02:00
Benjamin Canac
6d3309c42d chore(Notification): move padding to app.config 2023-05-23 11:25:56 +02:00
Benjamin Canac
530b85136d docs: handle color mode in volta embed 2023-05-23 11:11:19 +02:00
Benjamin Canac
cb9ed9ad3f chore(Button): inject NuxtLink in components 2023-05-22 19:05:39 +02:00
Benjamin Canac
524e220914 chore(VerticalNavigation): improve binds & types 2023-05-22 19:05:17 +02:00
Benjamin Canac
e3e6ef27a2 docs: improve Dropdown example with click and disabled 2023-05-22 19:04:39 +02:00
Benjamin Canac
55f115f9fe chore(Dropdown): use ULinkCustom + improve item binds & types
Fixes #215
2023-05-22 19:04:18 +02:00
Benjamin Canac
bdaf2dbbd4 chore(CommandPalette): handle loading state (#221) 2023-05-22 16:00:31 +02:00
Benjamin Canac
e7eea067b2 chore(Notification): add progressColor and progressVariant props (#219)
Co-authored-by: Sébastien Chopin <seb@nuxtjs.com>
2023-05-22 15:01:19 +02:00
Benjamin Canac
a56dbeab35 fix(Radio/Checkbox): remove ring offset on focus 2023-05-22 13:41:56 +02:00
Benjamin Canac
570b82d1e7 chore(Avatar): allow default value for chipColor through app.config.ts 2023-05-22 12:24:17 +02:00
Harry Yep
b5189c0c07 docs: LogoLabs not shown (#216) 2023-05-21 23:01:08 +02:00
Sébastien Chopin
8a0a5d8ba0 docs: pre-render component-meta routes 2023-05-20 19:13:58 +02:00
Sébastien Chopin
d3e5f4e15d docs: remove console.log 2023-05-20 18:53:32 +02:00
Sébastien Chopin
5a592b7ee0 docs: use CF rules for redirect 2023-05-20 18:49:39 +02:00
Sébastien Chopin
43787eca74 docs: move vercel.json to public dir 2023-05-20 18:41:07 +02:00
Sébastien Chopin
595ed9fb46 docs: add vercel redirect 2023-05-20 18:38:19 +02:00
Sébastien Chopin
5c4ab26d25 docs: support ssg 2023-05-20 18:31:56 +02:00
Sébastien Chopin
2030f24a47 docs: update logo on aside on mobile 2023-05-20 13:18:19 +02:00
Benjamin Canac
6eda322496 chore(VerticalNavigation): links badge type as number
Resolves #206
2023-05-19 15:55:18 +02:00
Benjamin Canac
318f8b2f08 docs: improve theming colors section 2023-05-19 15:00:39 +02:00
Benjamin Canac
dfab900562 docs: add badge in VerticalNavigation example 2023-05-19 14:51:31 +02:00
Benjamin Canac
d2ee5058f8 fix(VerticalNavigation): badge display
Resolves #205
2023-05-19 14:51:16 +02:00
Benjamin Canac
e358183165 docs: getting started title on index 2023-05-19 13:01:34 +02:00
Benjamin Canac
26579538f5 docs: prevent Alert text hover without link 2023-05-19 13:00:50 +02:00
Benjamin Canac
f99b9e283a chore(release): 2.1.0 2023-05-19 12:25:37 +02:00
Benjamin Canac
246449b328 fix(module): add .mjs extension to tailwind content when builded
Resolves #172
2023-05-19 12:22:05 +02:00
Benjamin Canac
ea740bf10a fix(Textarea): expose ref 2023-05-19 12:15:40 +02:00
Benjamin Canac
2ded24bec9 fix(Input): expose ref 2023-05-19 12:15:33 +02:00
Benjamin Canac
180a1df374 chore(CommandPalette): remove useless ref 2023-05-19 12:13:32 +02:00
Benjamin Canac
b9edf31aed chore(Button): remove useless ref 2023-05-19 12:12:59 +02:00
Benjamin Canac
526f84692e docs: exclude label prop on button icon section 2023-05-19 11:56:29 +02:00
Benjamin Canac
d66f4c5e46 docs: reduce gap on prev next card 2023-05-19 11:56:17 +02:00
Benjamin Canac
ec3fd88472 docs: rename xxs to 2xs in button group section
Resolves #203
2023-05-19 11:50:21 +02:00
Benjamin Canac
0b097352b4 docs: theming icons with all param 2023-05-19 11:28:52 +02:00
Benjamin Canac
85b10ba4ee chore(module): handle icons as string 2023-05-19 11:28:24 +02:00
Benjamin Canac
4ac0e0c481 docs: update edit on github link icon 2023-05-19 11:22:02 +02:00
Benjamin Canac
617567d6e5 chore(SelectMenu): empty option shorter text 2023-05-19 11:20:30 +02:00
Sébastien Chopin
e59fe42cc9 docs: improve page descriptions and prev/next (#184)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-05-19 10:33:09 +02:00
Sébastien Chopin
1743ea91d5 chore(Notifications): simplify how to position them (#190)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-05-19 10:01:43 +02:00
BoBruce
fa59ea9d83 docs: fix copy button overlap code content (#194) 2023-05-18 17:57:35 +02:00
Benjamin Canac
efb9e8a263 docs: add _blank target on stackblitz link 2023-05-17 17:46:59 +02:00
Sébastien Chopin
0931372157 docs(footer): fix when page is not defined 2023-05-17 17:17:22 +02:00
Arash
7f00ec6c3d fix(DocsPageHeader): github component link (#182) 2023-05-17 17:00:36 +02:00
Benjamin Canac
2bdeb04f56 docs: add CommandPalette async search example 2023-05-17 15:13:34 +02:00
Benjamin Canac
c834f401cd docs: lint on page footer button 2023-05-17 15:03:48 +02:00
Benjamin Canac
421f3bd2be chore(github): add stackblitz link in bug report issue template 2023-05-17 15:00:50 +02:00
Benjamin Canac
e63d5b74fc docs: add stackblitz playground link in installation page 2023-05-17 14:57:07 +02:00
Benjamin Canac
9ffcef9cd0 docs: add footer with nuxtlabs and github edit links 2023-05-17 14:56:54 +02:00
Benjamin Canac
1aa8376d4f docs: add Select component description 2023-05-17 13:12:11 +02:00
Benjamin Canac
94f24da723 docs: volta embed url with gray and primary 2023-05-17 13:08:01 +02:00
Sébastien Chopin
44457a0530 chore: fix node engine version (#176)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-05-17 12:46:41 +02:00
ciaasteczkowy
3fa10aa4eb fix(ButtonGroup/AvatarGroup): allow v-for (#173)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-05-17 12:46:26 +02:00
Benjamin Canac
ead46f6798 chore(deps): use @nuxt/eslint-config 2023-05-17 12:30:03 +02:00
Benjamin Canac
1bfcf31964 docs: add roadmap page with volta board 2023-05-17 12:16:52 +02:00
Benjamin Canac
b795f0f65b docs: handle layout without toc 2023-05-17 12:13:24 +02:00
Benjamin Canac
d93e995298 chore(colors): prevent err and add warning when specific colors dont exist 2023-05-17 11:02:29 +02:00
Benjamin Canac
00641b1439 docs: add warning to prevent double module installation 2023-05-17 10:59:48 +02:00
Benjamin Canac
07197c531e chore(github): add issue templates 2023-05-17 10:05:23 +02:00
Žan Fras
4ac9a613e3 docs: update icon theming installation commands (#166) 2023-05-17 09:54:17 +02:00
Manaus
5336436da0 docs: use preferred naming for Tailwind CSS (#168) 2023-05-17 09:52:44 +02:00
Benjamin Canac
7588eca148 docs: use a single svg icon 2023-05-16 17:33:31 +02:00
Sébastien Chopin
c0a4b28f54 docs: add badges 2023-05-16 16:52:35 +02:00
Benjamin Canac
157e34f2c0 docs: put DocsSearch on client only 2023-05-16 16:48:49 +02:00
Benjamin Canac
6675bcf396 docs: add favicon 2023-05-16 16:48:48 +02:00
Sébastien Chopin
3d43fc7a32 docs: Update README.md 2023-05-16 16:47:00 +02:00
Benjamin Canac
b3b349aab7 docs: improve icons installations with all package managers 2023-05-16 16:15:55 +02:00
Benjamin Canac
ab67659412 docs: fix command palette examples links 2023-05-16 15:23:29 +02:00
Benjamin Canac
c090a9e9cd docs: improve seo 2023-05-16 15:20:58 +02:00
Benjamin Canac
5e3357b696 docs: add social-preview 2023-05-16 15:20:48 +02:00
Benjamin Canac
11941bd581 chore(module): default primary to green 2023-05-16 15:18:12 +02:00
Benjamin Canac
036658b6de chore(readme): update 2023-05-16 15:17:54 +02:00
Benjamin Canac
86dd6092f3 docs: add @nuxthq/studio and @nuxt/devtools 2023-05-16 15:17:04 +02:00
Benjamin Canac
cba95de72b chore(Container): add as prop 2023-05-16 12:57:35 +02:00
Benjamin Canac
4d8dbfb912 chore(Card): add as prop 2023-05-16 12:57:35 +02:00
Benjamin Canac
703fdef9bd fix(app.config): trailing space 2023-05-16 12:57:35 +02:00
Sylvain Marroufin
c78d5ab0d3 docs(shortcuts): add config details (#155) 2023-05-16 12:48:10 +02:00
Benjamin Canac
10f3c0c271 docs: improve index description 2023-05-16 12:07:05 +02:00
Benjamin Canac
370c31296a chore(license): rename to md 2023-05-16 12:03:05 +02:00
Benjamin Canac
f5b57bb3c8 chore(license): add 2023-05-16 12:02:04 +02:00
Benjamin Canac
8286d15d1e chore(readme): improve 2023-05-16 12:01:58 +02:00
Benjamin Canac
f528a5ebc0 docs: move dark mode in theming 2023-05-16 12:00:33 +02:00
Benjamin Canac
5fea0eb8d5 chore(deps): bump @nuxtjs/tailwindcss 2023-05-15 19:02:38 +02:00
Benjamin Canac
5ede417bd8 chore(deps): bump 2023-05-15 14:14:19 +02:00
107 changed files with 11839 additions and 9579 deletions

12
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,12 @@
module.exports = {
root: true,
extends: ['@nuxt/eslint-config'],
rules: {
'vue/multi-word-component-names': 0,
'vue/max-attributes-per-line': ['error', {
singleline: {
max: 5
}
}]
}
}

View File

@@ -1,9 +0,0 @@
module.exports = {
root: true,
extends: [
'@nuxtjs/eslint-config-typescript'
],
rules: {
'vue/multi-word-component-names': 0
}
}

31
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@@ -0,0 +1,31 @@
---
name: Bug report
about: Report a bug report to help us improve the module.
title: ''
labels: 'bug'
assignees: ''
---
<!-- **IMPORTANT!**
Before reporting a bug, please make sure that you have read through our documentation and you think your problem is indeed an issue related to our module. -->
### Version
@nuxthq/ui: <!-- ex: v2.0.0 -->
nuxt: <!-- ex: v3.5.0 -->
### Reproduction Link
<!--
A minimal test case based on one of:
- a GitHub repository that can reproduce the bug
- https://stackblitz.com/edit/nuxtlabs-ui
-->
### Steps to reproduce
### What is Expected?
### What is actually happening?

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Nuxt Community Discord
url: https://discord.nuxtjs.org/
about: Consider asking questions about the module here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea or enhancement for the module.
title: ''
labels: 'enhancement'
assignees: ''
---
### Is your feature request related to a problem? Please describe.
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
### Describe the solution you'd like
<!-- A clear and concise description of what you want to happen. -->
### Describe alternatives you've considered
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
### Additional context
<!-- Add any other context or screenshots about the feature request here. -->

16
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Question
about: Ask a question about the module.
title: ''
labels: 'question'
assignees: ''
---
<!-- **IMPORTANT!**
Please make sure to look for an answer to your question in our documentation and the documentation before asking a question here.
If you have a general question regarding the module use Discord `modules` channel. Thanks!
Nuxt Discord: https://discord.nuxtjs.org/
-->

View File

@@ -22,30 +22,41 @@ jobs:
with:
node-version: ${{ matrix.node }}
- name: Checkout
- name: checkout
uses: actions/checkout@master
with:
persist-credentials: false
fetch-depth: 0
- name: Cache
uses: actions/cache@v3
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
path: node_modules
key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
version: 7
run_install: false
- name: Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: yarn
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Lint
run: yarn lint
run: pnpm run lint
- name: Typecheck
run: yarn typecheck
run: pnpm run typecheck
- name: Build
run: yarn build
run: pnpm run build
- name: Release Edge
if: github.event_name == 'push'

View File

@@ -22,30 +22,41 @@ jobs:
with:
node-version: ${{ matrix.node }}
- name: Checkout
- name: checkout
uses: actions/checkout@master
with:
persist-credentials: false
fetch-depth: 0
- name: Cache
uses: actions/cache@v3
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
path: node_modules
key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
version: 7
run_install: false
- name: Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: yarn
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Lint
run: yarn lint
run: pnpm run lint
- name: Typecheck
run: yarn typecheck
run: pnpm run typecheck
- name: Build
run: yarn build
run: pnpm run build
- name: Version Check
id: check

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ nuxt.d.ts
dist
.DS_Store
.history
.vercel

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
shamefully-hoist=true

View File

@@ -2,6 +2,51 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [2.2.1](https://github.com/nuxtlabs/ui/compare/v2.2.0...v2.2.1) (2023-05-27)
### Bug Fixes
* **FormGroup:** missing `h` import from `vue` ([a96dc19](https://github.com/nuxtlabs/ui/commit/a96dc192157725143503b1a5e4b404cb48dc9d3f)), closes [#236](https://github.com/nuxtlabs/ui/issues/236)
## [2.2.0](https://github.com/nuxtlabs/ui/compare/v2.1.0...v2.2.0) (2023-05-26)
### ⚠ BREAKING CHANGES
* handle color states on form elements (#234)
* **Notification:** rename `progressColor` to `color` and style icon
* **Avatar:** remove `chipVariant` prop
* **VerticalNavigation:** split preset
### Features
* handle color states on form elements ([#234](https://github.com/nuxtlabs/ui/issues/234)) ([9ce531a](https://github.com/nuxtlabs/ui/commit/9ce531a06f1a972bc003876162e0503c1bbbdbd8))
### Bug Fixes
* **Notification:** remove default color on icon ([1a9dc5c](https://github.com/nuxtlabs/ui/commit/1a9dc5c980d8477cdf9386a17e20fc9fec0d883e))
* **Radio/Checkbox:** remove ring offset on focus ([a56dbea](https://github.com/nuxtlabs/ui/commit/a56dbeab351a5c58e5bb49f5762669e2884c6483))
* **VerticalNavigation:** badge display ([d2ee505](https://github.com/nuxtlabs/ui/commit/d2ee5058f819fc17f281f323dab2f0b3d80cf7bd)), closes [#205](https://github.com/nuxtlabs/ui/issues/205)
* **Avatar:** remove `chipVariant` prop ([1f22f84](https://github.com/nuxtlabs/ui/commit/1f22f84360c20498eea8971b21db9293a4c9c3dc))
* **Notification:** rename `progressColor` to `color` and style icon ([1b61ec7](https://github.com/nuxtlabs/ui/commit/1b61ec72e292325d7776a4719f14a75bdb18e110))
* **VerticalNavigation:** split preset ([19923cb](https://github.com/nuxtlabs/ui/commit/19923cbf1edc6c6d4aefb9ffab9f908b116e1c69))
## [2.1.0](https://github.com/nuxtlabs/ui/compare/v2.0.4...v2.1.0) (2023-05-19)
### Bug Fixes
* **app.config:** trailing space ([703fdef](https://github.com/nuxtlabs/ui/commit/703fdef9bd4c0e26b0e38a13c30aff5b1d9d19aa))
* **ButtonGroup/AvatarGroup:** allow `v-for` ([#173](https://github.com/nuxtlabs/ui/issues/173)) ([3fa10aa](https://github.com/nuxtlabs/ui/commit/3fa10aa4ebf9ff7d443f8f2564dcaf9b63ce1fa8))
* **DocsPageHeader:** github component link ([#182](https://github.com/nuxtlabs/ui/issues/182)) ([7f00ec6](https://github.com/nuxtlabs/ui/commit/7f00ec6c3d059e5e78172a8e4bab905a7f02fa63))
* **Input:** expose ref ([2ded24b](https://github.com/nuxtlabs/ui/commit/2ded24bec90a5ea6665ab6895ced15d9dd49e8ef))
* **module:** add `.mjs` extension to tailwind `content` when builded ([246449b](https://github.com/nuxtlabs/ui/commit/246449b32850db805c1133151b8449687e7c14be)), closes [#172](https://github.com/nuxtlabs/ui/issues/172)
* **Textarea:** expose ref ([ea740bf](https://github.com/nuxtlabs/ui/commit/ea740bf10a6090545ed58ff26322ee3a679b5452))
### [2.0.4](https://github.com/nuxtlabs/ui/compare/v2.0.3...v2.0.4) (2023-05-15)

9
LICENSE.md Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2023 NuxtLabs
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,6 +1,22 @@
# @nuxthq/ui
# NuxtLabs UI
Components library as a Nuxt module using [TailwindCSS](https://tailwindcss.com) and [HeadlessUI](https://headlessui.com).
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![License][license-src]][license-href]
[![Nuxt][nuxt-src]][nuxt-href]
This module has been developed by [NuxtLabs](https://nuxtlabs.com/) for [Volta](https://volta.net) and [Nuxt Studio](https://nuxt.studio/). It provides everything related to UI when building a Nuxt application, including components, icons, colors, dark mode and also keyboard shortcuts.
[![social preview](https://repository-images.githubusercontent.com/428329515/5a18c5dd-bb58-4874-b6ef-1c44e4884344)](https://ui.nuxtlabs.com)
## Features
- Built with [Headless UI](https://headlessui.dev/) and [Tailwind CSS](https://tailwindcss.com/)
- HMR support through Nuxt App Config
- Dark mode support
- Keyboard shortcuts
- Bundled icons
- Fully typed
## Installation
@@ -8,13 +24,11 @@ Components library as a Nuxt module using [TailwindCSS](https://tailwindcss.com)
yarn add --dev @nuxthq/ui
```
Then, register the module in your `nuxt.config.js`:
Then, register the module in your `nuxt.config.ts`:
```js
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
buildModules: [
modules: [
'@nuxthq/ui'
]
})
@@ -29,3 +43,34 @@ If you want latest updates, please use `@nuxthq/ui-edge` in your `package.json`:
}
}
```
## Documentation
Visit http://ui.nuxtlabs.com to view the documentation.
## Credits
- [nuxt/nuxt](https://github.com/nuxt/nuxt)
- [nuxt-modules/color-mode](https://github.com/nuxt-modules/color-mode)
- [nuxt-modules/tailwindcss](https://github.com/nuxt-modules/tailwindcss)
- [tailwindlabs/tailwindcss](https://github.com/tailwindlabs/tailwindcss)
- [tailwindlabs/headlessui](https://github.com/tailwindlabs/headlessui)
- [vueuse/vueuse](https://github.com/vueuse/vueuse)
- [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons)
## License
Licensed under the [MIT license](https://github.com/nuxtlabs/ui/blob/dev/LICENSE.md).
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/@nuxthq/ui/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-href]: https://npmjs.com/package/@nuxthq/ui
[npm-downloads-src]: https://img.shields.io/npm/dm/@nuxthq/ui.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-downloads-href]: https://npmjs.com/package/@nuxthq/ui
[license-src]: https://img.shields.io/github/license/nuxtlabs/ui.svg?style=flat&colorA=18181B&colorB=28CF8D
[license-href]: https://github.com/nuxtlabs/ui/blob/main/LICENSE
[nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
[nuxt-href]: https://nuxt.com

View File

@@ -6,46 +6,51 @@
<div class="relative grid lg:grid-cols-10 lg:gap-8">
<DocsAside class="lg:col-span-2" />
<div class="relative lg:col-span-6 pt-8 pb-16">
<div class="relative pt-8 pb-16" :class="[toc ? 'lg:col-span-6' : 'lg:col-span-8']">
<DocsPageHeader />
<NuxtPage />
<hr class="border-gray-200 dark:border-gray-800 my-12">
<DocsPageFooter class="mt-12" />
<hr class="border-gray-200 dark:border-gray-800 my-6">
<DocsPrevNext />
<DocsFooter class="mt-16" />
</div>
<DocsToc class="lg:col-span-2 order-first lg:order-last" />
<DocsToc v-if="toc" class="lg:col-span-2 order-first lg:order-last" />
</div>
</UContainer>
<DocsSearch />
<ClientOnly>
<DocsSearch />
</ClientOnly>
<UNotifications />
</div>
</template>
<script setup lang="ts">
const colorScheme = usePreferredColorScheme()
const { toc } = useContent()
const colorMode = useColorMode()
// Computed
const href = computed(() => colorScheme.value === 'dark' ? '/icon-dark.svg' : '/icon-light.svg')
const color = computed(() => colorMode.value === 'dark' ? '#18181b' : 'white')
// Head
useHead({
titleTemplate: title => title && title !== 'nuxthq/ui' ? `${title} - nuxthq/ui` : 'nuxthq/ui',
titleTemplate: title => title && title.includes('NuxtLabs UI') ? title : `${title} - NuxtLabs UI`,
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' },
{ key: 'theme-color', name: 'theme-color', content: color }
],
link: [
{ rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' },
{ rel: 'icon', type: 'image/svg+xml', href }
{ rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' }
],
htmlAttrs: {
lang: 'en'
@@ -54,4 +59,10 @@ useHead({
class: 'antialiased font-sans text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-900'
}
})
useSeoMeta({
ogImage: '/social-preview.jpg',
twitterImage: '/social-preview.jpg',
twitterCard: 'summary_large_image'
})
</script>

View File

@@ -3,10 +3,10 @@
<UContainer>
<div class="flex items-center justify-between h-16">
<div class="flex items-center gap-3">
<NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-gray-900 dark:text-white">
<NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
nuxthq/ui
NuxtLabs<span class="text-primary-500 dark:text-primary-400">UI</span>
</NuxtLink>
</div>
@@ -62,10 +62,9 @@
<div class="px-4 sm:px-6 sticky top-0 border-b border-gray-900/10 dark:border-gray-50/[0.06] bg-white/75 dark:bg-gray-900/75 backdrop-blur z-10">
<div class="flex items-center justify-between h-16">
<div class="flex items-center gap-3">
<NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-gray-900 dark:text-white">
<NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
nuxthq/ui
NuxtLabs<span class="text-primary-500 dark:text-primary-400">UI</span>
</NuxtLink>
</div>

View File

@@ -0,0 +1,9 @@
<template>
<svg width="312" height="78" viewBox="0 0 312 78" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M65.6381 78H109.132C110.513 78.0002 111.87 77.6398 113.067 76.9552C114.263 76.2705 115.257 75.2857 115.947 74.0998C116.637 72.9139 117.001 71.5688 117 70.1996C116.999 68.8305 116.635 67.4856 115.944 66.3003L86.7343 16.1575C86.0439 14.9719 85.0508 13.9873 83.8547 13.3028C82.6586 12.6183 81.3017 12.2579 79.9205 12.2579C78.5393 12.2579 77.1825 12.6183 75.9864 13.3028C74.7903 13.9873 73.7971 14.9719 73.1067 16.1575L65.6381 28.9873L51.0356 3.89908C50.3446 2.71356 49.351 1.72914 48.1546 1.04472C46.9581 0.360308 45.6011 0 44.2196 0C42.8382 0 41.4811 0.360308 40.2847 1.04472C39.0883 1.72914 38.0947 2.71356 37.4037 3.89908L1.05644 66.3003C0.364965 67.4856 0.000601816 68.8305 7.44871e-07 70.1996C-0.000600326 71.5688 0.362582 72.9139 1.05302 74.0998C1.74346 75.2857 2.7368 76.2705 3.93315 76.9552C5.12949 77.6398 6.48665 78.0002 7.86812 78H35.1699C45.9872 78 53.9645 73.2907 59.4537 64.1032L72.7803 41.2289L79.9184 28.9873L101.341 65.7584H72.7803L65.6381 78ZM34.7248 65.7458L15.6717 65.7416L44.2324 16.7162L58.483 41.2289L48.9416 57.6127C45.2963 63.5739 41.155 65.7458 34.7248 65.7458Z" fill="currentColor" />
<path d="M175.417 77.3598V66.9562H149.03V21.3406H136.5V77.3598H175.417Z" fill="currentColor" />
<path d="M198.81 78C203.706 78 208.103 76.0793 210.178 73.1183V77.3598H221.795V37.026H210.178V41.0274C207.854 38.2264 203.706 36.3858 198.644 36.3858C186.446 36.3858 179.061 44.6286 179.061 57.1929C179.061 69.7572 186.446 78 198.81 78ZM200.635 68.3967C194.495 68.3967 190.429 63.9152 190.429 57.1929C190.429 50.3906 194.495 45.909 200.635 45.909C206.859 45.909 210.925 50.3906 210.925 57.1929C210.925 63.9152 206.859 68.3967 200.635 68.3967Z" fill="currentColor" />
<path d="M254.606 78C266.97 78 274.604 69.7572 274.604 57.1929C274.604 44.6286 266.97 36.3858 254.772 36.3858C249.544 36.3858 245.478 38.3064 243.155 41.2674V19.5H231.621V77.3598H243.155V72.9583C245.478 76.0793 249.793 78 254.606 78ZM252.78 68.3967C246.557 68.3967 242.491 63.9152 242.491 57.1929C242.491 50.3906 246.557 45.909 252.78 45.909C258.838 45.909 262.987 50.3906 262.987 57.1929C262.987 63.9152 258.838 68.3967 252.78 68.3967Z" fill="currentColor" />
<path d="M295.736 78C305.528 78 312 72.9583 312 65.2757C312 47.0294 289.098 56.3926 289.098 48.1498C289.098 45.5889 291.339 44.2285 294.575 44.2285C297.728 44.2285 300.964 46.0691 301.462 49.5103H311.502C311.087 41.5876 304.283 36.3858 294.243 36.3858C285.696 36.3858 279.307 41.4275 279.307 48.5499C279.307 65.5157 301.794 57.433 301.794 65.6758C301.794 67.9166 299.304 69.5971 295.736 69.5971C291.422 69.5971 288.517 67.3564 288.102 63.7551H278.145C278.56 72.4781 285.53 78 295.736 78Z" fill="currentColor" />
</svg>
</template>

View File

@@ -1,48 +1,52 @@
<template>
<div class="flex items-center shadow-sm">
<USelectMenu
v-model="primary"
name="primary"
class="w-full [&>div>button]:!rounded-r-none"
appearance="gray"
:ui="{ width: 'w-[194px]' }"
:popper="{ placement: 'bottom-start' }"
:options="primaryOptions"
>
<template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${primary.hex}`}" />
<ClientOnly>
<USelectMenu
v-model="primary"
name="primary"
class="w-full [&>div>button]:!rounded-r-none"
color="gray"
:ui="{ width: 'w-[194px]' }"
:popper="{ placement: 'bottom-start' }"
:options="primaryOptions"
>
<template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${primary.hex}`}" />
{{ primary.text }}
</template>
{{ primary.text }}
</template>
<template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
<template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
{{ option.text }}
</template>
</USelectMenu>
{{ option.text }}
</template>
</USelectMenu>
</ClientOnly>
<USelectMenu
v-model="gray"
name="gray"
class="w-full [&>div>button]:!rounded-l-none [&>div>button]:-ml-px"
appearance="gray"
:ui="{ width: 'w-[194px]' }"
:popper="{ placement: 'bottom-end' }"
:options="grayOptions"
>
<template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${gray.hex}`}" />
<ClientOnly>
<USelectMenu
v-model="gray"
name="gray"
class="w-full [&>div>button]:!rounded-l-none [&>div>button]:-ml-px"
color="gray"
:ui="{ width: 'w-[194px]' }"
:popper="{ placement: 'bottom-end' }"
:options="grayOptions"
>
<template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${gray.hex}`}" />
{{ gray.text }}
</template>
{{ gray.text }}
</template>
<template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
<template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
{{ option.text }}
</template>
</USelectMenu>
{{ option.text }}
</template>
</USelectMenu>
</ClientOnly>
</div>
</template>
@@ -84,4 +88,43 @@ const gray = computed({
grayCookie.value = option.value
}
})
// Hack for SSG
const hexToRgb = (hex) => {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
hex = hex.replace(shorthandRegex, function (_, r, g, b) {
return r + r + g + g + b + b
})
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}`
: null
}
const root = computed(() => {
return `:root {
${Object.entries(colors[primary.value.value] || colors.green).map(([key, value]) => `--color-primary-${key}: ${hexToRgb(value)};`).join('\n')}
${Object.entries(colors[gray.value.value] || colors.cool).map(([key, value]) => `--color-gray-${key}: ${hexToRgb(value)};`).join('\n')}
}`
})
if (process.client) {
watch(root, () => {
window.localStorage.setItem('nuxt-ui-root', root.value)
}, { immediate: true })
}
if (process.server) {
useHead({
script: [
{
innerHTML: `
if (localStorage.getItem('nuxt-ui-root')) {
document.querySelector('style#nuxt-ui-colors').innerHTML = localStorage.getItem('nuxt-ui-root')
}`.replace(/\s+/g, ' '),
type: 'text/javascript',
tagPriority: -1
}
]
})
}
</script>

View File

@@ -2,8 +2,8 @@
<component
:is="to ? NuxtLink : 'div'"
:to="to"
class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-200 text-sm leading-6 my-5 last:mb-0 font-normal group relative"
:class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed' : '']"
class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm leading-6 my-5 last:mb-0 font-normal group relative"
:class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed hover:text-gray-800 dark:hover:text-gray-200' : '']"
>
<UIcon v-if="!!to" name="i-heroicons-link-20-solid" class="w-3 h-3 absolute right-2 top-2 text-gray-400 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400" />

View File

@@ -7,7 +7,7 @@
v-if="prop.type === 'boolean'"
v-model="componentProps[prop.name]"
:name="prop.name"
appearance="none"
variant="none"
class="justify-center"
/>
<USelectMenu
@@ -16,7 +16,7 @@
:options="prop.options"
:name="prop.name"
:label="componentProps[prop.name]"
appearance="none"
variant="none"
class="inline-flex"
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md' }"
:ui-select="{ custom: '!py-0' }"
@@ -27,7 +27,7 @@
:model-value="componentProps[prop.name]"
:type="prop.type === 'number' ? 'number' : 'text'"
:name="prop.name"
appearance="none"
variant="none"
autocomplete="off"
:ui="{ custom: '!py-0' }"
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
@@ -49,6 +49,7 @@
// @ts-expect-error
import { transformContent } from '@nuxt/content/transformers'
// eslint-disable-next-line vue/no-dupe-keys
const props = defineProps({
slug: {
type: String,
@@ -78,17 +79,23 @@ const props = defineProps({
type: Array,
default: () => []
},
extraColors: {
type: Array,
default: () => []
},
backgroundClass: {
type: String,
default: 'bg-white dark:bg-gray-900'
}
})
// eslint-disable-next-line vue/no-dupe-keys
const baseProps = reactive({ ...props.baseProps })
const componentProps = reactive({ ...props.props })
const appConfig = useAppConfig()
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`
@@ -97,6 +104,7 @@ const meta = await fetchComponentMeta(name)
// Computed
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => ({ ...appConfig.ui[camelName], ...props.ui }))
const fullProps = computed(() => ({ ...props.baseProps, ...componentProps }))
@@ -117,7 +125,8 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
const keys = useGet(ui.value, dottedKey, {})
let options = typeof keys === 'object' && Object.keys(keys)
if (key.toLowerCase().endsWith('color')) {
options = appConfig.ui.colors
// @ts-ignore
options = [...appConfig.ui.colors, ...props.extraColors]
}
return {
@@ -128,6 +137,7 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
}
}).filter(Boolean))
// eslint-disable-next-line vue/no-dupe-keys
const code = computed(() => {
let code = `\`\`\`html
<${name}`

View File

@@ -15,6 +15,7 @@ const props = defineProps({
const appConfig = useAppConfig()
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`

View File

@@ -43,6 +43,7 @@ const props = defineProps({
})
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`

View File

@@ -26,6 +26,7 @@ const props = defineProps({
})
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`

View File

@@ -0,0 +1,17 @@
<template>
<iframe :src="src" class="w-full min-h-[calc(100vh/1.5)] border border-gray-200 dark:border-gray-800 rounded-md" />
</template>
<script setup lang="ts">
const props = defineProps({
token: {
type: String,
required: true
}
})
const appConfig = useAppConfig()
const colorMode = useColorMode()
const src = computed(() => `https://volta.net/embed/${props.token}?theme=${colorMode.value}&gray=${appConfig.ui.gray}&primary=${appConfig.ui.primary}`)
</script>

View File

@@ -0,0 +1,21 @@
<script setup>
const groups = computed(() => {
return [{
key: 'users',
label: q => q && `Users matching “${q}”...`,
search: async (q) => {
if (!q) {
return []
}
const users = await $fetch(`https://jsonplaceholder.typicode.com/users`, { params: { q } })
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email }))
}
}].filter(Boolean)
})
</script>
<template>
<UCommandPalette :groups="groups" :autoselect="false" />
</template>

View File

@@ -8,11 +8,15 @@ const items = [
}], [{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
shortcuts: ['E']
shortcuts: ['E'],
click: () => {
console.log('Edit')
}
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid',
shortcuts: ['D']
shortcuts: ['D'],
disabled: true
}], [{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'

View File

@@ -7,8 +7,23 @@ const isOpen = ref(false)
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen">
<div class="p-4 h-full">
<Placeholder class="w-full h-full" />
<div class="p-4 sm:p-6 flex flex-col flex-1 gap-4 sm:gap-6">
<div class="flex items-center justify-between">
<h2 class="font-semibold text-gray-900 dark:text-white">
Title
</h2>
<UButton
icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="link"
size="md"
:padded="false"
@click="isOpen = false"
/>
</div>
<Placeholder class="flex-1 w-full" />
</div>
</USlideover>
</div>

View File

@@ -3,7 +3,8 @@ const links = [{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
},
badge: 100
}, {
label: 'Installation',
icon: 'i-heroicons-home',

View File

@@ -50,7 +50,7 @@ export default defineComponent({
<div class="group relative" :class="`language-${language}`">
<UButton
:icon="icon"
variant="link"
variant="solid"
class="absolute right-3 top-3 opacity-0 group-hover:opacity-100 transition-opacity z-[1]"
size="xs"
tabindex="-1"

View File

@@ -0,0 +1,28 @@
<script setup>
const links = [{
label: 'Installation',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
to: '/navigation/vertical-navigation'
}, {
label: 'Command Palette',
to: '/navigation/command-palette'
}]
</script>
<template>
<UVerticalNavigation
:links="links"
:ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-l -ml-px lg:leading-6',
padding: 'pl-4',
rounded: '',
font: '',
ring: '',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}"
/>
</template>

View File

@@ -10,8 +10,11 @@
class="mt-1"
:ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-l -ml-px lg:leading-6',
padding: 'pl-4',
base: 'group text-sm block border-l -ml-px lg:leading-6',
rounded: '',
font: '',
ring: '',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}"

View File

@@ -0,0 +1,10 @@
<template>
<footer class="flex items-center justify-end gap-1.5">
<div class="flex items-baseline gap-1.5 text-sm text-center text-gray-500 dark:text-gray-400">
Made by
<NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs">
<LogoLabs class="text-primary-500 w-14 h-auto dark:text-primary-400" />
</NuxtLink>
</div>
</footer>
</template>

View File

@@ -0,0 +1,17 @@
<template>
<div class="flex items-center justify-between gap-1.5">
<UButton
v-if="page"
:to="`https://github.com/nuxtlabs/ui/edit/dev/docs/content/${page._file}`"
label="Edit this page on GitHub"
color="primary"
variant="link"
:padded="false"
icon="i-heroicons-pencil-square"
/>
</div>
</template>
<script setup lang="ts">
const { page } = useContent()
</script>

View File

@@ -1,7 +1,7 @@
<template>
<header v-if="page" class="relative border-b border-gray-200 dark:border-gray-800 pb-8 mb-12">
<p class="mb-4 text-sm leading-6 font-semibold text-primary-500 dark:text-primary-400 capitalize">
{{ useLowerCase(page._dir) }}
{{ page._dir?.title ? page._dir.title : useLowerCase(page._dir) }}
</p>
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 tracking-tight dark:text-white">
@@ -22,7 +22,7 @@
label="GitHub"
icon="i-simple-icons-github"
color="white"
:to="`https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/${page._dir}/U${page.title}.vue`"
:to="`https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/${page._dir}/${page.title.replace(' ', '')}.vue`"
/>
</div>
</div>

View File

@@ -1,8 +1,15 @@
<template>
<div class="flex items-center justify-between">
<UButton v-if="prev" :label="prev.navigation?.title || prev.title" :to="prev._path" icon="i-heroicons-arrow-small-left-20-solid" color="white" />
<span v-else>&nbsp;</span>
<UButton v-if="next" :label="next.navigation?.title || next.title" :to="next._path" trailing-icon="i-heroicons-arrow-small-right-20-solid" color="white" />
<div class="grid gap-6 sm:grid-cols-2">
<DocsPrevNextCard v-if="prev" :title="prev.navigation?.title || prev.title" :description="prev.navigation?.description || prev.description" :to="prev._path" icon="i-heroicons-arrow-left-20-solid" />
<span v-else class="hidden sm:block">&nbsp;</span>
<DocsPrevNextCard
v-if="next"
:title="next.navigation?.title || next.title"
:description="next.navigation?.description || next.description"
:to="next._path"
icon="i-heroicons-arrow-right-20-solid"
class="text-right"
/>
</div>
</template>

View File

@@ -0,0 +1,36 @@
<template>
<NuxtLink :to="to" class="block px-5 py-8 border not-prose rounded-lg border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/25 group">
<div v-if="icon" class="inline-flex items-center rounded-full p-1.5 bg-gray-50 dark:bg-gray-800 group-hover:bg-primary-50 dark:group-hover:bg-primary-400/10 ring-1 ring-gray-300 dark:ring-gray-700 mb-4 group-hover:ring-primary-500/50 dark:group-hover:ring-primary-400/50">
<UIcon :name="icon" class="w-5 h-5 text-gray-900 dark:text-gray-100 group-hover:text-primary-500 dark:group-hover:text-primary-400" />
</div>
<p class="text-gray-900 dark:text-gray-50 font-medium text-[15px] mb-1">
{{ title }}
</p>
<p class="text-sm font-normal text-gray-500 dark:text-gray-400">
{{ description }}
</p>
</NuxtLink>
</template>
<script setup lang="ts">
defineProps({
icon: {
type: String,
default: null
},
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
},
to: {
type: String,
required: true
}
})
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="toc" class="sticky top-16 bg-white/75 dark:bg-gray-900/75 backdrop-blur group lg:self-start -mx-4 sm:-mx-6 lg:mx-0 px-4 sm:px-6 lg:pl-8 lg:pr-0">
<div class="sticky top-16 bg-white/75 dark:bg-gray-900/75 backdrop-blur group lg:self-start -mx-4 sm:-mx-6 lg:mx-0 px-4 sm:px-6 lg:pl-8 lg:pr-0">
<div class="py-3 lg:py-8 border-b border-dashed border-gray-200 dark:border-gray-800 lg:border-0">
<button class="flex items-center gap-2" tabindex="-1" @click="isTocOpen = !isTocOpen">
<span class="text-sm text-slate-900 font-semibold text-sm leading-6 dark:text-slate-100 truncate">Table of Contents</span>

View File

@@ -10,7 +10,16 @@ export async function fetchComponentMeta (name: string) {
if (state.value[name]) { return state.value[name] }
// Store promise to avoid multiple calls
state.value[name] = $fetch(`/api/component-meta/${name}`).then((meta) => {
// add to nitro prerender
if (process.server) {
const event = useRequestEvent()
event.node.res.setHeader(
'x-nitro-prerender',
[event.node.res.getHeader('x-nitro-prerender'), `/api/component-meta/${name}.json`].filter(Boolean).join(',')
)
}
state.value[name] = $fetch(`/api/component-meta/${name}.json`).then((meta) => {
state.value[name] = meta
})

View File

@@ -1,6 +1,9 @@
---
title: Introduction
description: 'Components library as a Nuxt module using TailwindCSS and HeadlessUI.'
description: 'Fully styled and customizable components for Nuxt.'
head:
title: 'NuxtLabs UI: Fully styled and customizable components for Nuxt'
description: 'It provides everything related to UI when building your Nuxt app. This includes components, icons, colors, dark mode but also keyboard shortcuts. Built with Headless UI and Tailwind CSS, published under MIT License.'
---
This module has been developed by the [NuxtLabs](https://nuxtlabs.com/) team for [Volta](https://volta.net) and [Nuxt Studio](https://nuxt.studio/), its goal is to provide everything related to UI when building a Nuxt app. This includes components, icons, colors, dark mode but also keyboard shortcuts.
@@ -11,7 +14,7 @@ This module has been developed by the [NuxtLabs](https://nuxtlabs.com/) team for
## Features
- Fully styled and customizable components
- Built with [Headless UI](https://headlessui.dev/) and [Tailwind CSS](https://tailwindcss.com/)
- HMR support through Nuxt App Config
- Dark mode support
- Keyboard shortcuts

View File

@@ -2,8 +2,6 @@
description: 'Learn how to install and configure the module in your Nuxt app.'
---
## Overview
1. Install `@nuxthq/ui` dependency to your project:
::code-group
@@ -30,10 +28,16 @@ export default defineNuxtConfig({
})
```
::alert
That's it! You can now use all the components and composables in your Nuxt app ✨
::alert{icon="i-heroicons-exclamation-triangle"}
As this module installs [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/) and [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) for you, you should remove them from your `modules` and `dependencies` if you've previously installed them manually.
::
## Playground
:u-button{icon="i-simple-icons-stackblitz" label="Play on StackBlitz" size="lg" to="https://stackblitz.com/edit/nuxtlabs-ui?file=app.config.ts,app.vue" target="_blank"}
## Options
| Key | Default | Description |
@@ -42,7 +46,7 @@ That's it! You can now use all the components and composables in your Nuxt app
| `global` | `false` | Expose components globally. |
| `icons` | `['heroicons']` | Icon collections to load. |
## Edge channel
## Edge
To use the latest updates pushed on the [`dev`](https://github.com/nuxtlabs/ui/tree/dev) branch, you can use `@nuxthq/ui-edge`.

View File

@@ -13,25 +13,37 @@ Components are based on a `primary` and a `gray` color. You can change them in y
```ts [app.config.ts]
export default defineAppConfig({
ui: {
primary: 'sky',
primary: 'green',
gray: 'cool'
}
})
```
As this module uses TailwindCSS under the hood, you can use any of the [TailwindCSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors. By default, the `primary` color is `sky` and the `gray` color is `cool`.
To provide dynamic colors that can be changed at runtime, this module uses CSS variables. As TailwindCSS already has a `gray` color, the module automatically renames it to `cool` to avoid conflicts (`coolGray` was renamed to `gray` when Tailwind CSS v3.0 was released).
::alert{icon="i-heroicons-light-bulb"}
Try to change the `primary` and `gray` colors in the navbar and see the colors change live.
Try to change the `primary` and `gray` colors in the navbar and see the documentation change live.
::
Components that have a `color` prop like [Avatar](/elements/avatar), [Badge](/elements/badge) and [Button](/elements/button) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default TailwindCSS colors.
As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors. By default, the `primary` color is `green` and the `gray` color is `cool`.
To provide dynamic colors that can be changed at runtime, this module uses CSS variables. As Tailwind CSS already has a `gray` color, the module automatically renames it to `cool` to avoid conflicts (`coolGray` was renamed to `gray` when Tailwind CSS v3.0 was released).
Likewise, you can't define a `primary` color in your `tailwind.config.ts` as it would conflict with the `primary` color defined by the module.
::alert{icon="i-heroicons-light-bulb"}
We'd advise you to use those colors in your components and pages, e.g. `text-primary-500 dark:text-primary-400`, `bg-gray-100 dark:bg-gray-900`, etc. so your app automatically adapts when changing your `app.config.ts`.
::
Components that have a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style) and [Notification](/overlays/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors.
## Dark mode
All the components are styled with dark mode in mind.
Thanks to [Tailwind CSS dark mode](https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually) `class` strategy and the [@nuxtjs/color-mode](https://github.com/nuxt-modules/color-mode) module, you literally have nothing to do.
## Components
Components are styled with TailwindCSS but classes are all defined in the default [app.config.ts](https://github.com/nuxtlabs/ui/blob/dev/src/runtime/app.config.ts) file. You can override them in your `app.config.ts`.
Components are styled with Tailwind CSS but classes are all defined in the default [app.config.ts](https://github.com/nuxtlabs/ui/blob/dev/src/runtime/app.config.ts) file. You can override them in your `app.config.ts`.
```ts [app.config.ts]
export default defineAppConfig({
@@ -109,10 +121,32 @@ Search the icon you want to use on https://icones.js.org built by [@antfu](https
Unlike the official [nuxt-icon](https://github.com/nuxt-modules/icon/) module, this module will not fetch any icon from the web and will only bundle the icons you use in your app thanks to [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons).
However, you will need to install the icon packages you want to use.
However, you will need to install either `@iconify/json` (full icon collections, 50MB) or the individual icon packages you want to use in your app.
```sh
yarn add --dev @iconify/json-{collection_name}
::code-group
```bash [yarn]
yarn add -D @iconify-json/{collection_name}
```
```bash [npm]
npm install -D @iconify-json/{collection_name}
```
```sh [pnpm]
pnpm i -D @iconify-json/{collection_name}
```
::
When using `@iconify/json`, you can specifiy `icons: 'all'` in your `nuxt.config.ts` to use any icon in your app.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
ui: {
icons: 'all'
}
})
```
You can easily replace all the default icons of the components in your `app.config.ts`.
@@ -150,6 +184,7 @@ export default defineAppConfig({
commandPalette: {
default: {
icon: 'i-octicon-search-24',
loadingIcon: 'i-octicon-sync-24',
selectedIcon: 'i-octicon-check-24',
empty: {
icon: 'i-octicon-search-24'
@@ -159,9 +194,3 @@ export default defineAppConfig({
}
})
```
## Dark mode
All the components are styled with dark mode in mind.
Thanks to [TailwindCSS dark mode](https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually) `class` strategy and the [@nuxtjs/color-mode](https://github.com/nuxt-modules/color-mode) module, you literally have nothing to do.

View File

@@ -48,6 +48,65 @@ defineShortcuts({
</script>
```
### `usingInput`
Prop: `usingInput?: string | boolean`
By default, `usingInput` is `false`, meaning it will only trigger when no input is focused. When set to `true`, the shortcut will also trigger when any input is focused.
For a more advanced behavior, `usingInput` can be set to the name of an input, so it only triggers when focusing this specific input.
```vue
<template>
<UInput v-model="query" name="queryInput" />
</template>
<script setup lang="ts">
const query = ref('')
defineShortcuts({
enter: {
usingInput: 'queryInput',
handler: () => {
// TODO
}
}
})
</script>
```
_`enter` shortcut will only trigger when `queryInput` is focused._
### `whenever`
Prop: `whenever?: WatchSource<Boolean>[]`
`whenever` allows to add constraints on the shortcut triggering behavior. This array can contain `Ref<Boolean>`, `ComputedRef<Boolean>` or `() => Boolean`.
```ts
defineShortcuts({
meta_k: {
usingInput: true,
handler: () => { isOpen.value = !isOpen.value }
},
escape: {
usingInput: true,
whenever: [isOpen],
handler: () => { isOpen.value = false }
}
})
```
_`escape` shortcut will only trigger when `isOpen` is `true`._
### Simple shortcut
In case the shortcut does not need any config, it can be written as a function.
```ts
defineShortcuts({
'?': () => openHelpModal()
})
```
## `useShortcuts`
To display shortcuts in your app according to the user's OS, you can use the `useShortcuts` composable.
@@ -61,3 +120,4 @@ const { metaSymbol } = useShortcuts()
<UKbd>{{ metaSymbol }}</UKbd>
</template>
```
_`metaSymbol` will display either `⌘` on MacOS or `Ctrl` on any other OS_

View File

@@ -0,0 +1,7 @@
---
title: Roadmap
description: Discover our Volta board for @nuxthq/ui development status.
toc: false
---
:volta-embed{token="eyJ2aWV3IjoiYm9hcmQiLCJib2FyZFN0YXR1c2VzIjpbInRyaWFnZSIsImJhY2tsb2ciLCJ0b2RvIiwiaW5fcHJvZ3Jlc3MiLCJpbl9yZXZpZXciLCJkb25lIiwicmVsZWFzZWQiXSwiYm9hcmRMaW5rZWRQcnMiOnRydWUsImxpc3RHcm91cCI6InN0YXRlIiwibGlzdE9yZGVyIjoiY3JlYXRlZF9hdCIsInRpbWVsaW5lWm9vbSI6Im1vbnRoIiwidGltZWxpbmVPcmRlciI6InN0YXRlIiwidGltZWxpbmVEaXNwbGF5IjoiYWxsX21pbGVzdG9uZXMiLCJmaWx0ZXJzIjp7fSwib3duZXIiOiJudXh0bGFicyIsIm5hbWUiOiJ1aSJ9"}

View File

@@ -0,0 +1 @@
title: Getting Started

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display an image that represents a resource or a group of resources.
---
## Usage
@@ -28,14 +29,15 @@ baseProps:
### Chip
Use the `chipColor`, `chipVariant` and `chipPosition` props to display a chip on the Avatar.
Use the `chip-color` and `chip-position` props to display a chip on the Avatar.
::component-card
---
props:
chipColor: 'primary'
chipVariant: 'solid'
chipPosition: 'top-right'
extraColors:
- gray
baseProps:
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
alt: 'Avatar'

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a short text to represent a status or a category.
---
## Usage

View File

@@ -1,5 +1,6 @@
---
github: true
description: Create a button with icon or link capabilities.
---
## Usage
@@ -112,7 +113,7 @@ Button
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `leading` and `trailing` props to set the icon position or the `leadingIcon` and `trailingIcon` props to set a different icon for each position.
Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.
::component-card
---
@@ -125,6 +126,7 @@ props:
trailing: false
excludedProps:
- icon
- label
---
::
@@ -161,7 +163,7 @@ Button
Use the `loading` prop to show a loading icon and disable the Button.
Use the `loadingIcon` prop to set a different icon or change it globally in `ui.button.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
Use the `loading-icon` prop to set a different icon or change it globally in `ui.button.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
::component-card
---
@@ -257,7 +259,7 @@ props:
size: 'sm'
ui:
size:
xxs: ''
2xs: ''
xs: ''
sm: ''
md: ''

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a list of actions in a dropdown menu.
headlessui:
label: 'Menu'
to: 'https://headlessui.com/vue/menu'
@@ -23,11 +24,15 @@ const items = [
}], [{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
shortcuts: ['E']
shortcuts: ['E'],
click: () => {
console.log('Edit')
}
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid',
shortcuts: ['D']
shortcuts: ['D'],
disabled: true
}], [{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display an icon from Iconify library.
---
## Usage

View File

@@ -1,6 +1,7 @@
---
github: true
title: 'Keyboard Key'
description: Display a keyboard key in a text block.
navigation:
title: 'Kbd'
---

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display an input field.
---
## Usage
@@ -11,6 +12,53 @@ baseProps:
---
::
### Style
Use the `color` and `variant` props to change the visual style of the Input.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size
Use the `size` prop to change the size of the Input.
@@ -37,25 +85,11 @@ props:
---
::
### Appearance
Use the `appearance` prop to change the style of the Input.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
appearance: 'white'
---
::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `leading` and `trailing` props to set the icon position or the `leadingIcon` and `trailingIcon` props to set a different icon for each position.
Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.
::component-card
---
@@ -64,9 +98,12 @@ baseProps:
placeholder: 'Search...'
props:
icon: 'i-heroicons-magnifying-glass-20-solid'
appearance: 'white'
size: 'sm'
color: 'white'
trailing: false
extraColors:
- white
- gray
excludedProps:
- icon
---
@@ -80,12 +117,9 @@ Use the `disabled` prop to disable the Input.
---
baseProps:
name: 'input'
props:
placeholder: 'Search...'
appearance: 'white'
props:
disabled: true
excludedProps:
- placeholder
---
::
@@ -93,7 +127,7 @@ excludedProps:
Use the `loading` prop to show a loading icon and disable the Input.
Use the `loadingIcon` prop to set a different icon or change it globally in `ui.input.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
Use the `loading-icon` prop to set a different icon or change it globally in `ui.input.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
::component-card
---
@@ -108,32 +142,6 @@ excludedProps:
---
::
### Group
You can use the `InputGroup` component to add a label and additional informations to a form element.
::component-card{slug="InputGroup"}
---
baseProps:
name: 'group'
props:
label: 'Email'
help: "We'll only use this for spam."
hint: 'Required'
required: true
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{name="group" placeholder="you@example.com" icon="i-heroicons-envelope"}
::
::alert{icon="i-heroicons-light-bulb"}
This also works with `Textarea`, `Select` and `SelectMenu` components.
::
## Props
:component-props

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a textarea field.
---
## Usage
@@ -11,6 +12,53 @@ baseProps:
---
::
### Style
Use the `color` and `variant` props to change the visual style of the Textarea.
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size
Use the `size` prop to change the size of the Textarea.
@@ -37,20 +85,6 @@ props:
---
::
### Appearance
Use the `appearance` prop to change the style of the Textarea.
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
appearance: 'white'
---
::
### Disabled
Use the `disabled` prop to disable the Textarea.
@@ -61,7 +95,6 @@ baseProps:
name: 'input'
placeholder: 'Search...'
props:
appearance: 'white'
disabled: true
---
::

View File

@@ -1,9 +1,12 @@
---
github: true
description: Display a select field.
---
## Usage
The Select component is a wrapper around the native `<select>` HTML element. For more advanced use cases like searching or multiple selection, consider using the [SelectMenu](/forms/select-menu) component.
::component-card
---
baseProps:
@@ -19,9 +22,65 @@ excludedProps:
---
::
### Style
Use the `color` and `variant` props to change the visual style of the Select.
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size
Use the `size` prop to change the size of the Input.
Use the `size` prop to change the size of the Select.
::component-card
---
@@ -53,29 +112,11 @@ props:
---
::
### Appearance
Use the `appearance` prop to change the style of the Select.
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
placeholder: 'Search...'
props:
appearance: 'white'
---
::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `trailingIcon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
Use the `trailing-icon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
::component-card
---
@@ -88,8 +129,11 @@ baseProps:
placeholder: 'Search...'
props:
icon: 'i-heroicons-magnifying-glass-20-solid'
appearance: 'white'
color: 'white'
size: 'sm'
extraColors:
- white
- gray
excludedProps:
- icon
---
@@ -97,7 +141,7 @@ excludedProps:
### Disabled
Use the `disabled` prop to disable the Input.
Use the `disabled` prop to disable the Select.
::component-card
---
@@ -109,7 +153,6 @@ baseProps:
- 'Mexico'
placeholder: 'Search...'
props:
appearance: 'white'
disabled: true
---
::

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a select menu with advanced features.
headlessui:
label: 'Listbox'
to: 'https://headlessui.com/vue/listbox'
@@ -7,7 +8,7 @@ headlessui:
## Usage
The SelectMenu component renders by default a [Select](/forms/select) component and is based on the `ui.select` preset. You can use most of the Select props to configure the display if you don't want to override the default slot such as [size](/forms/select#size), [placeholder](/forms/select#placeholder), [appearance](/forms/select#appearance), [icon](/forms/select#icon), [disabled](/forms/select#disabled), etc.
The SelectMenu component renders by default a [Select](/forms/select) component and is based on the `ui.select` preset. You can use most of the Select props to configure the display if you don't want to override the default slot such as [color](/forms/select#style), [variant](/forms/select#style), [size](/forms/select#size), [placeholder](/forms/select#placeholder), [icon](/forms/select#icon), [disabled](/forms/select#disabled), etc.
Like the Select component, you can use the `options` prop to pass an array of strings or objects.
@@ -80,7 +81,7 @@ const selected = ref(people[3])
```
::
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `optionAttribute` prop that defaults to `label`.
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `option-attribute` prop that defaults to `label`.
::component-example
#default
@@ -135,9 +136,9 @@ const selected = ref(people[0])
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `trailingIcon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
Use the `trailing-icon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
Use the `selectedIcon` prop to set a different icon or change it globally in `ui.selectMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
Use the `selected-icon` prop to set a different icon or change it globally in `ui.selectMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
::component-card
---
@@ -147,6 +148,10 @@ baseProps:
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
icon: 'i-heroicons-magnifying-glass-20-solid'
color: 'white'
extraColors:
- white
- gray
excludedProps:
- icon
---
@@ -156,7 +161,9 @@ excludedProps:
Use the `searchable` prop to enable search.
This will use HeadlessUI [Combobox](https://headlessui.com/vue/combobox) component instead of [Listbox](https://headlessui.com/vue/listbox).
Use the `searchable-placeholder` prop to set a different placeholder.
This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) component instead of [Listbox](https://headlessui.com/vue/listbox).
::component-card
---
@@ -166,6 +173,7 @@ baseProps:
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
searchable: true
searchablePlaceholder: 'Search a person...'
---
::

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a checkbox field.
---
## Usage

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a radio field.
---
## Usage

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a toggle field.
headlessui:
label: 'Switch'
to: 'https://headlessui.com/vue/switch'

View File

@@ -0,0 +1,140 @@
---
github: true
description: Display a label and additional informations around a form element.
---
## Usage
Use the FormGroup component around an [Input](/forms/input), [Textarea](/forms/textarea), [Select](/forms/select) or a [SelectMenu](/forms/select-menu) with the `name` prop to automatically associate a `<label>` element with the form element.
::component-card
---
props:
name: 'email'
label: 'Email'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Required
Use the `required` prop to indicate that the form element is required.
::component-card
---
baseProps:
name: 'group-required'
props:
label: 'Email'
required: true
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Description
Use the `description` prop to display a description below the label.
::component-card
---
baseProps:
name: 'group-description'
props:
label: 'Email'
description: "We'll only use this for spam."
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Hint
Use the `hint` prop to display a hint above the form element.
::component-card
---
baseProps:
name: 'group-hint'
props:
label: 'Email'
hint: 'Optional'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Help
Use the `help` prop to display an help message below the form element.
::component-card
---
baseProps:
name: 'group-help'
props:
label: 'Email'
help: 'We will never share your email with anyone else.'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Error
Use the `error` prop to display an error message below the form element.
When used together with the `help` prop, the `error` prop will take precedence.
::component-card
---
baseProps:
name: 'group-error'
props:
label: 'Email'
help: 'We will never share your email with anyone else.'
error: "Not a valid email address."
code: >-
<UInput placeholder="you@example.com" trailing-icon="i-heroicons-exclamation-triangle-20-solid" />
---
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com" trailing-icon="i-heroicons-exclamation-triangle-20-solid"}
::
You can also use the `error` prop as a boolean to mark the form element as invalid.
::alert{icon="i-heroicons-light-bulb"}
The `error` prop will automatically set the `color` prop of the form element to `red`.
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a vertical navigation.
---
## Usage
@@ -15,7 +16,8 @@ const links = [{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
},
badge: 100
}, {
label: 'Installation',
icon: 'i-heroicons-home',
@@ -37,6 +39,48 @@ const links = [{
```
::
## Themes
Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do.
### Tailwind
::component-example
#default
:vertical-navigation-theme-tailwind
#code
```vue
<script setup>
const links = [{
label: 'Installation',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
to: '/navigation/vertical-navigation'
}, {
label: 'Command Palette',
to: '/navigation/command-palette'
}]
</script>
<template>
<UVerticalNavigation
:links="links"
:ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-l -ml-px lg:leading-6',
padding: 'pl-4',
rounded: '',
font: '',
ring: '',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}"
/>
</template>
```
::
## Props
:component-props

View File

@@ -1,5 +1,6 @@
---
github: true
description: Add a customizable command palette to your app.
headlessui:
label: 'Combobox'
to: 'https://headlessui.com/vue/combobox'
@@ -159,7 +160,7 @@ function onSelect (option) {
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `selectedIcon` prop to set a different icon or change it globally in `ui.commandPalette.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
Use the `selected-icon` prop to set a different icon or change it globally in `ui.commandPalette.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
::component-card
---
@@ -173,6 +174,24 @@ excludedProps:
---
::
### Loading
Use the `loading` prop to show a loading icon.
Use the `loading-icon` prop to set a different icon or change it globally in `ui.commandPalette.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
::component-card
---
padding: false
baseProps:
empty: null
props:
loading: true
excludedProps:
- icon
---
::
### Placeholder
Use the `placeholder` prop to change the input placeholder
@@ -217,6 +236,8 @@ Use the `empty` prop to display a message when there are no results.
You can pass an `object` through the `empty` prop or globally through `ui.commandPalette.default.empty`. Here is the default:
You can also set it to `null` to hide the empty label.
::component-card
---
padding: false
@@ -236,7 +257,7 @@ excludedProps:
The CommandPalette component takes care of the full-text search for you with [Fuse.js](https://fusejs.io). You can pass all the options of Fuse.js through the `fuse` prop.
When searching for a command, the component will look for a `label` property on the command by default. You can customize this behaviour by overriding the `commandAttribute` prop. This will also affect the display of the command.
When searching for a command, the component will look for a `label` property on the command by default. You can customize this behaviour by overriding the `command-attribute` prop. This will also affect the display of the command.
You can also highlight the matches in the command by setting the `fuse.fuseOptions.includeMatches` to `true`. The CommandPalette component automatically takes care of the highlighting for you.
@@ -261,6 +282,47 @@ You can also highlight the matches in the command by setting the `fuse.fuseOptio
Try it yourself in this documentation's search by pressing :badge-shortcut{value="meta"} :badge-shortcut{value="K" class="ml-1"}.
::
## Async search
You can also pass an `async` function to the `search` property of a group to perform an async search. The function will receive the query as its first argument and should return an array of commands.
::component-example
---
padding: false
---
#default
:command-palette-example-async{class="h-[274px]"}
#code
```vue
<script setup>
const groups = computed(() => {
return [{
key: 'users',
label: q => q && `Users matching “${q}”...`,
search: async (q) => {
if (!q) {
return []
}
const users = await $fetch(`https://jsonplaceholder.typicode.com/users`, { params: { q } })
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email }))
}
}].filter(Boolean)
})
</script>
<template>
<UCommandPalette :groups="groups" />
</template>
```
::
::alert{icon="i-heroicons-light-bulb"}
The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behaviour by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`.
::
## Themes
Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do.
@@ -276,7 +338,7 @@ padding: false
:command-palette-theme-algolia{class="max-h-[480px] rounded-md"}
::
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/docs/rework/docs/components/content/themes/CommandPaletteThemeAlgolia.vue#L23"}
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeAlgolia.vue#L23"}
Take a look at the component!
::
@@ -291,7 +353,7 @@ padding: false
:command-palette-theme-raycast{class="max-h-[480px] rounded-md"}
::
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/docs/rework/docs/components/content/themes/CommandPaletteThemeRaycast.vue#L30"}
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeRaycast.vue#L30"}
Take a look at the component!
::

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a modal within your application.
headlessui:
label: 'Dialog'
to: 'https://headlessui.com/vue/dialog'

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a dialog that slides in from the edge of the screen.
headlessui:
label: 'Dialog'
to: 'https://headlessui.com/vue/dialog'

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a non-modal dialog that floats around a trigger element.
headlessui:
label: 'Popover'
to: 'https://headlessui.com/vue/popover'

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display content that appears on hover next to an element.
---
## Usage

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a menu that appears on right click.
---
## Usage

View File

@@ -1,13 +1,12 @@
---
github: true
description: Display a toast notification in your app.
---
## Usage
First of all, add the `Notifications` component to your app, preferably inside `app.vue`.
This component will render by default the notifications at the bottom right of the screen. You can configure its behaviour in the `app.config.ts` through `ui.notifications`.
```vue [app.vue]
<template>
<div>
@@ -37,6 +36,19 @@ const toast = useToast()
```
::
This component will render by default the notifications at the bottom right of the screen. You can configure its behaviour in the `app.config.ts` through `ui.notifications`:
```ts [app.config.ts]
export default defineAppConfig({
ui: {
notifications: {
// Show toasts at the top right of the screen
position: 'top-0 right-0'
}
}
})
```
### Description
You can add a `description` in addition of the `title`.
@@ -80,8 +92,8 @@ baseProps:
id: 4
timeout: 0
title: 'Notification'
description: 'This is a notification.'
props:
description: 'This is a notification.'
avatar:
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
excludedProps:
@@ -102,7 +114,28 @@ baseProps:
title: 'Notification'
description: 'This is a notification.'
props:
timeout: 10000
timeout: 60000
---
::
### Color
Use the `color` prop to change the progress and icon color of the Notification.
::component-card
---
baseProps:
id: 5
title: 'Notification'
description: 'This is a notification.'
timeout: 600000
props:
icon: 'i-heroicons-x-circle'
color: 'red'
extraColors:
- gray
excludedProps:
- icon
---
::

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a card for content with a header, body and footer.
---
## Usage

View File

@@ -1,5 +1,6 @@
---
github: true
description: A container lets you center and constrain the width of your content.
---
## Usage

View File

@@ -1,5 +1,6 @@
---
github: true
description: Display a placeholder while content is loading.
---
## Usage

View File

@@ -6,6 +6,8 @@ export default defineNuxtConfig({
ui,
'@vueuse/nuxt',
'@nuxt/content',
'@nuxt/devtools',
'@nuxthq/studio',
'@nuxtjs/plausible',
'nuxt-lodash',
'nuxt-component-meta'
@@ -15,6 +17,7 @@ export default defineNuxtConfig({
highlight: {
theme: {
light: 'material-lighter',
default: 'material-default',
dark: 'material-palenight'
},
preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'diff', 'shell', 'markdown', 'yaml', 'bash', 'ini']
@@ -28,11 +31,18 @@ export default defineNuxtConfig({
strict: false,
includeWorkspace: true
},
// @ts-ignore
$production: {
routeRules: {
'/api/_content/**': { isr: true, static: true },
'/api/component-meta/**': { isr: true, static: true }
routeRules: {
'/': { redirect: '/getting-started' }
},
generate: {
routes: ['/getting-started']
},
componentMeta: {
metaFields: {
props: true,
slots: false,
events: false,
exposed: false
}
}
})

View File

@@ -1,7 +0,0 @@
<template>
<div />
</template>
<script setup lang="ts">
await navigateTo('/getting-started')
</script>

BIN
docs/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,3 +0,0 @@
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="#0C0C0D"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,3 +1,15 @@
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="white"/>
<style>
path {
fill: black;
}
@media (prefers-color-scheme: dark) {
path {
fill: white;
}
}
</style>
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@nuxthq/ui",
"version": "2.0.4",
"version": "2.2.1",
"repository": "https://github.com/nuxtlabs/ui",
"license": "MIT",
"exports": {
@@ -15,26 +15,27 @@
"dist"
],
"engines": {
"node": ">=18 <19"
"node": ">=16.14.0"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "nuxt-module-build",
"prepack": "yarn build",
"prepack": "pnpm build",
"dev": "nuxi dev docs",
"build:docs": "nuxi build docs",
"lint": "eslint --ext .ts,.js,.vue .",
"build:docs": "nuxi generate docs",
"lint": "eslint .",
"typecheck": "nuxi typecheck",
"prepare": "nuxi prepare docs",
"release": "yarn lint && standard-version && git push --follow-tags"
"release": "pnpm lint && standard-version && git push --follow-tags"
},
"dependencies": {
"@egoist/tailwindcss-icons": "^1.0.7",
"@headlessui/vue": "1.7.10",
"@iconify-json/heroicons": "^1.1.10",
"@nuxt/kit": "^3.4.3",
"@nuxt/kit": "^3.5.1",
"@nuxtjs/color-mode": "^3.2.0",
"@nuxtjs/tailwindcss": "^6.6.8",
"@popperjs/core": "^2.11.7",
"@nuxtjs/tailwindcss": "^6.7.0",
"@popperjs/core": "^2.11.8",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
@@ -47,17 +48,19 @@
"tailwindcss": "^3.3.2"
},
"devDependencies": {
"@iconify-json/simple-icons": "^1.1.52",
"@iconify-json/simple-icons": "^1.1.54",
"@nuxt/content": "^2.6.0",
"@nuxt/module-builder": "^0.3.1",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
"@nuxt/devtools": "^0.5.5",
"@nuxt/eslint-config": "^0.1.1",
"@nuxt/module-builder": "^0.4.0",
"@nuxthq/studio": "^0.12.1",
"@nuxtjs/plausible": "^0.2.1",
"@types/lodash-es": "^4.17.7",
"@types/node": "^20.1.2",
"@types/node": "^20.2.4",
"@vueuse/nuxt": "^10.1.2",
"eslint": "^8.40.0",
"nuxt": "^3.4.3",
"nuxt-component-meta": "^0.5.1",
"eslint": "^8.41.0",
"nuxt": "^3.5.1",
"nuxt-component-meta": "^0.5.3",
"nuxt-lodash": "^2.4.1",
"standard-version": "^9.5.0",
"unbuild": "^1.2.1",

10100
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
#!/bin/bash
# Restore all git changes
git restore -s@ -SW -- example src test
git restore -s@ -SW -- .
# Bump versions to edge
yarn jiti ./scripts/bump-edge
pnpm jiti ./scripts/bump-edge
# Resolve yarn
yarn
# Resolve pnpm
pnpm install
# Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then

View File

@@ -1,10 +1,10 @@
#!/bin/bash
# Restore all git changes
git restore -s@ -SW -- example src test
git restore -s@ -SW -- .
# Resolve yarn
yarn
# Resolve pnpm
pnpm install
# Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then

View File

@@ -39,7 +39,7 @@ export interface ModuleOptions {
*/
global?: boolean
icons: string[]
icons: string[] | string
}
export default defineNuxtModule<ModuleOptions>({
@@ -115,51 +115,55 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.appConfig.ui = {
...nuxt.options.appConfig.ui,
primary: 'sky',
primary: 'green',
gray: 'cool',
colors: variantColors
}
tailwindConfig.safelist = tailwindConfig.safelist || []
tailwindConfig.safelist.push(...['bg-gray-400', {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|400|500)`)
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-500`),
variants: ['disabled']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(400|950)`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(500|900|950)`),
variants: ['dark:hover']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`),
variants: ['dark:disabled']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|100|600)`),
variants: ['hover']
}, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-500`),
variants: ['focus-visible']
}, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-500`),
variants: ['focus-visible']
}, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-400`),
variants: ['dark', 'dark:focus-visible']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-600`),
variants: ['hover']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-500`),
variants: ['dark:hover']
}])
tailwindConfig.safelist.push(...[
'bg-gray-500',
'dark:bg-gray-400',
{
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|400|500)`)
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-500`),
variants: ['disabled']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(400|950)`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(500|900|950)`),
variants: ['dark:hover']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`),
variants: ['dark:disabled']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|100|600)`),
variants: ['hover']
}, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-500`),
variants: ['focus-visible']
}, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-500`),
variants: ['focus', 'focus-visible']
}, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-400`),
variants: ['dark', 'dark:focus', 'dark:focus-visible']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-500`),
variants: ['dark:hover']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-600`),
variants: ['hover']
}
])
tailwindConfig.plugins = tailwindConfig.plugins || []
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
@@ -177,7 +181,7 @@ export default defineNuxtModule<ModuleOptions>({
require('@tailwindcss/typography')
],
content: [
resolve(runtimeDir, 'components/**/*.{vue,js,ts}'),
resolve(runtimeDir, 'components/**/*.{vue,mjs,ts}'),
resolve(runtimeDir, '*.{mjs,js,ts}')
]
}

View File

@@ -18,15 +18,13 @@ const avatar = {
},
chip: {
base: 'absolute block rounded-full ring-1 ring-white dark:ring-gray-900',
background: 'bg-{color}-500 dark:bg-{color}-400',
position: {
'top-right': 'top-0 right-0',
'bottom-right': 'bottom-0 right-0',
'top-left': 'top-0 left-0',
'bottom-left': 'bottom-0 left-0'
},
variant: {
solid: 'bg-{color}-400'
},
size: {
'3xs': 'h-1 w-1',
'2xs': 'h-1 w-1',
@@ -41,7 +39,7 @@ const avatar = {
},
default: {
size: 'sm',
chipVariant: 'solid',
chipColor: null,
chipPosition: 'top-right'
}
}
@@ -220,7 +218,9 @@ const kbd = {
const input = {
wrapper: 'relative',
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none',
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0',
rounded: 'rounded-md',
placeholder: 'placeholder-gray-400 dark:placeholder-gray-500',
custom: '',
size: {
'2xs': 'text-xs',
@@ -266,13 +266,21 @@ const input = {
xl: 'pr-12'
}
},
appearance: {
white: 'border-0 bg-white dark:bg-gray-900 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500',
gray: 'border-0 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500',
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none placeholder:text-gray-400 dark:placeholder:text-gray-500'
color: {
white: {
outline: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400',
},
gray: {
outline: 'shadow-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400',
}
},
variant: {
outline: 'shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 focus:ring-2 focus:ring-{color}-500 dark:focus:ring-{color}-400',
none: 'bg-transparent focus:ring-0 focus:shadow-none'
},
icon: {
base: 'text-gray-400 dark:text-gray-500',
base: 'flex-shrink-0 text-gray-400 dark:text-gray-500',
color: 'text-{color}-500 dark:text-{color}-400',
size: {
'2xs': 'h-3.5 w-3.5',
xs: 'h-4 w-4',
@@ -306,27 +314,32 @@ const input = {
},
default: {
size: 'sm',
appearance: 'white',
color: 'white',
variant: 'outline',
loadingIcon: 'i-heroicons-arrow-path-20-solid'
}
}
const inputGroup = {
const formGroup = {
wrapper: '',
label: 'block text-sm font-medium text-gray-700 dark:text-gray-200',
labelWrapper: 'flex content-center justify-between',
label: {
wrapper: 'flex content-center justify-between',
base: 'block text-sm font-medium text-gray-700 dark:text-gray-200',
required: `after:content-['*'] after:ml-0.5 after:text-red-500 dark:after:text-red-400`
},
description: 'text-sm text-gray-500 dark:text-gray-400',
container: 'mt-1 relative',
required: 'text-red-500 dark:text-red-400 ml-0.5',
description: 'text-sm leading-5 text-gray-500 dark:text-gray-400',
hint: 'text-sm leading-5 text-gray-500 dark:text-gray-400',
help: 'mt-2 text-sm text-gray-500 dark:text-gray-400'
hint: 'text-sm text-gray-500 dark:text-gray-400',
help: 'mt-2 text-sm text-gray-500 dark:text-gray-400',
error: 'mt-2 text-sm text-red-500 dark:text-red-400'
}
const textarea = {
...input,
default: {
size: 'sm',
appearance: 'white'
color: 'white',
variant: 'outline',
}
}
@@ -334,7 +347,8 @@ const select = {
...input,
default: {
size: 'sm',
appearance: 'white',
color: 'white',
variant: 'outline',
trailingIcon: 'i-heroicons-chevron-down-20-solid'
}
}
@@ -396,7 +410,7 @@ const selectMenu = {
const radio = {
wrapper: 'relative flex items-start',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus:ring-offset-white dark:focus:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
label: 'font-medium text-gray-700 dark:text-gray-200',
required: 'text-red-500 dark:text-red-400',
help: 'text-gray-500 dark:text-gray-400'
@@ -467,9 +481,14 @@ const skeleton = {
const verticalNavigation = {
wrapper: 'relative',
base: 'group flex items-center gap-2 text-sm font-medium rounded-md w-full relative focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:before:ring-inset focus-visible:before:ring-1 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 before:absolute before:inset-px before:rounded-md disabled:cursor-not-allowed disabled:opacity-75',
base: 'group relative flex items-center gap-2 focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-1 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 before:absolute before:inset-px before:rounded-md disabled:cursor-not-allowed disabled:opacity-75',
ring: 'focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
padding: 'px-3 py-1.5',
active: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800 ',
width: 'w-full',
rounded: 'rounded-md',
font: 'font-medium',
size: 'text-sm',
active: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800',
inactive: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50',
label: 'truncate relative',
icon: {
@@ -482,9 +501,9 @@ const verticalNavigation = {
size: '3xs'
},
badge: {
base: 'ml-auto inline-block py-0.5 px-2 text-xs rounded-md -mr-1 -my-0.5',
base: 'relative ml-auto inline-block py-0.5 px-2 text-xs rounded-md -mr-1 -my-0.5',
active: 'bg-white dark:bg-gray-900',
inactive: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 group-hover:bg-white dark:group-hover:bg-gray-900'
inactive: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white group-hover:bg-white dark:group-hover:bg-gray-900'
}
}
@@ -545,6 +564,7 @@ const commandPalette = {
},
default: {
icon: 'i-heroicons-magnifying-glass-20-solid',
loadingIcon: 'i-heroicons-arrow-path-20-solid',
empty: {
icon: 'i-heroicons-magnifying-glass-20-solid',
label: 'We couldn\'t find any items.',
@@ -694,10 +714,20 @@ const notification = {
background: 'bg-white dark:bg-gray-900',
shadow: 'shadow-lg',
rounded: 'rounded-lg',
padding: 'p-4',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
icon: 'flex-shrink-0 w-5 h-5 text-gray-900 dark:text-white',
avatar: 'flex-shrink-0 pt-0.5',
progress: 'absolute bottom-0 left-0 right-0 h-1 bg-primary-500 dark:bg-primary-400',
icon: {
base: 'flex-shrink-0 w-5 h-5 text-gray-400 dark:text-gray-500',
color: 'text-{color}-500 dark:text-{color}-400'
},
avatar: {
base: 'flex-shrink-0 self-center',
size: 'md'
},
progress: {
base: 'absolute bottom-0 left-0 right-0 h-1',
background: 'bg-{color}-500 dark:bg-{color}-400'
},
transition: {
enterActiveClass: 'transform ease-out duration-300 transition',
enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2',
@@ -707,6 +737,8 @@ const notification = {
leaveToClass: 'opacity-0'
},
default: {
color: 'primary',
icon: null,
close: {
icon: 'i-heroicons-x-mark-20-solid',
color: 'gray',
@@ -721,7 +753,9 @@ const notification = {
}
const notifications = {
wrapper: 'fixed bottom-0 right-0 flex flex-col justify-end w-full z-[55] sm:w-96',
wrapper: 'fixed flex flex-col justify-end z-[55]',
position: 'bottom-0 right-0',
width: 'w-full sm:w-96',
container: 'px-4 sm:px-6 py-6 space-y-3 overflow-y-auto'
}
@@ -735,7 +769,7 @@ export default {
dropdown,
kbd,
input,
inputGroup,
formGroup,
textarea,
select,
selectMenu,

View File

@@ -43,18 +43,11 @@ export default defineComponent({
},
chipColor: {
type: String,
default: null,
default: () => appConfig.ui.avatar.default.chipColor,
validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value)
}
},
chipVariant: {
type: String,
default: () => appConfig.ui.avatar.default.chipVariant,
validator (value: string) {
return Object.keys(appConfig.ui.avatar.chip.variant).includes(value)
}
},
chipPosition: {
type: String,
default: () => appConfig.ui.avatar.default.chipPosition,
@@ -94,7 +87,7 @@ export default defineComponent({
ui.value.chip.base,
ui.value.chip.size[props.size],
ui.value.chip.position[props.chipPosition],
ui.value.chip.variant[props.chipVariant]?.replaceAll('{color}', props.chipColor)
ui.value.chip.background.replaceAll('{color}', props.chipColor)
)
})

View File

@@ -1,7 +1,7 @@
import { h, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { classNames } from '../../utils'
import { classNames, getSlotsChildren } from '../../utils'
import Avatar from './Avatar.vue'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -34,15 +34,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defu({}, props.ui, appConfig.ui.avatarGroup))
const children = computed(() => {
let children = slots.default?.()
// @ts-ignore-next
if (children.length && children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
return children
})
const children = computed(() => getSlotsChildren(slots))
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max)

View File

@@ -1,7 +1,6 @@
<template>
<component
:is="buttonIs"
ref="button"
:class="buttonClass"
:aria-label="ariaLabel"
v-bind="buttonProps"
@@ -17,9 +16,9 @@
</template>
<script lang="ts">
import { ref, computed, defineComponent, useSlots } from 'vue'
import { computed, defineComponent, useSlots } from 'vue'
import type { PropType } from 'vue'
import type { RouteLocationNormalized, RouteLocationRaw } from 'vue-router'
import type { RouteLocationRaw } from 'vue-router'
import { defu } from 'defu'
import UIcon from '../elements/Icon.vue'
import { classNames } from '../../utils'
@@ -33,7 +32,8 @@ import appConfig from '#build/app.config'
export default defineComponent({
components: {
UIcon
UIcon,
NuxtLink
},
props: {
type: {
@@ -109,7 +109,7 @@ export default defineComponent({
default: false
},
to: {
type: [String, Object] as PropType<string | RouteLocationNormalized | RouteLocationRaw>,
type: [String, Object] as PropType<string | RouteLocationRaw>,
default: null
},
target: {
@@ -141,11 +141,9 @@ export default defineComponent({
const slots = useSlots()
const button = ref(null)
const buttonIs = computed(() => {
if (props.to) {
return NuxtLink
return 'NuxtLink'
}
return 'button'
@@ -217,7 +215,6 @@ export default defineComponent({
})
return {
button,
buttonIs,
buttonProps,
isLeading,

View File

@@ -1,6 +1,7 @@
import { h, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { getSlotsChildren } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -28,15 +29,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defu({}, props.ui, appConfig.ui.buttonGroup))
const children = computed(() => {
let children = slots.default?.()
// @ts-ignore-next
if (children.length && children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
return children
})
const children = computed(() => getSlotsChildren(slots))
const rounded = computed(() => ({
'rounded-none': { left: 'rounded-l-none', right: 'rounded-r-none' },

View File

@@ -20,9 +20,8 @@
<MenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background]" static>
<div v-for="(subItems, index) of items" :key="index" :class="ui.padding">
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
<Component
v-bind="omit(item, ['click'])"
:is="(item.to && NuxtLink) || (item.click && 'button') || 'div'"
<ULinkCustom
v-bind="omit(item, ['label', 'icon', 'iconClass', 'avatar', 'shortcuts', 'click'])"
:class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]"
@click="item.click"
>
@@ -36,7 +35,7 @@
<UKbd v-for="shortcut of item.shortcuts" :key="shortcut">{{ shortcut }}</UKbd>
</span>
</slot>
</Component>
</ULinkCustom>
</MenuItem>
</div>
</MenuItems>
@@ -48,17 +47,17 @@
<script lang="ts">
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router'
import type { RouteLocationRaw } from 'vue-router'
import { defineComponent, ref, computed, onMounted } from 'vue'
import { defu } from 'defu'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue'
import ULinkCustom from '../elements/LinkCustom.vue'
import { omit } from '../../utils'
import { usePopper } from '../../composables/usePopper'
import type { Avatar as AvatarType } from '../../types/avatar'
import type { Avatar } from '../../types/avatar'
import type { PopperOptions } from '../../types'
import { NuxtLink } from '#components'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -75,22 +74,23 @@ export default defineComponent({
MenuItem,
UIcon,
UAvatar,
UKbd
UKbd,
ULinkCustom
},
props: {
items: {
type: Array as PropType<{
to?: RouteLocationNormalized
exact?: boolean
label: string
disabled?: boolean
slot?: string
icon?: string
iconClass?: string
avatar?: Partial<AvatarType>
click?: Function
shortcuts?: string[]
}[][]>,
to?: string | RouteLocationRaw
exact?: boolean
label: string
slot?: string
icon?: string
iconClass?: string
avatar?: Partial<Avatar>
shortcuts?: string[]
disabled?: boolean
click?: Function
}[][]>,
default: () => []
},
mode: {
@@ -196,8 +196,7 @@ export default defineComponent({
container,
onMouseOver,
onMouseLeave,
omit,
NuxtLink
omit
}
}
})

View File

@@ -0,0 +1,82 @@
import { h, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { getSlotsChildren } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
props: {
name: {
type: String,
default: null
},
label: {
type: String,
default: null
},
description: {
type: String,
default: null
},
required: {
type: Boolean,
default: false
},
help: {
type: String,
default: null
},
error: {
type: [String, Boolean],
default: null
},
hint: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.formGroup>>,
default: () => appConfig.ui.formGroup
}
},
setup (props, { slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.formGroup>>(() => defu({}, props.ui, appConfig.ui.formGroup))
const children = computed(() => getSlotsChildren(slots))
const clones = computed(() => children.value.map((node) => {
if (props.error) {
node.props.oldColor = node.props.color
node.props.color = 'red'
} else {
node.props.color = node.props.oldColor
}
if (props.name) {
node.props.name = props.name
}
return node
}))
return () => h('div', { class: [ui.value.wrapper] }, [
props.label && h('div', { class: [ui.value.label.wrapper] }, [
h('label', { for: props.name, class: [ui.value.label.base, props.required && ui.value.label.required] }, props.label),
props.hint && h('span', { class: [ui.value.hint] }, props.hint)
]),
props.description && h('p', { class: [ui.value.description] }, props.description),
h('div', { class: [!!props.label && ui.value.container] }, [
...clones.value,
props.error && typeof props.error === 'string' ? h('p', { class: [ui.value.error] }, props.error) : props.help ? h('p', { class: [ui.value.help] }, props.help) : null
])
])
}
})

View File

@@ -55,7 +55,7 @@ export default defineComponent({
},
name: {
type: String,
required: true
default: null
},
placeholder: {
type: String,
@@ -113,6 +113,10 @@ export default defineComponent({
type: Boolean,
default: false
},
padded: {
type: Boolean,
default: true
},
size: {
type: String,
default: () => appConfig.ui.input.default.size,
@@ -120,11 +124,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.input.size).includes(value)
}
},
appearance: {
color: {
type: String,
default: () => appConfig.ui.input.default.appearance,
default: () => appConfig.ui.input.default.color,
validator (value: string) {
return Object.keys(appConfig.ui.input.appearance).includes(value)
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.input.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.input.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.input.variant),
...Object.values(appConfig.ui.input.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
ui: {
@@ -158,11 +172,15 @@ export default defineComponent({
})
const inputClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames(
ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size],
ui.value.padding[props.size],
ui.value.appearance[props.appearance],
props.padded && ui.value.padding[props.size],
variant?.replaceAll('{color}', props.color),
isLeading.value && ui.value.leading.padding[props.size],
isTrailing.value && ui.value.trailing.padding[props.size],
ui.value.custom
@@ -196,6 +214,7 @@ export default defineComponent({
const iconClass = computed(() => {
return classNames(
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size],
props.loading && 'animate-spin'
)
@@ -218,6 +237,7 @@ export default defineComponent({
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
input,
isLeading,
isTrailing,
inputClass,

View File

@@ -1,78 +0,0 @@
<template>
<div :class="ui.wrapper">
<div v-if="label || $slots.label" :class="ui.labelWrapper">
<label :for="name" :class="ui.label">
<slot name="label">{{ label }}</slot>
<span v-if="required" :class="ui.required">*</span>
</label>
<span v-if="$slots.hint || hint" :class="ui.hint">
<slot name="hint">{{ hint }}</slot>
</span>
</div>
<p v-if="description" :class="ui.description">
{{ description }}
</p>
<div :class="!!label && ui.container">
<slot />
<p v-if="help" :class="ui.help">
{{ help }}
</p>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
props: {
name: {
type: String,
default: null
},
label: {
type: String,
default: null
},
description: {
type: String,
default: null
},
required: {
type: Boolean,
default: false
},
help: {
type: String,
default: null
},
hint: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.inputGroup>>,
default: () => appConfig.ui.inputGroup
}
},
setup (props) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.inputGroup>>(() => defu({}, props.ui, appConfig.ui.inputGroup))
return {
// eslint-disable-next-line vue/no-dupe-keys
ui
}
}
})
</script>

View File

@@ -71,7 +71,7 @@ export default defineComponent({
},
name: {
type: String,
required: true
default: null
},
placeholder: {
type: String,
@@ -97,6 +97,10 @@ export default defineComponent({
type: Array,
default: () => []
},
padded: {
type: Boolean,
default: true
},
size: {
type: String,
default: () => appConfig.ui.select.default.size,
@@ -104,11 +108,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.select.size).includes(value)
}
},
appearance: {
color: {
type: String,
default: () => appConfig.ui.select.default.appearance,
default: () => appConfig.ui.select.default.color,
validator (value: string) {
return Object.keys(appConfig.ui.select.appearance).includes(value)
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.select.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.select.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.select.variant),
...Object.values(appConfig.ui.select.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
textAttribute: {
@@ -188,11 +202,15 @@ export default defineComponent({
})
const selectClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames(
ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size],
ui.value.padding[props.size],
ui.value.appearance[props.appearance],
props.padded && ui.value.padding[props.size],
variant?.replaceAll('{color}', props.color),
!!props.icon && ui.value.leading.padding[props.size],
ui.value.trailing.padding[props.size],
ui.value.custom
@@ -202,6 +220,7 @@ export default defineComponent({
const iconClass = computed(() => {
return classNames(
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size]
)
})

View File

@@ -53,7 +53,7 @@
ref="searchInput"
:display-value="() => query"
name="q"
placeholder="Search..."
:placeholder="searchablePlaceholder"
autofocus
autocomplete="off"
:class="ui.input"
@@ -101,7 +101,7 @@
</component>
<p v-else-if="searchable && query && !filteredOptions.length" :class="ui.option.empty">
<slot name="option-empty" :query="query">
No results found for "{{ query }}".
No results for "{{ query }}".
</slot>
</p>
</component>
@@ -186,6 +186,10 @@ export default defineComponent({
type: Boolean,
default: false
},
searchablePlaceholder: {
type: String,
default: 'Search...'
},
creatable: {
type: Boolean,
default: false
@@ -194,6 +198,10 @@ export default defineComponent({
type: String,
default: null
},
padded: {
type: Boolean,
default: true
},
size: {
type: String,
default: () => appConfig.ui.select.default.size,
@@ -201,11 +209,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.select.size).includes(value)
}
},
appearance: {
color: {
type: String,
default: () => appConfig.ui.select.default.appearance,
default: () => appConfig.ui.select.default.color,
validator (value: string) {
return Object.keys(appConfig.ui.select.appearance).includes(value)
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.select.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.select.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.select.variant),
...Object.values(appConfig.ui.select.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
optionAttribute: {
@@ -245,13 +263,17 @@ export default defineComponent({
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
const selectMenuClass = computed(() => {
const variant = uiSelect.value.color?.[props.color as string]?.[props.variant as string] || uiSelect.value.variant[props.variant]
return classNames(
uiSelect.value.base,
uiSelect.value.rounded,
uiSelect.value.placeholder,
'text-left cursor-default',
uiSelect.value.size[props.size],
uiSelect.value.gap[props.size],
uiSelect.value.padding[props.size],
uiSelect.value.appearance[props.appearance],
props.padded && uiSelect.value.padding[props.size],
variant?.replaceAll('{color}', props.color),
!!props.icon && uiSelect.value.leading.padding[props.size],
uiSelect.value.trailing.padding[props.size],
uiSelect.value.custom,
@@ -262,6 +284,7 @@ export default defineComponent({
const iconClass = computed(() => {
return classNames(
uiSelect.value.icon.base,
appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
uiSelect.value.icon.size[props.size]
)
})

View File

@@ -38,7 +38,7 @@ export default defineComponent({
},
name: {
type: String,
required: true
default: null
},
placeholder: {
type: String,
@@ -72,6 +72,10 @@ export default defineComponent({
type: Boolean,
default: false
},
padded: {
type: Boolean,
default: true
},
size: {
type: String,
default: () => appConfig.ui.textarea.default.size,
@@ -79,11 +83,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.textarea.size).includes(value)
}
},
appearance: {
color: {
type: String,
default: () => appConfig.ui.textarea.default.appearance,
default: () => appConfig.ui.textarea.default.color,
validator (value: string) {
return Object.keys(appConfig.ui.textarea.appearance).includes(value)
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.textarea.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.textarea.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.textarea.variant),
...Object.values(appConfig.ui.textarea.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
ui: {
@@ -146,11 +160,15 @@ export default defineComponent({
})
const textareaClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames(
ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size],
ui.value.padding[props.size],
ui.value.appearance[props.appearance],
props.padded && ui.value.padding[props.size],
variant?.replaceAll('{color}', props.color),
!props.resize && 'resize-none',
ui.value.custom
)
@@ -159,6 +177,7 @@ export default defineComponent({
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
textarea,
textareaClass,
onInput
}

View File

@@ -1,6 +1,6 @@
<template>
<component
:is="$attrs.onSubmit ? 'form': 'div'"
:is="$attrs.onSubmit ? 'form': as"
:class="[ui.base, ui.rounded, ui.divide, ui.ring, ui.shadow, ui.background]"
v-bind="$attrs"
>
@@ -30,6 +30,10 @@ import appConfig from '#build/app.config'
export default defineComponent({
inheritAttrs: false,
props: {
as: {
type: String,
default: 'div'
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.card>>,
default: () => appConfig.ui.card

View File

@@ -1,7 +1,7 @@
<template>
<div :class="[ui.base, ui.padding, ui.constrained]">
<component :is="as" :class="[ui.base, ui.padding, ui.constrained]">
<slot />
</div>
</component>
</template>
<script lang="ts">
@@ -17,6 +17,10 @@ import appConfig from '#build/app.config'
export default defineComponent({
props: {
as: {
type: String,
default: 'div'
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.container>>,
default: () => appConfig.ui.container

View File

@@ -1,6 +1,5 @@
<template>
<Combobox
ref="comboboxRef"
:by="by"
:model-value="modelValue"
:multiple="multiple"
@@ -9,7 +8,7 @@
>
<div :class="ui.wrapper">
<div v-show="searchable" :class="ui.input.wrapper">
<UIcon v-if="icon" :name="icon" :class="[ui.input.icon.base, ui.input.icon.size]" aria-hidden="true" />
<UIcon v-if="iconName" :name="iconName" :class="iconClass" aria-hidden="true" />
<ComboboxInput
ref="comboboxInput"
:value="query"
@@ -74,7 +73,8 @@ import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import type { Group, Command } from '../../types/command-palette'
import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import type { Button as ButtonType } from '../../types/button'
import type { Button } from '../../types/button'
import { classNames } from '../../utils'
import CommandPaletteGroup from './CommandPaletteGroup.vue'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -113,6 +113,10 @@ export default defineComponent({
type: Boolean,
default: true
},
loading: {
type: Boolean,
default: false
},
groups: {
type: Array as PropType<Group[]>,
default: () => []
@@ -121,12 +125,16 @@ export default defineComponent({
type: String,
default: () => appConfig.ui.commandPalette.default.icon
},
loadingIcon: {
type: String,
default: () => appConfig.ui.commandPalette.default.loadingIcon
},
selectedIcon: {
type: String,
default: () => appConfig.ui.commandPalette.default.selectedIcon
},
close: {
type: Object as PropType<Partial<ButtonType>>,
type: Object as PropType<Partial<Button>>,
default: () => appConfig.ui.commandPalette.default.close
},
empty: {
@@ -176,6 +184,7 @@ export default defineComponent({
const query = ref('')
const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>()
const comboboxApi = ref(null)
const isLoading = ref(false)
onMounted(() => {
if (props.autoselect) {
@@ -232,10 +241,17 @@ export default defineComponent({
const debouncedSearch = useDebounceFn(async () => {
const searchableGroups = props.groups.filter(group => !!group.search)
if (!searchableGroups.length) {
return
}
isLoading.value = true
await Promise.all(searchableGroups.map(async (group) => {
searchResults.value[group.key] = await group.search(query.value)
}))
isLoading.value = false
}, props.debounce)
watch(query, () => {
@@ -248,6 +264,22 @@ export default defineComponent({
}, 0)
})
const iconName = computed(() => {
if ((props.loading || isLoading.value) && props.loadingIcon) {
return props.loadingIcon
}
return props.icon
})
const iconClass = computed(() => {
return classNames(
ui.value.input.icon.base,
ui.value.input.icon.size,
((props.loading || isLoading.value) && props.loadingIcon) && 'animate-spin'
)
})
// Methods
function activateFirstOption () {
@@ -293,6 +325,8 @@ export default defineComponent({
groups,
comboboxInput,
query,
iconName,
iconClass,
onSelect,
onClear
}

View File

@@ -4,11 +4,11 @@
v-for="(link, index) of links"
v-slot="{ isActive }"
:key="index"
v-bind="link"
:class="[ui.base, ui.padding]"
v-bind="omit(link, ['label', 'icon', 'iconClass', 'avatar', 'badge', 'click'])"
:class="[ui.base, ui.padding, ui.width, ui.ring, ui.rounded, ui.font, ui.size]"
:active-class="ui.active"
:inactive-class="ui.inactive"
@click="link.click && link.click()"
@click="link.click"
@keyup.enter="$event.target.blur()"
>
<slot name="avatar" :link="link">
@@ -29,7 +29,7 @@
<span v-if="link.label" :class="ui.label">{{ link.label }}</span>
</slot>
<slot name="badge" :link="link" :is-active="isActive">
<span v-if="link.badge" :class="[ui.badge.baseClass, isActive ? ui.badge.active : ui.badge.inactive]">
<span v-if="link.badge" :class="[ui.badge.base, isActive ? ui.badge.active : ui.badge.inactive]">
{{ link.badge }}
</span>
</slot>
@@ -40,12 +40,13 @@
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router'
import type { RouteLocationRaw } from 'vue-router'
import { defu } from 'defu'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import ULinkCustom from '../elements/LinkCustom.vue'
import type { Avatar as AvatarType } from '../../types/avatar'
import { omit } from '../../utils'
import type { Avatar } from '../../types/avatar'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -62,15 +63,15 @@ export default defineComponent({
props: {
links: {
type: Array as PropType<{
to?: RouteLocationNormalized | string
exact?: boolean
label: string
icon?: string
iconClass?: string
avatar?: Partial<AvatarType>
click?: Function
badge?: string
}[]>,
to?: string | RouteLocationRaw
exact?: boolean
label: string
icon?: string
iconClass?: string
avatar?: Partial<Avatar>
click?: Function
badge?: string | number
}[]>,
default: () => []
},
ui: {
@@ -86,7 +87,8 @@ export default defineComponent({
return {
// eslint-disable-next-line vue/no-dupe-keys
ui
ui,
omit
}
}
})

View File

@@ -1,15 +1,11 @@
<template>
<transition appear v-bind="ui.transition">
<div
:class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
>
<div :class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]" @mouseover="onMouseover" @mouseleave="onMouseleave">
<div :class="[ui.container, ui.rounded, ui.ring]">
<div class="p-4">
<div :class="ui.padding">
<div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }">
<UIcon v-if="icon" :name="icon" :class="ui.icon" />
<UAvatar v-if="avatar" v-bind="avatar" :class="ui.avatar" />
<UIcon v-if="icon" :name="icon" :class="iconClass" />
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
<div class="w-0 flex-1">
<p :class="ui.title">
@@ -32,7 +28,7 @@
</div>
</div>
</div>
<div v-if="timeout" :class="ui.progress" :style="progressBarStyle" />
<div v-if="timeout" :class="progressClass" :style="progressStyle" />
</div>
</div>
</transition>
@@ -46,9 +42,10 @@ import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue'
import { useTimer } from '../../composables/useTimer'
import type { ToastNotificationAction } from '../../types'
import type { Avatar as AvatarType } from '../../types/avatar'
import type { Button as ButtonType } from '../../types/button'
import type { NotificationAction } from '../../types'
import type { Avatar} from '../../types/avatar'
import type { Button } from '../../types/button'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -77,14 +74,14 @@ export default defineComponent({
},
icon: {
type: String,
default: null
default: () => appConfig.ui.notification.default.icon
},
avatar: {
type: Object as PropType<Partial<AvatarType>>,
type: Object as PropType<Partial<Avatar>>,
default: null
},
close: {
type: Object as PropType<Partial<ButtonType>>,
type: Object as PropType<Partial<Button>>,
default: () => appConfig.ui.notification.default.close
},
timeout: {
@@ -92,13 +89,20 @@ export default defineComponent({
default: 5000
},
actions: {
type: Array as PropType<ToastNotificationAction[]>,
type: Array as PropType<NotificationAction[]>,
default: () => []
},
callback: {
type: Function,
default: null
},
color: {
type: String,
default: () => appConfig.ui.notification.default.color,
validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value)
}
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.notification>>,
default: () => appConfig.ui.notification
@@ -114,12 +118,26 @@ export default defineComponent({
let timer: any = null
const remaining = ref(props.timeout)
const progressBarStyle = computed(() => {
const progressStyle = computed(() => {
const remainingPercent = remaining.value / props.timeout * 100
return { width: `${remainingPercent || 0}%` }
})
const progressClass = computed(() => {
return classNames(
ui.value.progress.base,
ui.value.progress.background?.replaceAll('{color}', props.color)
)
})
const iconClass = computed(() => {
return classNames(
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color?.replaceAll('{color}', props.color)
)
})
function onMouseover () {
if (timer) {
timer.pause()
@@ -144,7 +162,7 @@ export default defineComponent({
emit('close')
}
function onAction (action: ToastNotificationAction) {
function onAction (action: NotificationAction) {
if (timer) {
timer.stop()
}
@@ -179,7 +197,9 @@ export default defineComponent({
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
progressBarStyle,
progressStyle,
progressClass,
iconClass,
onMouseover,
onMouseleave,
onClose,

View File

@@ -1,5 +1,5 @@
<template>
<div :class="ui.wrapper">
<div :class="[ui.wrapper, ui.position, ui.width]">
<div v-if="notifications.length" :class="ui.container">
<div v-for="notification of notifications" :key="notification.id">
<UNotification
@@ -17,7 +17,7 @@
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import type { ToastNotification } from '../../types'
import type { Notification } from '../../types'
import { useToast } from '../../composables/useToast'
import UNotification from './Notification.vue'
import { useState, useAppConfig } from '#imports'
@@ -44,7 +44,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.notifications>>(() => defu({}, props.ui, appConfig.ui.notifications))
const toast = useToast()
const notifications = useState<ToastNotification[]>('notifications', () => [])
const notifications = useState<Notification[]>('notifications', () => [])
return {
// eslint-disable-next-line vue/no-dupe-keys

View File

@@ -1,8 +1,8 @@
import { useClipboard } from '@vueuse/core'
import type { ToastNotification } from '../types/toast'
import type { Notification } from '../types/notification'
import { useToast } from './useToast'
export function useCopyToClipboard (options: Partial<ToastNotification> = {}) {
export function useCopyToClipboard (options: Partial<Notification> = {}) {
const { copy: copyToClipboard, isSupported } = useClipboard()
const toast = useToast()

View File

@@ -1,5 +1,6 @@
import { createSharedComposable, useActiveElement } from '@vueuse/core'
import { ref, computed, onMounted } from 'vue'
import type {} from '@vueuse/shared'
export const _useShortcuts = () => {
const macOS = computed(() => process.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/))

View File

@@ -18,7 +18,6 @@ export function useTimer (cb: (...args: unknown[]) => any, interval: number, opt
timer = setTimeout(() => {
timer = null
startTime.value = null
// eslint-disable-next-line n/no-callback-literal
cb(...args)
}, remaining.value) as unknown as number
}

Some files were not shown because too many files have changed in this diff Show More