Compare commits

..

86 Commits

Author SHA1 Message Date
Benjamin Canac
64897a39bf chore(release): 2.9.0 2023-10-02 17:29:03 +02:00
Benjamin Canac
dfda33c1aa chore(deps): bump 2023-10-02 11:07:56 +02:00
Benjamin Canac
d46eafb248 chore(Badge): add type 2023-09-29 16:17:06 +02:00
Benjamin Canac
ee6f0d0c49 chore(deps): dedupe lock 2023-09-29 14:55:03 +02:00
Haytham A. Salama
b7b86bcc44 docs: add discord link to the section community (#759)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-29 11:34:00 +02:00
Haytham A. Salama
bbf3424933 docs: add contributing page (#729)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-29 11:10:03 +02:00
Levy
2fc938575d feat(FormGroup): add slots (#714)
Co-authored-by: Romain Hamel <rom.hml@gmail.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
Co-authored-by: saveliy <savelii.moshkota@ext.jumingo.com>
2023-09-28 18:30:41 +02:00
renovate[bot]
ff9d51863e chore(deps): update all non-major dependencies (#683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-28 18:01:28 +02:00
Benjamin Canac
adb0a0fbe4 docs: bump @nuxt/content & @nuxt/ui-pro
Resolves #754
2023-09-28 17:42:42 +02:00
Haytham A. Salama
a071e4b875 fix(Pagination): handle max > 5 and max equal total pages (#728) 2023-09-28 17:01:44 +02:00
Benjamin Canac
a74de152d7 chore(deps): bump @nuxt/ui-pro 2023-09-28 14:24:15 +02:00
Aditio Pangestu
109ec52d50 fix(module): retain props reactivity through useUI (#745)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-28 14:06:57 +02:00
Haytham A. Salama
874447cb41 feat(Table): add ability to custom style for td and tr (#741)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-28 12:11:26 +02:00
KeJun
8b7a013319 docs(ComponentCard): fix inline highlighter (#750)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-28 11:28:43 +02:00
Benjamin Canac
3e647e4af1 fix(module): move @headlessui/tailwindcss to plugins on module install 2023-09-27 15:13:57 +02:00
Benjamin Canac
0da85e1463 docs: bump @nuxt/content 2023-09-27 14:52:44 +02:00
Benjamin Canac
dcf6e63471 docs: bump @nuxt/content to 2.8.3 2023-09-27 13:48:06 +02:00
Benjamin Canac
cbb2f28c3f fix(Tabs): prevent focus of TabPanel with tabindex="-1" 2023-09-27 13:47:48 +02:00
Horu
be734fc026 fix(Tabs): add visible focus indicator on active tabs (#690) 2023-09-27 13:38:58 +02:00
Sébastien Chopin
b306138574 docs: add figma kit 2023-09-26 16:41:09 +02:00
Benjamin Canac
1ebf456ffc docs: add figma kit community link 2023-09-26 15:10:53 +02:00
Benjamin Canac
8257a11dcb feat(Link): add active prop to override default behaviour (#732)
Co-authored-by: Sébastien Chopin <seb@nuxt.com>
2023-09-25 20:57:41 +02:00
Haytham A. Salama
6887f732ee fix(Accordion): close other items in circular order (#735) 2023-09-24 11:36:35 +02:00
Benjamin Canac
d088d8a7b8 chore(github): missing question form 2023-09-23 14:34:55 +02:00
Benjamin Canac
f60543a234 chore(github): update issue forms 2023-09-23 14:33:44 +02:00
Benjamin Canac
2531c8e66d chore(github): use issue forms 2023-09-23 14:26:18 +02:00
Benjamin Canac
4b68760f6a chore(github): improve issue templates 2023-09-23 12:04:43 +02:00
Benjamin Canac
568772382f playground: add missing .nuxtrc 2023-09-22 10:17:45 +02:00
Romain Hamel
46879dc1b7 chore(FormGroup): simplify bindings between input and form group p… (#704)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-21 23:22:55 +02:00
Benjamin Canac
a94782d94b docs: fetch select values from config 2023-09-21 22:16:34 +02:00
Benjamin Canac
853d58ad5f chore(module): add @ts-ignore on appConfig assign 2023-09-21 16:01:24 +02:00
Benjamin Canac
38b1eb6c5f docs: migrate to @nuxt/ui-pro 2023-09-21 15:00:08 +02:00
Benjamin Canac
f24ff9c47f chore: revert ui. prefix when using useUI composable 2023-09-21 14:30:34 +02:00
Benjamin Canac
60210aad75 chore(module): allow key extend in app.config 2023-09-21 14:26:21 +02:00
Benjamin Canac
67e85f98e2 docs: bump @nuxthq/elements 2023-09-21 13:14:20 +02:00
Benjamin Canac
b3a52482f2 docs: invalid Edit this page link on main branch 2023-09-21 12:53:42 +02:00
Benjamin Canac
86dc49ecc9 chore: use get in useUI 2023-09-21 12:50:18 +02:00
Benjamin Canac
c937736734 chore: rename prepare to dev:prepare 2023-09-21 11:29:14 +02:00
Benjamin Canac
d379c579c0 docs: fix preset display 2023-09-21 11:12:03 +02:00
Benjamin Canac
f983c974c4 chore(scripts): remove pnpm install 2023-09-20 18:51:29 +02:00
Benjamin Canac
b90b151588 chore(github): add pull request template 2023-09-20 18:11:08 +02:00
Benjamin Canac
34d2f57801 feat(module)!: use tailwind-merge for app.config & move config to components & type props (#692)
Co-authored-by: Pooya Parsa <pooya@pi0.io>
2023-09-20 18:07:51 +02:00
Benjamin Canac
2c98628f98 docs: add discord link in footer 2023-09-20 12:28:49 +02:00
Aditio Pangestu
681f0e5684 fix(FormGroup): use explicit label instead of implicit label (#638) 2023-09-20 11:06:23 +02:00
Haytham A. Salama
e40491208a feat(Link): add as prop (#535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-16 21:50:55 +02:00
Benjamin Canac
00594ea59b docs: improve plausible track event 2023-09-15 17:50:54 +02:00
Benjamin Canac
3ba95d3c4d docs: fix validation warns on color picker 2023-09-15 17:50:40 +02:00
Benjamin Canac
3424ce118d docs: fetch index page from dev source 2023-09-15 17:50:22 +02:00
Benjamin Canac
40b1d30f5c docs: bump @nuxthq/elements & nuxt-component-meta 2023-09-15 17:50:12 +02:00
Benjamin Canac
8ec23c042d docs: improve multi-source handling (#682) 2023-09-15 14:37:53 +02:00
Benjamin Canac
81463cd21d docs: lazy load images for performances 2023-09-14 22:55:55 +02:00
renovate[bot]
c44d363f62 chore(deps): update all non-major dependencies (#649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-14 19:40:25 +02:00
Benjamin Canac
fbfa14a6a3 docs: track search 2023-09-14 19:30:26 +02:00
Benjamin Canac
4127caac76 docs: remove lodash (#678) 2023-09-14 19:19:20 +02:00
Younes Barrad
d6476d17f9 feat: remove lodash-es (#648)
Co-authored-by: Daniel Roe <daniel@roe.dev>
2023-09-14 18:47:09 +02:00
Benjamin Canac
5fc44b97c6 chore(CommandPalette): add search? function to types 2023-09-14 18:43:14 +02:00
Honza Pobořil
15e418e6c6 fix(Tabs): allow custom keys in TabItem (#671) 2023-09-13 17:39:29 +02:00
Benjamin Canac
3b8ca9886d docs: fix demo components z-index
Fixes #670
2023-09-13 15:28:58 +02:00
Romain Hamel
4c5833083f fix(FormGroup): prevent input click from propagating to label (#651) 2023-09-12 16:01:01 +02:00
Sma11X
83d609d530 fix(Table): select all rows without select listener (#652) 2023-09-12 15:55:50 +02:00
Farnabaz
1b34df15ac docs(ComponentCard): use inline highlighter (#664) 2023-09-12 15:49:44 +02:00
Benjamin Canac
0178ca9586 docs: hmr for tailwindcss classes in yml 2023-09-12 15:15:07 +02:00
Benjamin Canac
40ecb23d9a docs: add more padding on demo 2023-09-12 15:14:51 +02:00
Benjamin Canac
85734b8615 docs: accessibilty issue on range example 2023-09-12 15:14:43 +02:00
Benjamin Canac
ab26e4ba7d docs: embed playground 2023-09-12 14:50:35 +02:00
Benjamin Canac
edbbb33f69 docs: improve demo animation performances 2023-09-12 14:49:30 +02:00
Benjamin Canac
3de3aa006c chore(CommandPalette): add aria-label on input 2023-09-12 11:35:50 +02:00
Benjamin Canac
784f1f51dd docs: improve demo accessibility 2023-09-12 11:31:02 +02:00
Benjamin Canac
0787ec2d12 docs: bump @nuxthq/elements 2023-09-12 11:30:53 +02:00
Benjamin Canac
a8f643939e docs: improve notification in demo 2023-09-12 11:05:16 +02:00
Benjamin Canac
6f77ee80ce chore: add aria-label on close buttons 2023-09-12 10:59:26 +02:00
Florent Delerue
e2d4ba529d docs: improve landing demo animation (#661)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-09-12 10:50:05 +02:00
Benjamin Canac
1c707ca00d docs: remove pick usage 2023-09-11 19:08:40 +02:00
Benjamin Canac
00e951f708 docs(SelectMenu): improve default slot example 2023-09-11 14:48:56 +02:00
Benjamin Canac
0544a01c5b fix(SelectMenu): handle numbers
Resolves #574
2023-09-11 14:48:44 +02:00
Benjamin Canac
290ab1d9c5 chore: reactive attrs without class
Fixes #650
2023-09-11 12:55:24 +02:00
Benjamin Canac
254c4ed7d3 docs: lazy load DatePicker component 2023-09-11 11:51:01 +02:00
Benjamin Canac
a603ea56c1 fix(Table): add missing classes in app.config.ts
Fixes #655
2023-09-11 11:31:04 +02:00
Benjamin Canac
a90e95f7d1 docs: bump @nuxthq/elements 2023-09-11 11:30:32 +02:00
Benjamin Canac
bc2315b7d9 docs: improve accessibility 2023-09-11 11:25:23 +02:00
Benjamin Canac
87fd85ec3f chore(Table): improve accessibility 2023-09-11 11:25:06 +02:00
Benjamin Canac
3fef86834f chore(Pagination): improve accessibility 2023-09-11 11:24:57 +02:00
Benjamin Canac
b5e8685a2c docs: prevent code ast duplicate with slots
Fixes #654
2023-09-10 21:38:25 +02:00
renovate[bot]
15ee768729 chore(deps): update all non-major dependencies (#612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-09 18:25:04 +02:00
jduartea
8955595dc6 fix(Range): fix track pseudo-elements for mozilla (#636) 2023-09-09 18:24:06 +02:00
Haytham A. Salama
fd6bcd3f84 docs: add examples link in header (#618) 2023-09-09 18:21:23 +02:00
117 changed files with 3644 additions and 3405 deletions

View File

@@ -1,31 +0,0 @@
---
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
@nuxt/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/nuxt-ui
-->
### Steps to reproduce
### What is Expected?
### What is actually happening?

60
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: "🐛 Bug report"
description: Report a bug to help us improve the module.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Before reporting a bug, please make sure that you have read through our [documentation](https://ui.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
- type: textarea
id: env
attributes:
label: Environment
description: You can use `npx nuxi info` to fill this section
placeholder: |
- Operating System: `Darwin`
- Node Version: `v18.16.0`
- Nuxt Version: `3.7.3`
- CLI Version: `3.8.4`
- Nitro Version: `2.6.3`
- Package Manager: `pnpm@8.7.4`
- Builder: `-`
- User Config: `-`
- Runtime Modules: `-`
- Build Modules: `-`
validations:
required: true
- type: input
id: version
attributes:
label: Version
placeholder: v2.8.0
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Please provide a reproduction link using this template https://stackblitz.com/edit/nuxt-ui. A minimal [reproduction is required](https://antfu.me/posts/why-reproductions-are-required) unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "needs reproduction" label. If no reproduction is provided we might close it.
placeholder: https://stackblitz.com/edit/nuxt-ui
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description.
validations:
required: true
- type: textarea
id: additonal
attributes:
label: Additional context
description: If applicable, add any other context or screenshots here.
- type: textarea
id: logs
attributes:
label: Logs
description: |
Optional if provided reproduction. Please try not to insert an image but copy paste the log text.
render: shell-script

View File

@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Nuxt Community Discord
url: https://discord.nuxtjs.org/
about: Consider asking questions about the module here.
- name: 📖 Documentation
url: https://ui.nuxt.com
about: Check the documentation for guides and examples.
- name: 📚 Discord
url: https://discord.com/channels/473401852243869706/1153996761426300948
about: Consider asking questions in the `#ui` channel.

View File

@@ -1,20 +0,0 @@
---
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. -->

View File

@@ -0,0 +1,20 @@
name: "🚀 Feature request"
description: Suggest an idea or enhancement for the module.
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Before requesting a feature, please make sure that you have read through our [documentation](https://ui.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
- type: textarea
id: description
attributes:
label: Description
description: A clear and concise description of what you think would be an helpful addition to the module, including the possible use cases and alternatives you have considered. If you have a working prototype or module that implements it, please include a link.
validations:
required: true
- type: textarea
id: additonal
attributes:
label: Additional context
description: If applicable, add any other context or screenshots here.

View File

@@ -1,16 +0,0 @@
---
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/
-->

14
.github/ISSUE_TEMPLATE/question.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: "💬 Question"
description: Ask a question about the module.
labels: ["question"]
body:
- type: markdown
attributes:
value: |
Before asking a question, please make sure that you have read through our [documentation](https://ui.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
- type: textarea
id: description
attributes:
label: Description
validations:
required: true

33
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,33 @@
<!---
☝️ PR title should follow conventional commits (https://conventionalcommits.org)
-->
### 🔗 Linked issue
<!-- Please ensure there is an open issue and mention its number as #123 -->
### ❓ Type of change
<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->
- [ ] 📖 Documentation (updates to the documentation or readme)
- [ ] 🐞 Bug fix (a non-breaking change that fixes an issue)
- [ ] 👌 Enhancement (improving an existing functionality)
- [ ] ✨ New feature (a non-breaking change that adds functionality)
- [ ] 🧹 Chore (updates to the build process or auxiliary tools and libraries)
- [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change)
### 📚 Description
<!-- Describe your changes in detail -->
<!-- Why is this change required? What problem does it solve? -->
<!-- If it resolves an open issue, please link to the issue here. For example "Resolves #1337" -->
### 📝 Checklist
<!-- Put an `x` in all the boxes that apply. -->
<!-- If your change requires a documentation PR, please link it appropriately -->
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] I have linked an issue or discussion.
- [ ] I have updated the documentation accordingly.

View File

@@ -49,15 +49,18 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Prepare
run: pnpm run dev:prepare
- name: Lint
run: pnpm run lint
- name: Build
run: pnpm run build
- name: Typecheck
run: pnpm run typecheck
- name: Build
run: pnpm run build
- name: Release Edge
if: github.event_name == 'push'
run: ./scripts/release-edge.sh

View File

@@ -49,15 +49,18 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Prepare
run: pnpm run dev:prepare
- name: Lint
run: pnpm run lint
- name: Build
run: pnpm run build
- name: Typecheck
run: pnpm run typecheck
- name: Build
run: pnpm run build
- name: Version Check
id: check
uses: EndBug/version-check@v2

3
.nuxtrc Normal file
View File

@@ -0,0 +1,3 @@
imports.autoImport=false
typescript.includeWorkspace=true
typescript.strict=false

View File

@@ -1,5 +1,38 @@
# Changelog
## [2.9.0](https://github.com/nuxt/ui/compare/v2.8.1...v2.9.0) (2023-10-02)
### ⚠ BREAKING CHANGES
* **module:** use `tailwind-merge` for `app.config` & move config to components & type props (#692)
### Features
* **FormGroup:** add slots ([#714](https://github.com/nuxt/ui/issues/714)) ([2fc9385](https://github.com/nuxt/ui/commit/2fc938575d2e409ba9df9fb2ddb8d51d021a1756))
* **Link:** add `active` prop to override default behaviour ([#732](https://github.com/nuxt/ui/issues/732)) ([8257a11](https://github.com/nuxt/ui/commit/8257a11dcba9c34053f8061ed1383894d06b2a6c))
* **Link:** add `as` prop ([#535](https://github.com/nuxt/ui/issues/535)) ([e404912](https://github.com/nuxt/ui/commit/e40491208ac1096e505803072df0d9e2e771008e))
* **module:** use `tailwind-merge` for `app.config` & move config to components & type props ([#692](https://github.com/nuxt/ui/issues/692)) ([34d2f57](https://github.com/nuxt/ui/commit/34d2f57801d08d26262fdff4398ec3d3329b4bb0))
* remove `lodash-es` ([#648](https://github.com/nuxt/ui/issues/648)) ([d6476d1](https://github.com/nuxt/ui/commit/d6476d17f9b17317a7160271dacdb854f30237ae))
* **Table:** add ability to custom style for `td` and `tr` ([#741](https://github.com/nuxt/ui/issues/741)) ([874447c](https://github.com/nuxt/ui/commit/874447cb41a77868513459eee5d3301fe8b8e9a1))
### Bug Fixes
* **Accordion:** close other items in circular order ([#735](https://github.com/nuxt/ui/issues/735)) ([6887f73](https://github.com/nuxt/ui/commit/6887f732ee8e14625459a0576460523845cb0a6d))
* **FormGroup:** prevent input click from propagating to label ([#651](https://github.com/nuxt/ui/issues/651)) ([4c58330](https://github.com/nuxt/ui/commit/4c5833083f0840add52f3c67efc42b8db5687d37))
* **FormGroup:** use explicit label instead of implicit label ([#638](https://github.com/nuxt/ui/issues/638)) ([681f0e5](https://github.com/nuxt/ui/commit/681f0e5684feaad0c711130404751f2fd65ddbe4))
* **module:** move `@headlessui/tailwindcss` to plugins on module install ([3e647e4](https://github.com/nuxt/ui/commit/3e647e4af154dad7fa186f062ce984e4d8d0e202))
* **module:** retain props reactivity through `useUI` ([#745](https://github.com/nuxt/ui/issues/745)) ([109ec52](https://github.com/nuxt/ui/commit/109ec52d50b0b32b0f0b24ece5b92cd7bbce29da))
* **Pagination:** handle `max > 5` and `max` equal total pages ([#728](https://github.com/nuxt/ui/issues/728)) ([a071e4b](https://github.com/nuxt/ui/commit/a071e4b8755f5dbbdfd05985c8fcb65c3cdab3ec))
* **Range:** fix track pseudo-elements for mozilla ([#636](https://github.com/nuxt/ui/issues/636)) ([8955595](https://github.com/nuxt/ui/commit/8955595dc6904d0090ad7f82ed8b376a15e51f94))
* **SelectMenu:** handle numbers ([0544a01](https://github.com/nuxt/ui/commit/0544a01c5b7ae534a595e6c91d2884a601ae3185)), closes [#574](https://github.com/nuxt/ui/issues/574)
* **Table:** add missing classes in `app.config.ts` ([a603ea5](https://github.com/nuxt/ui/commit/a603ea56c165e9ad01482d092460da3991f3e41d)), closes [#655](https://github.com/nuxt/ui/issues/655)
* **Table:** select all rows without select listener ([#652](https://github.com/nuxt/ui/issues/652)) ([83d609d](https://github.com/nuxt/ui/commit/83d609d53067b2639a55a0e367a5e7adbd8a22fc))
* **Tabs:** add visible focus indicator on active tabs ([#690](https://github.com/nuxt/ui/issues/690)) ([be734fc](https://github.com/nuxt/ui/commit/be734fc026b75bc8c921e9401ba6e97f65356cec))
* **Tabs:** allow custom keys in `TabItem` ([#671](https://github.com/nuxt/ui/issues/671)) ([15e418e](https://github.com/nuxt/ui/commit/15e418e6c6f981afd2c0e8f27dedb303b8cbad70))
* **Tabs:** prevent focus of `TabPanel` with `tabindex="-1"` ([cbb2f28](https://github.com/nuxt/ui/commit/cbb2f28c3fd96e45c7af20675b5b67576ddc0d63))
## [2.8.1](https://github.com/nuxt/ui/compare/v2.8.0...v2.8.1) (2023-09-09)

View File

@@ -20,6 +20,7 @@ Is has been developed by [NuxtLabs](https://nuxtlabs.com/) for [Volta](https://v
- Keyboard shortcuts
- Bundled icons
- Fully typed
- [Figma Kit](https://www.figma.com/community/file/1288455405058138934)
Read more on [ui.nuxt.com](https://ui.nuxt.com)

View File

@@ -1,4 +1,6 @@
# To use Nuxt Elements in production
NUXT_ELEMENTS_TOKEN=
# To link Nuxt UI Pro in development
NUXT_UI_PRO_PATH=
# To use Nuxt UI Pro in production
NUXT_UI_PRO_TOKEN=
# Used when pre-rendering the docs for dynamic OG images
NUXT_PUBLIC_SITE_URL=

1
docs/.nuxtrc Normal file
View File

@@ -0,0 +1 @@
imports.autoImport=true

View File

@@ -10,7 +10,7 @@
<Footer />
<ClientOnly>
<LazyUDocsSearch :files="files" :navigation="navigation" />
<LazyUDocsSearch ref="searchRef" :files="files" :navigation="navigation" :groups="groups" />
</ClientOnly>
<UNotifications>
@@ -27,36 +27,52 @@
<script setup lang="ts">
import { withoutTrailingSlash } from 'ufo'
import { debounce } from 'perfect-debounce'
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
const searchRef = ref()
const route = useRoute()
const colorMode = useColorMode()
const { prefix, removePrefixFromNavigation, removePrefixFromFiles } = useContentSource()
const { branch, branches } = useContentSource()
const { data: nav } = await useAsyncData('navigation', () => fetchContentNavigation())
const { data: search } = useLazyFetch('/api/search.json', {
default: () => [],
server: false
})
const { data: files } = useLazyFetch<ParsedContent[]>('/api/search.json', { default: () => [], server: false })
// Computed
const navigation = computed(() => {
const navigation = nav.value.find(link => link._path === prefix.value)?.children || []
return prefix.value === '/main' ? removePrefixFromNavigation(navigation) : navigation
const navigation = computed(() => {
const main = nav.value.filter(item => item._path !== '/dev')
const dev = nav.value.find(item => item._path === '/dev')?.children
return branch.value?.name === 'dev' ? dev : main
})
const files = computed(() => {
const files = search.value.filter(file => file._path.startsWith(prefix.value))
const groups = computed(() => {
if (route.path === '/') {
return []
}
return prefix.value === '/main' ? removePrefixFromFiles(files) : files
return [{ key: 'branch', label: 'Branch', commands: branches.value }]
})
const color = computed(() => colorMode.value === 'dark' ? '#18181b' : 'white')
// Watch
watch(() => searchRef.value?.commandPaletteRef?.query, debounce((query: string) => {
if (!query) {
return
}
useTrackEvent('Search', { props: { query: `${query} - ${searchRef.value?.commandPaletteRef.results.length} results` } })
}, 500))
// Head
useHead({
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ key: 'theme-color', name: 'theme-color', content: color }
],
link: [
@@ -74,6 +90,7 @@ useServerSeoMeta({
})
// Provide
provide('navigation', navigation)
provide('files', files)
</script>

View File

@@ -10,7 +10,7 @@
color="gray"
:ui="{ icon: { trailing: { padding: { sm: 'pe-1.5' } } } }"
:ui-menu="{ option: { container: 'gap-1.5' } }"
@update:model-value="selectBranch"
@update:model-value="select"
>
<template #label>
<UIcon v-if="branch.icon" :name="branch.icon" class="w-4 h-4 flex-shrink-0 text-gray-600 dark:text-gray-300" />
@@ -32,19 +32,5 @@
</template>
<script setup lang="ts">
const route = useRoute()
const router = useRouter()
const { branches, branch } = useContentSource()
function selectBranch (branch) {
if (branch.name === 'dev') {
if (route.path.startsWith('/dev')) {
return
}
router.push(`/dev${route.path}`)
} else {
router.push(route.path.replace('/dev', ''))
}
}
const { branches, branch, select } = useContentSource()
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="w-full h-px bg-gray-200 dark:bg-gray-800 flex items-center justify-center">
<div v-if="$route.path !== '/playground'" class="w-full h-px bg-gray-200 dark:bg-gray-800 flex items-center justify-center">
<div class="bg-white dark:bg-gray-900 px-4">
<LogoOnly class="w-5 h-5" />
</div>
@@ -24,9 +24,14 @@
</template>
<template #right>
<USocialButton aria-label="Nuxt Website" icon="i-simple-icons-nuxtdotjs" to="https://nuxt.com" />
<USocialButton aria-label="Nuxt on X" icon="i-simple-icons-x" to="https://x.com/nuxt_js" />
<USocialButton aria-label="Nuxt UI on GitHub" icon="i-simple-icons-github" to="https://github.com/nuxt/ui" />
<UButton aria-label="Nuxt Website" icon="i-simple-icons-nuxtdotjs" to="https://nuxt.com" target="_blank" v-bind="($ui.button.secondary as any)" />
<UButton aria-label="Nuxt UI on Discord" icon="i-simple-icons-discord" to="https://discord.com/invite/ps2h6QT" target="_blank" v-bind="($ui.button.secondary as any)" />
<UButton aria-label="Nuxt on X" icon="i-simple-icons-x" to="https://x.com/nuxt_js" target="_blank" v-bind="($ui.button.secondary as any)" />
<UButton aria-label="Nuxt UI on GitHub" icon="i-simple-icons-github" to="https://github.com/nuxt/ui" target="_blank" v-bind="($ui.button.secondary as any)" />
</template>
</UFooter>
</template>
<script setup lang="ts">
// force typescript
</script>

View File

@@ -12,18 +12,23 @@
</NuxtLink>
</template>
<template v-if="$route.path !== '/'" #center>
<UDocsSearchButton class="ml-1.5 hidden lg:flex lg:w-64 xl:w-96" />
</template>
<template #right>
<ColorPicker />
<UDocsSearchButton :class="[$route.path !== '/' && 'lg:hidden']" icon-only />
<UTooltip text="Search" :shortcuts="[metaSymbol, 'K']">
<UDocsSearchButton :label="null" />
</UTooltip>
<UColorModeButton v-if="!$colorMode.forced" />
<UColorModeButton />
<USocialButton to="https://github.com/nuxt/ui" target="_blank" icon="i-simple-icons-github" aria-label="GitHub" class="hidden lg:inline-flex" />
<UButton
to="https://github.com/nuxt/ui"
target="_blank"
icon="i-simple-icons-github"
aria-label="GitHub"
class="hidden lg:inline-flex"
v-bind="($ui.button.secondary as any)"
/>
</template>
<template #panel>
@@ -37,25 +42,23 @@
<script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
const route = useRoute()
const { mapContentNavigation } = useElementsHelpers()
const { metaSymbol } = useShortcuts()
const navigation = inject<Ref<NavItem[]>>('navigation')
const links = computed(() => {
if (route.path !== '/') {
return []
}
return [{
label: 'Documentation',
icon: 'i-heroicons-book-open-solid',
to: '/getting-started'
}, {
label: 'Examples',
icon: 'i-heroicons-square-3-stack-3d',
to: '/getting-started/examples'
}, {
label: 'Playground',
icon: 'i-simple-icons-stackblitz',
to: 'https://stackblitz.com/edit/nuxt-ui?file=app.config.ts,app.vue',
target: '_blank'
to: '/playground'
}, {
label: 'Releases',
icon: 'i-heroicons-rocket-launch-solid',

View File

@@ -1,7 +1,7 @@
<template>
<UPopover mode="hover">
<template #default="{ open }">
<UButton color="gray" variant="ghost" square :class="[open && 'bg-gray-50 dark:bg-gray-800']">
<UButton color="gray" variant="ghost" square :class="[open && 'bg-gray-50 dark:bg-gray-800']" aria-label="Color picker">
<UIcon name="i-heroicons-swatch-20-solid" class="w-5 h-5 text-primary-500 dark:text-primary-400" />
</UButton>
</template>
@@ -30,7 +30,7 @@ const colorMode = useColorMode()
// Computed
const primaryColors = computed(() => useWithout(appConfig.ui.colors, 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const primaryColors = computed(() => appConfig.ui.colors.filter(color => color !== 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const primary = computed({
get () {
return primaryColors.value.find(option => option.value === appConfig.ui.primary)

View File

@@ -1,12 +1,12 @@
<template>
<UTooltip :text="color.value" class="capitalize" :open-delay="500">
<UButton
color="transparent"
color="white"
square
:ui="{
color: {
transparent: {
solid: 'bg-gray-100 dark:bg-gray-800',
white: {
solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800',
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
}
}

View File

@@ -46,13 +46,17 @@
</component>
</div>
<ContentRenderer v-if="!previewOnly" :value="ast" class="[&>div>pre]:!rounded-t-none" />
<ContentRenderer v-if="!previewOnly" :value="ast" class="[&>div>pre]:!rounded-t-none [&>div>pre]:!mt-0" />
</div>
</template>
<script setup lang="ts">
// @ts-expect-error
import { transformContent } from '@nuxt/content/transformers'
// @ts-ignore
import { useShikiHighlighter } from '@nuxtjs/mdc/runtime'
import { upperFirst, camelCase, kebabCase } from 'scule'
import * as config from '#ui/ui.config'
// eslint-disable-next-line vue/no-dupe-keys
const props = defineProps({
@@ -114,15 +118,14 @@ const appConfig = useAppConfig()
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`
const camelName = camelCase(slug)
const name = `U${upperFirst(camelName)}`
const meta = await fetchComponentMeta(name)
// Computed
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => ({ ...appConfig.ui[camelName], ...props.ui }))
const ui = computed(() => ({ ...config[camelName], ...props.ui }))
const fullProps = computed(() => ({ ...baseProps, ...componentProps }))
const vModel = computed({
@@ -138,8 +141,8 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
}
const prop = meta?.meta?.props?.find((prop: any) => prop.name === key)
const dottedKey = useKebabCase(key).replaceAll('-', '.')
const keys = useGet(ui.value, dottedKey, {})
const dottedKey = kebabCase(key).replaceAll('-', '.')
const keys = ui.value[dottedKey] ?? {}
let options = typeof keys === 'object' && Object.keys(keys)
if (key.toLowerCase().endsWith('color')) {
// @ts-ignore
@@ -149,7 +152,7 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
return {
type: prop?.type || 'string',
name: key,
label: key === 'modelValue' ? 'value' : useCamelCase(key),
label: key === 'modelValue' ? 'value' : camelCase(key),
options
}
}).filter(Boolean))
@@ -163,7 +166,7 @@ const code = computed(() => {
continue
}
code += ` ${(typeof value === 'boolean' && value !== true) || typeof value === 'object' || typeof value === 'number' ? ':' : ''}${key === 'modelValue' ? 'value' : useKebabCase(key)}${typeof value === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}`
code += ` ${(typeof value === 'boolean' && value !== true) || typeof value === 'object' || typeof value === 'number' ? ':' : ''}${key === 'modelValue' ? 'value' : kebabCase(key)}${typeof value === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}`
}
if (props.slots) {
@@ -205,12 +208,17 @@ function renderObject (obj: any) {
return obj
}
const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify(componentProps)}`, () => transformContent('content:_markdown.md', code.value, {
highlight: {
theme: {
light: 'material-theme-lighter',
default: 'material-theme',
dark: 'material-theme-palenight'
const shikiHighlighter = useShikiHighlighter({})
const codeHighlighter = async (code: string, lang: string, theme: any, highlights: number[]) => shikiHighlighter.getHighlightedAST(code, lang, theme, { highlights })
const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify({ props: componentProps, slots: props.slots })}`, () => transformContent('content:_markdown.md', code.value, {
markdown: {
highlight: {
highlighter: codeHighlighter,
theme: {
light: 'material-theme-lighter',
default: 'material-theme',
dark: 'material-theme-palenight'
}
}
}
}), { watch: [code] })

View File

@@ -1,5 +1,5 @@
<template>
<div class="[&>div>pre]:!rounded-t-none">
<div class="[&>div>pre]:!rounded-t-none [&>div>pre]:!mt-0">
<div class="flex border border-gray-200 dark:border-gray-700 relative not-prose rounded-t-md" :class="[{ 'p-4': padding, 'rounded-b-md': !$slots.code, 'border-b-0': !!$slots.code }, backgroundClass, overflowClass]">
<ContentSlot v-if="$slots.default" :use="$slots.default" />
</div>

View File

@@ -5,6 +5,8 @@
<script setup lang="ts">
// @ts-expect-error
import { transformContent } from '@nuxt/content/transformers'
import { upperFirst, camelCase } from 'scule'
import * as config from '#ui/ui.config'
const props = defineProps({
slug: {
@@ -13,17 +15,16 @@ const props = defineProps({
}
})
const appConfig = useAppConfig()
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`
const camelName = camelCase(slug)
const name = `U${upperFirst(camelName)}`
const preset = appConfig.ui[camelName]
const preset = config[camelName]
const { data: ast } = await useAsyncData(`${name}-preset`, () => transformContent('content:_markdown.md', `
\`\`\`json [appConfig.ui.${camelName}]
\`\`\`json
${JSON.stringify(preset, null, 2)}
\`\`\`\
`, {

View File

@@ -7,6 +7,8 @@
</template>
<script setup lang="ts">
import { upperFirst, camelCase } from 'scule'
const props = defineProps({
slug: {
type: String,
@@ -17,8 +19,8 @@ const props = defineProps({
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`
const camelName = camelCase(slug)
const name = `U${upperFirst(camelName)}`
const meta = await fetchComponentMeta(name)
</script>

View File

@@ -18,6 +18,8 @@
</template>
<script setup lang="ts">
import { upperFirst, camelCase } from 'scule'
const props = defineProps({
slug: {
type: String,
@@ -28,8 +30,8 @@ const props = defineProps({
const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`
const camelName = camelCase(slug)
const name = `U${upperFirst(camelName)}`
const meta = await fetchComponentMeta(name)
</script>

View File

@@ -5,9 +5,9 @@ const toast = useToast()
const commandPaletteRef = ref()
const users = [
{ id: 'benjamincanac', label: 'benjamincanac', href: 'https://github.com/benjamincanac', target: '_blank', avatar: { src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/benjamincanac', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac 2x' } },
{ id: 'Atinux', label: 'Atinux', href: 'https://github.com/Atinux', target: '_blank', avatar: { src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/Atinux', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux 2x' } },
{ id: 'smarroufin', label: 'smarroufin', href: 'https://github.com/smarroufin', target: '_blank', avatar: { src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/smarroufin', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin 2x' } }
{ id: 'benjamincanac', label: 'benjamincanac', href: 'https://github.com/benjamincanac', target: '_blank', avatar: { src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/benjamincanac', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac 2x', loading: 'lazy' } },
{ id: 'Atinux', label: 'Atinux', href: 'https://github.com/Atinux', target: '_blank', avatar: { src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/Atinux', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux 2x', loading: 'lazy' } },
{ id: 'smarroufin', label: 'smarroufin', href: 'https://github.com/smarroufin', target: '_blank', avatar: { src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/smarroufin', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin 2x', loading: 'lazy' } }
]
const actions = [

View File

@@ -10,7 +10,7 @@ const label = computed(() => date.value.toLocaleDateString('en-us', { weekday: '
<UButton icon="i-heroicons-calendar-days-20-solid" :label="label" />
<template #panel="{ close }">
<DatePicker v-model="date" @close="close" />
<LazyDatePicker v-model="date" @close="close" />
</template>
</UPopover>
</template>

View File

@@ -80,7 +80,7 @@ async function submit (event: FormSubmitEvent<Schema>) {
</UFormGroup>
<UFormGroup name="checkbox" label="Checkbox">
<UCheckbox v-model="state.checkbox" />
<UCheckbox v-model="state.checkbox" label="Check me" />
</UFormGroup>
<UFormGroup name="radio" label="Radio">

View File

@@ -0,0 +1,16 @@
<template>
<UFormGroup label="Email" :error="!email && 'You must enter an email'" help="This is a nice email!">
<template #default="{ error }">
<UInput v-model="email" type="email" placeholder="Enter email" :trailing-icon="error ? 'i-heroicons-exclamation-triangle-20-solid' : undefined" />
</template>
<template #error="{ error }">
<UAlert v-if="error" icon="i-heroicons-exclamation-triangle-20-solid" :title="error" color="red" />
<UAlert v-else icon="i-heroicons-check-circle-20-solid" title="Your email is valid" color="green" />
</template>
</UFormGroup>
</template>
<script setup lang="ts">
const email = ref('')
</script>

View File

@@ -3,5 +3,6 @@ const value = ref(50)
</script>
<template>
<URange v-model="value" />
<label for="range" class="sr-only" />
<URange id="range" v-model="value" name="range" />
</template>

View File

@@ -6,7 +6,7 @@ const selected = ref(people[3])
<template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton color="gray">
<UButton color="gray" class="flex-1 justify-between">
{{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="[open && 'transform rotate-90']" />

View File

@@ -0,0 +1,40 @@
<script setup>
const columns = [{
key: 'id',
label: '#'
}, {
key: 'quantity',
label: 'Quantity',
class: 'italic'
}, {
key: 'name',
label: 'Name'
}]
const items = [{
id: 1,
name: 'Apple',
quantity: { value: 100, class: 'bg-green-500/50 dark:bg-green-400/50' }
}, {
id: 2,
name: 'Orange',
quantity: { value: 0 },
class: 'bg-red-500/50 dark:bg-red-400/50 animate-pulse'
}, {
id: 3,
name: 'Banana',
quantity: { value: 30, class: 'bg-green-500/50 dark:bg-green-400/50' }
}, {
id: 4,
name: 'Mango',
quantity: { value: 5, class: 'bg-green-500/50 dark:bg-green-400/50' }
}]
</script>
<template>
<UTable :rows="items" :columns="columns">
<template #quantity-data="{ row }">
{{ row.quantity.value }}
</template>
</UTable>
</template>

View File

@@ -3,7 +3,7 @@ const links = [{
avatar: {
src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/benjamincanac',
srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac 2x',
alt: 'benjamincanac'
alt: ''
},
label: 'benjamincanac',
to: 'https://github.com/benjamincanac',
@@ -12,7 +12,7 @@ const links = [{
avatar: {
src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/Atinux',
srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux 2x',
alt: 'Atinux'
alt: ''
},
label: 'Atinux',
to: 'https://github.com/Atinux',
@@ -21,14 +21,12 @@ const links = [{
avatar: {
src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/smarroufin',
srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin 2x',
alt: 'smarroufin'
alt: ''
},
label: 'smarroufin',
to: 'https://github.com/smarroufin',
target: '_blank'
}]
const { ui } = useAppConfig()
</script>
<template>
@@ -36,8 +34,9 @@ const { ui } = useAppConfig()
<template #avatar="{ link }">
<UAvatar
v-if="link.avatar"
v-bind="{ size: ui.verticalNavigation.avatar.size, ...link.avatar }"
:class="[ui.verticalNavigation.avatar.base]"
v-bind="link.avatar"
size="3xs"
loading="lazy"
/>
<UIcon v-else name="i-heroicons-user-circle-20-solid" class="text-lg" />
</template>

View File

@@ -1,105 +1,206 @@
<script setup lang="ts">
const refs = ref([])
const section = ref()
const { stop } = useIntersectionObserver(
section,
([{ isIntersecting }]) => {
if (!isIntersecting) {
return
}
refs.value.forEach(element => element.style.animationPlayState = 'running')
stop()
},
{ threshold: 0.3 }
)
onMounted(() => {
refs.value.forEach((element) => {
if (!element) {
return
}
element.style.animationFillMode = 'forwards'
element.style.transformOrigin = 'center'
element.style.animationPlayState = 'paused'
element.style.animationDuration = '1s'
})
})
</script>
<template>
<Transition appear name="fade">
<ULandingGrid class="lg:grid-cols-10 lg:gap-8">
<div class="col-span-8 flex items-center">
<RangeExample />
</div>
<ULandingGrid ref="section" class="lg:grid-cols-10 lg:gap-8">
<div :ref="(el) => (refs[1] = el)" class="col-span-8 flex items-center animate-top">
<RangeExample />
</div>
<div class="col-span-2 row-span-2 flex items-center">
<RadioExample />
</div>
<div :ref="(el) => (refs[2] = el)" class="col-span-2 row-span-2 flex items-center animate-right">
<RadioExample />
</div>
<div class="col-span-2">
<DropdownExampleBasic :popper="{ placement: 'bottom-start', strategy: 'absolute' }" />
</div>
<div :ref="(el) => (refs[4] = el)" class="col-span-2 animate-left z-10">
<DropdownExampleBasic :popper="{ placement: 'bottom-start', strategy: 'absolute' }" />
</div>
<div class="col-span-6 flex flex-wrap items-center justify-between gap-1">
<UAvatarGroup :max="2">
<UAvatar
src="https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac"
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/benjamincanac 2x"
alt="benjamincanac"
width="40"
height="40"
/>
<UAvatar
src="https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux"
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/Atinux 2x"
alt="Atinux"
width="40"
height="40"
/>
<UAvatar
src="https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin"
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/smarroufin 2x"
alt="smarroufin"
width="40"
height="40"
/>
</UAvatarGroup>
<div
:ref="(el) => (refs[3] = el)"
class="col-span-6 flex flex-wrap items-center justify-between gap-1 animate-bottom"
>
<UAvatarGroup :max="2">
<UAvatar
src="https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac"
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/benjamincanac 2x"
alt="benjamincanac"
width="40"
height="40"
loading="lazy"
/>
<UAvatar
src="https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux"
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/Atinux 2x"
alt="Atinux"
width="40"
height="40"
loading="lazy"
/>
<UAvatar
src="https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin"
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/smarroufin 2x"
alt="smarroufin"
width="40"
height="40"
loading="lazy"
/>
</UAvatarGroup>
<UButton label="Button" icon="i-heroicons-pencil-square" />
<UButton label="Button" icon="i-heroicons-pencil-square" />
<UBadge label="Badge" />
<UBadge label="Badge" />
<UColorModeToggle />
<UColorModeToggle />
<PaginationExampleBasic />
</div>
<PaginationExampleBasic />
</div>
<div class="col-span-3 row-span-8 gap-6 flex flex-col justify-between">
<UNotification :id="1" title="Notification" description="This is a notification!" icon="i-heroicons-command-line" />
<div :ref="(el) => (refs[5] = el)" class="col-span-3 row-span-8 gap-6 flex flex-col justify-between animate-left">
<UNotification
:id="1"
title="Notification"
description="This is a notification!"
icon="i-heroicons-command-line"
:ui="{ shadow: 'shadow' }"
:close-button="null"
:timeout="30000"
/>
<TabsExampleItemCustomSlot />
<TabsExampleItemCustomSlot />
<UCard class="flex-shrink-0">
<div class="flex items-center gap-4 justify-center">
<USkeleton class="h-14 w-14 flex-shrink-0" :ui="{ rounded: 'rounded-full' }" />
<div class="space-y-3 flex-1">
<USkeleton class="h-4 w-full" />
<USkeleton class="h-4 w-2/3" />
</div>
<UCard class="flex-shrink-0">
<div class="flex items-center gap-4 justify-center">
<USkeleton class="h-14 w-14 flex-shrink-0" :ui="{ rounded: 'rounded-full' }" />
<div class="space-y-3 flex-1">
<USkeleton class="h-4 w-full" />
<USkeleton class="h-4 w-2/3" />
</div>
</UCard>
</div>
</div>
</UCard>
</div>
<div class="col-span-5 row-span-2 flex flex-col">
<UCard :ui="{ body: { base: 'flex-1 flex flex-col overflow-y-auto', padding: '' } }" class="col-span-4 row-span-6 flex-1 flex flex-col">
<CommandPaletteExampleGroups />
</UCard>
</div>
<div :ref="(el) => (refs[6] = el)" class="col-span-5 row-span-2 flex flex-col animate-bottom">
<UCard
:ui="{ body: { base: 'flex-1 flex flex-col overflow-y-auto', padding: '' } }"
class="col-span-4 row-span-6 flex-1 flex flex-col"
>
<CommandPaletteExampleGroups />
</UCard>
</div>
<div class="col-span-2 row-span-2 gap-6 flex flex-col">
<CheckboxExample />
<div :ref="(el) => (refs[7] = el)" class="col-span-2 row-span-2 gap-6 flex flex-col animate-right z-10">
<CheckboxExample />
<InputExampleClearable />
<InputExampleClearable />
<UFormGroup label="Labels">
<SelectMenuExampleCreatable />
</UFormGroup>
<UFormGroup label="Labels">
<SelectMenuExampleCreatable />
</UFormGroup>
<UCard :ui="{ body: { padding: '!p-1' } }">
<VerticalNavigationExampleAvatarSlot />
</UCard>
</div>
<UCard :ui="{ body: { padding: '!p-1' } }">
<VerticalNavigationExampleAvatarSlot />
</UCard>
</div>
<div class="col-span-7 row-span-6">
<UCard :ui="{ body: { padding: '' } }">
<TableExampleClickable :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" />
</UCard>
</div>
</ULandingGrid>
</Transition>
<div :ref="(el) => (refs[8] = el)" class="col-span-7 row-span-6 animate-bottom">
<UCard :ui="{ body: { padding: '' } }">
<TableExampleClickable :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" />
</UCard>
</div>
</ULandingGrid>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
<style scoped lang="postcss">
.animate-top {
animation: translateDown;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
.animate-bottom {
animation: translateUp;
}
.animate-left {
animation: translateLeft;
}
.animate-right {
animation-name: translateRight;
}
@keyframes translateDown {
0% {
transform: translate3D(0, -100px, 0);
opacity: 0;
}
100% {
transform: translateY(0, 0, 0);
opacity: 1;
}
}
@keyframes translateUp {
0% {
transform: translate3D(0, 100px, 0);
opacity: 0;
}
100% {
transform: translateY(0, 0, 0);
opacity: 1;
}
}
@keyframes translateLeft {
0% {
transform: translate3D(-100px, 0, 0);
opacity: 0;
}
100% {
transform: translate3D(0, 0, 0);
opacity: 1;
}
}
@keyframes translateRight {
0% {
transform: translate3D(100px, 0, 0);
opacity: 0;
}
100% {
transform: translate3D(0, 0, 0);
opacity: 1;
}
}
</style>

View File

@@ -1,57 +1,43 @@
import type { NavItem, ParsedContent } from '@nuxt/content/dist/runtime/types'
export const useContentSource = () => {
const route = useRoute()
const router = useRouter()
const config = useRuntimeConfig().public
const branches = [{
const branches = computed(() => [{
id: 'dev',
name: 'dev',
icon: 'i-heroicons-cube',
suffix: 'dev',
label: 'Edge'
label: 'Edge',
disabled: route.path.startsWith('/dev'),
click: () => select({ name: 'dev' })
}, {
id: 'main',
name: 'main',
icon: 'i-heroicons-cube',
suffix: 'latest',
label: `v${config.version}`
}]
label: `v${config.version}`,
disabled: !route.path.startsWith('/dev'),
click: () => select({ name: 'main' })
}])
const branch = computed(() => branches.find(b => b.name === (route.path.startsWith('/dev') ? 'dev' : 'main')))
const branch = computed(() => branches.value.find(b => b.name === (route.path.startsWith('/dev') ? 'dev' : 'main')))
const prefix = computed(() => `/${branch.value.name}`)
function removePrefixFromNavigation (navigation: NavItem[]): NavItem[] {
return navigation.map((link) => {
const { _path, children, ...rest } = link
return {
...rest,
_path: route.path.startsWith(prefix.value) ? _path : _path.replace(new RegExp(`^${prefix.value}`, 'g'), ''),
children: children?.length ? removePrefixFromNavigation(children) : undefined
}
})
}
function removePrefixFromFiles (files: ParsedContent[]) {
return files.map((file) => {
if (!file) {
function select (branch) {
if (branch.name === 'dev') {
if (route.path.startsWith('/dev')) {
return
}
const { _path, ...rest } = file
return {
...rest,
_path: route.path.startsWith(prefix.value) ? _path : _path.replace(new RegExp(`^${prefix.value}`, 'g'), '')
}
})
router.push(`/dev${route.path}`)
} else {
router.push(route.path.replace('/dev', ''))
}
}
return {
branches,
branch,
prefix,
removePrefixFromNavigation,
removePrefixFromFiles
select
}
}

View File

@@ -18,6 +18,7 @@ This module has been developed by the [NuxtLabs](https://nuxtlabs.com/) team for
- Keyboard shortcuts
- Bundled icons
- Fully typed
- [Figma Kit](https://www.figma.com/community/file/1288455405058138934)
## Credits

View File

@@ -1,5 +1,7 @@
---
description: 'Learn how to customize the look and feel of the components.'
navigation:
badge: New
---
## Overview
@@ -79,7 +81,7 @@ This can also happen when you bind a dynamic color to a component: `<UBadge :col
### `app.config.ts`
Components are styled with Tailwind CSS but classes are all defined in the default [app.config.ts](https://github.com/nuxt/ui/blob/dev/src/runtime/app.config.ts) file. You can override those in your own `app.config.ts`.
Components are styled with Tailwind CSS but classes are all defined in the default [ui.config.ts](https://github.com/nuxt/ui/blob/dev/src/runtime/ui.config.ts) file. You can override those in your own `app.config.ts`.
```ts [app.config.ts]
export default defineAppConfig({
@@ -91,6 +93,25 @@ export default defineAppConfig({
})
```
Thanks to [tailwind-merge](https://github.com/dcastil/tailwind-merge), the `app.config.ts` is smartly merged with the default config. This means you don't have to rewrite everything.
You can change this behaviour by setting `strategy` to `override` in your `app.config.ts`: :u-badge{label="New" class="!rounded-full" variant="subtle"}
```ts [app.config.ts]
export default defineAppConfig({
ui: {
strategy: 'override',
button: {
color: {
white: {
solid: 'bg-white dark:bg-gray-900'
}
}
}
}
})
```
### `ui` prop
Each component has a `ui` prop that allows you to customize everything specifically.
@@ -113,25 +134,36 @@ For example, the default preset of the `FormGroup` component looks like this:
```json
{
...
"label": {
"base": "block font-medium text-gray-700 dark:text-gray-200",
...
"base": "block font-medium text-gray-700 dark:text-gray-200"
}
...
}
```
To change the font of the `label`, you only need to write:
```vue
<UFormGroup name="email" label="Email" :ui="{ label: { base: 'font-semibold' } }">
...
</UFormGroup>
<UFormGroup name="email" label="Email" :ui="{ label: { base: 'font-semibold' } }" />
```
This will smartly replace the `font-medium` by `font-semibold` and prevent any class duplication and any class priority issue.
You can change this behaviour by setting `strategy` to `override` inside the `ui` prop: :u-badge{label="New" class="!rounded-full" variant="subtle"}
```vue
<UButton
to="https://github.com/nuxt/ui"
:ui="{
strategy: 'override',
color: {
white: {
solid: 'bg-white dark:bg-gray-900'
}
}
}"
/>
```
### `class` attribute
You can also use the `class` attribute to add classes to the component.

View File

@@ -0,0 +1,188 @@
---
title: Contributing
description: Learn how to contribute to Nuxt UI.
---
## Overview
Nuxt UI thrives thanks to its fantastic community ❤️, which contributes by submitting issues, creating pull requests, and offering valuable feedback.
Before reporting a bug or reporting a feature, please make sure that you have read through our documentation and existing [issues](https://github.com/nuxt/ui/issues).
## Submitting a Pull Request
### 1. Before You Start
Check if there's an existing issue describing the problem or feature request you're working on. If there is, please leave a comment on the issue to let us know you're working on it.
If there isn't, open a new issue to discuss the problem or feature.
### 2. Local Development Setup
To begin local development, follow these steps:
1. Clone the `nuxt/ui` repository to your local machine:
```sh
git clone https://github.com/nuxt/ui.git
```
2. Install dependencies and prepare the project:
```sh
pnpm install
pnpm run dev:prepare
```
3. To configure your local development environment, use the following commands:
- To work on the **documentation** in the `docs` folder, run:
```sh
pnpm run dev
```
- To test the components located in the `playground` folder within `app.vue`, run:
```sh
pnpm run play
```
#### IDE Setup
We recommend using VS Code along with the ESLint extension. You can enable auto-fix and formatting when saving your code. Here's how:
```json
{
"editor.codeActionsOnSave": {
"source.fixAll": false,
"source.fixAll.eslint": true
}
}
```
You can also use the `lint` command:
```sh
pnpm run lint # check for linting errors
pnpm run lint:fix # fix linting errors
```
#### No Prettier
Since ESLint is already configured to format the code, there's no need for duplicating functionality with Prettier.
If you have Prettier installed in your editor, we recommend disabling it to avoid conflicts.
#### Type Checking
We use TypeScript for type checking. You can use the `typecheck` command to check for type errors:
```sh
pnpm run typecheck
```
### 3. Commit Conventions
Use Conventional Commits for commit messages with the following format:
```
<type>(optional scope): <description>
[optional body]
[optional footer(s)]
```
#### Types
- `feat` : for new features.
- `fix` : for bug fixes.
- `refactor` : for code changes that are neither bug fixes nor new features.
- `perf` : for code refactoring that improves performance.
- `test` : for code related to automatic testing.
- `style` : for refactoring related to code style (not for CSS).
- `docs` : for changes related to documentation.
- `chore` : for anything else.
#### Scope
Where the change occurred (e.g., `Table`, `Alert`, `Accordion`, etc.).
#### Description
A summary of the changes made.
#### Examples
```
feat(Alert): new component
chore(Table): improve accessibility
docs: migrate to @nuxt/ui-pro
```
### 4. Making the Pull Request
- Follow the guide for creating a pull request and ensure your PR's title adheres to the Commit Convention. Mention any related issues in the PR description.
- Multiple commits are fine; no need to rebase or force push. We'll use `Squash and Merge` when merging.
- Ensure linting and make tests manually before submitting the PR. Avoid making unrelated changes.
### 5. After You've Made a Pull Request
We'll review it promptly. If assigned to a maintainer, they'll review it carefully. Ignore the red text; it's for tracking purposes.
## Project Structure
In this project, you'll find a variety of folders and files that serve different purposes. Here's an overview of the main ones:
- **Documentation - `docs`** :
The documentation is located in the `docs` folder. It's a Nuxt app that uses the `@nuxt/content` module to generate the documentation pages from Markdown files. Here's a breakdown of its structure:
```
docs/
├── components/
│ ├── examples/ # Components used in documentation as examples
│ └── themes/ # Components used in the examples page in the theming section
├── content/ # Documentation, separated into categories according to component types
│ ├── 1.getting-started/
│ │ ├── 1.index.md
│ │ ├── 2.installation.md
│ │ ├── ... etc
│ ├── 2.elements/ # The category of components, which are elements
│ │ ├── 1.accordion.md # Docs for a single component (i.e., accordion)
│ │ ├── 2.alert.md
│ │ ├── ... etc
└── ... etc
```
- **Components - `src`** :
The components are located in the `src` folder. It's separated into categories according to component types. Here's a breakdown of its structure:
```
src/
├── runtime/
│ ├── composables/ # Composable functions used in components
│ ├── components/ # Components folder, separated into categories according to component types
│ │ ├── data/ # The category of components, which are data related
│ │ │ ├── table.vue/ # Table component
│ │ │ ├── elements/ # Elements category
│ │ │ │ ├── ...etc/
│ │ │ └── ... etc/
│ │ ├── plugins/ # Plugins used in components
│ │ ├── utils/ # Utility functions used on the components page (e.g., lodash)
│ │ ├── types/ # Types used in components
│ │ │ ├── accordion.d.ts/ # [componentName].d.ts type used for single component
│ │ │ ├── avatar.d.ts/
│ │ │ └── ... etc/
│ │ ├── ui.config.ts/ # Configuration file used to apply styles to every component
├── colors.ts/ # Everything related to color functions (e.g., safelistByComponent, generateSafelist)
└── ... etc/ # Other files and folders
```
## Thanks
Thank you again for being interested in this project! You are awesome! ❤️

View File

@@ -17,6 +17,6 @@ The Link component is a wrapper around [`<NuxtLink>`](https://nuxt.com/docs/api/
The incentive behind this is to provide the same API as NuxtLink back in Nuxt 2 / Vue 2. You can read more about it in the Vue Router [migration from Vue 2](https://router.vuejs.org/guide/migration/#removal-of-the-exact-prop-in-router-link) guide.
It also renders an `<a>` tag when a `to` prop is provided, otherwise it renders a `<button>` tag.
It also renders an `<a>` tag when a `to` prop is provided, otherwise it defaults to rendering a `<button>` tag. The default behavior can be customized using the `as` prop.
It is used underneath by the [Button](/elements/button), [Dropdown](/elements/dropdown) and [VerticalNavigation](/navigation/vertical-navigation) components.

View File

@@ -375,7 +375,7 @@ const selected = ref(people[3])
<template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton color="gray">
<UButton color="gray" class="flex-1 justify-between">
{{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />

View File

@@ -190,6 +190,115 @@ code: >-
This will only work with form elements that support the `size` prop.
::
## Slots
### `label`
Use the `#label` slot to set the custom content for label.
::component-card
---
slots:
label: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
default: <UInput model-value="benjamincanac" placeholder="you@example.com" />
---
#label
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mr-2 inline-flex"}
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com"}
::
### `description`
Use the `#description` slot to set the custom content for description.
::component-card
---
slots:
description: Write only valid email address <UIcon name="i-heroicons-arrow-right-20-solid" />
default: <UInput model-value="benjamincanac" placeholder="you@example.com" />
props:
label: 'Email'
---
#description
Write only valid email address :u-icon{name="i-heroicons-information-circle" class="align-middle"}
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com"}
::
### `hint`
Use the `#hint` slot to set the custom content for hint.
::component-card
---
slots:
hint: <UIcon name="i-heroicons-arrow-right-20-solid" />
default: <UInput model-value="benjamincanac" placeholder="you@example.com" />
props:
label: 'Step 1'
---
#hint
:u-icon{name="i-heroicons-arrow-right-20-solid"}
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com"}
::
### `help`
Use the `#help` slot to set the custom content for help.
::component-card
---
slots:
help: Here are some examples <UIcon name="i-heroicons-arrow-right-20-solid" />
default: <UInput model-value="benjamincanac" placeholder="you@example.com" />
props:
label: 'Email'
---
#help
Here are some examples :u-icon{name="i-heroicons-information-circle" class="align-middle"}
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com"}
::
### `error`
Use the `#error` slot to set the custom content for error.
::component-example
#default
:form-group-error-slot-example{class="w-60"}
#code
```vue
<template>
<UFormGroup label="Email" :error="!email && 'You must enter an email'" help="This is a nice email!">
<template #default="{ error }">
<UInput v-model="email" type="email" placeholder="Enter email" :trailing-icon="error ? 'i-heroicons-exclamation-triangle-20-solid' : undefined" />
</template>
<template #error="{ error }">
<UAlert v-if="error" icon="i-heroicons-exclamation-triangle-20-solid" :title="error" color="red" />
<UAlert v-else icon="i-heroicons-check-circle-20-solid" title="Your email is valid" color="green" />
</template>
</UFormGroup>
</template>
<script setup lang="ts">
const email = ref('')
</script>
```
::
## Props
:component-props

View File

@@ -524,6 +524,65 @@ excludedProps:
---
::
## Styling
You can apply styles to `tr` and `td` elements by passing a `class` to rows.
Also, you can apply styles to `th` elements by passing a `class` to columns.
::component-example
---
padding: false
---
#default
:table-example-style{class="w-full"}
#code
```vue
<script setup>
const columns = [{
key: 'id',
label: '#'
}, {
key: 'quantity',
label: 'Quantity',
class: 'italic' // Apply style to column header
}, {
key: 'name',
label: 'Name'
}]
const items = [{
id: 1,
name: 'Apple',
quantity: { value: 100, class: 'bg-green-500/50 dark:bg-green-400/50' } // Apply style to td
}, {
id: 2,
name: 'Orange',
quantity: { value: 0 },
class: 'bg-red-500/50 dark:bg-red-400/50 animate-pulse' // Apply style to tr
}, {
id: 3,
name: 'Banana',
quantity: { value: 30, class: 'bg-green-500/50 dark:bg-green-400/50' }
}, {
id: 4,
name: 'Mango',
quantity: { value: 5, class: 'bg-green-500/50 dark:bg-green-400/50' }
}]
</script>
<template>
<UTable :rows="items" :columns="columns">
<template #quantity-data="{ row }">
{{ row.quantity.value }}
</template>
</UTable>
</template>
```
::
## Slots
You can use slots to customize the header and data cells of the table.

View File

@@ -125,8 +125,6 @@ const links = [{
to: 'https://github.com/benjamincanac',
target: '_blank'
}, ...]
const { ui } = useAppConfig()
</script>
<template>
@@ -134,8 +132,8 @@ const { ui } = useAppConfig()
<template #avatar="{ link }">
<UAvatar
v-if="link.avatar"
v-bind="{ size: ui.verticalNavigation.avatar.size, ...link.avatar }"
:class="[ui.verticalNavigation.avatar.base]"
v-bind="link.avatar"
size="3xs"
/>
<UIcon v-else name="i-heroicons-user-circle-20-solid" class="text-lg" />
</template>

View File

@@ -6,7 +6,7 @@ hero:
description: 'Nuxt UI simplifies the creation of stunning and responsive web applications with its<br class="hidden lg:block"> comprehensive collection of fully styled and customizable UI components designed for Nuxt.'
sections:
- slot: demo
class: 'hidden lg:block dark:bg-gradient-to-b from-gray-900 to-gray-950/50 !pt-0'
class: 'hidden lg:block dark:bg-gradient-to-b from-gray-900 to-gray-950/50 !pt-12'
- title: Everything you expect from a<br class="hidden lg:block"> <span class="text-primary">UI component library</span>
slot: features
class: 'dark:bg-gradient-to-b from-gray-900 to-gray-950/50 dark:lg:bg-none dark:lg:bg-gray-950/50'

View File

@@ -11,7 +11,7 @@
</UContainer>
<ClientOnly>
<UDocsSearch :files="files" :navigation="navigation" />
<LazyUDocsSearch :files="files" :navigation="navigation" />
</ClientOnly>
<UNotifications />
@@ -20,8 +20,7 @@
<script setup lang="ts">
import type { NuxtError } from '#app'
const { prefix, removePrefixFromNavigation, removePrefixFromFiles } = useContentSource()
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
useSeoMeta({
title: 'Page not found',
@@ -32,22 +31,18 @@ defineProps<{
error: NuxtError
}>()
const { data: navigation } = await useLazyAsyncData('navigation', () => fetchContentNavigation(), {
default: () => [],
transform: (navigation) => {
navigation = navigation.find(link => link._path === prefix.value)?.children || []
const { branch } = useContentSource()
return prefix.value === '/main' ? removePrefixFromNavigation(navigation) : navigation
}
})
const { data: nav } = await useAsyncData('navigation', () => fetchContentNavigation())
const { data: files } = useLazyFetch<ParsedContent[]>('/api/search.json', { default: () => [], server: false })
const { data: files } = await useLazyAsyncData('files', () => queryContent().where({ _type: 'markdown', navigation: { $ne: false } }).find(), {
default: () => [],
transform: (files) => {
files = files.filter(file => file._path.startsWith(prefix.value))
// Computed
return prefix.value === '/main' ? removePrefixFromFiles(files) : files
}
const navigation = computed(() => {
const main = nav.value.filter(item => item._path !== '/dev')
const dev = nav.value.find(item => item._path === '/dev')?.children
return branch.value?.name === 'dev' ? dev : main
})
// Provide

View File

@@ -3,7 +3,7 @@
<UContainer>
<UPage>
<template #left>
<UAside :links="anchors">
<UAside>
<BranchSelect />
<UNavigationTree :links="mapContentNavigation(navigation)" />
@@ -19,23 +19,5 @@
<script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
const { mapContentNavigation } = useElementsHelpers()
const navigation = inject<NavItem[]>('navigation')
const anchors = [{
label: 'Documentation',
icon: 'i-heroicons-book-open-solid',
to: '/getting-started'
}, {
label: 'Playground',
icon: 'i-simple-icons-stackblitz',
to: 'https://stackblitz.com/edit/nuxt-ui?file=app.config.ts,app.vue',
target: '_blank'
}, {
label: 'Releases',
icon: 'i-heroicons-rocket-launch-solid',
to: 'https://github.com/nuxt/ui/releases',
target: '_blank'
}]
</script>

View File

@@ -7,7 +7,7 @@ import pkg from '../package.json'
const { resolve } = createResolver(import.meta.url)
export default defineNuxtConfig({
extends: process.env.NUXT_ELEMENTS_PATH || '@nuxthq/elements',
extends: process.env.NUXT_UI_PRO_PATH || '@nuxt/ui-pro',
modules: [
'@nuxt/content',
'nuxt-og-image',
@@ -18,8 +18,7 @@ export default defineNuxtConfig({
'@nuxtjs/google-fonts',
'@nuxtjs/plausible',
'@vueuse/nuxt',
'nuxt-component-meta',
'nuxt-lodash'
'nuxt-component-meta'
],
runtimeConfig: {
public: {
@@ -33,14 +32,13 @@ export default defineNuxtConfig({
},
content: {
sources: {
// overwrite default source AKA `content` directory
content: {
dev: {
prefix: '/dev',
driver: 'fs',
base: resolve('./content')
},
main: {
prefix: '/main',
// overwrite default source AKA `content` directory
content: {
driver: 'github',
repo: 'nuxt/ui',
branch: 'main',
@@ -70,7 +68,7 @@ export default defineNuxtConfig({
},
componentMeta: {
globalsOnly: true,
exclude: ['@nuxtjs/mdc', resolve('./components'), resolve('@nuxthq/elements/components')],
exclude: ['@nuxtjs/mdc', resolve('./components'), resolve('@nuxt/ui-pro/components')],
metaFields: {
props: true,
slots: false,
@@ -78,10 +76,6 @@ export default defineNuxtConfig({
exposed: false
}
},
typescript: {
strict: false,
includeWorkspace: true
},
hooks: {
// Related to https://github.com/nuxt/nuxt/pull/22558
'components:extend': (components) => {

View File

@@ -5,27 +5,27 @@
"@nuxt/ui": "workspace:latest"
},
"devDependencies": {
"@iconify-json/heroicons": "latest",
"@iconify-json/simple-icons": "latest",
"@nuxt/content": "^2.8.2",
"@nuxt/devtools": "^0.8.2",
"@iconify-json/heroicons": "^1.1.12",
"@iconify-json/simple-icons": "^1.1.73",
"@nuxt/content": "^2.8.5",
"@nuxt/devtools": "^0.8.5",
"@nuxt/eslint-config": "^0.2.0",
"@nuxthq/elements": "npm:@nuxthq/elements-edge@0.0.1-28236037.22a1d4d",
"@nuxthq/studio": "^0.13.4",
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@0.0.1-28265230.40bb224",
"@nuxthq/studio": "^0.14.1",
"@nuxtjs/fontaine": "^0.4.1",
"@nuxtjs/google-fonts": "^3.0.2",
"@nuxtjs/plausible": "^0.2.1",
"@nuxtjs/plausible": "^0.2.3",
"@vueuse/nuxt": "^10.4.1",
"eslint": "^8.48.0",
"joi": "^17.10.1",
"nuxt": "^3.7.1",
"nuxt-component-meta": "^0.5.3",
"nuxt-lodash": "^2.5.0",
"nuxt-og-image": "^2.0.25",
"eslint": "^8.50.0",
"joi": "^17.10.2",
"nuxt": "^3.7.4",
"nuxt-component-meta": "^0.5.4",
"nuxt-og-image": "^2.0.28",
"typescript": "^5.2.2",
"ufo": "^1.3.0",
"v-calendar": "^3.0.3",
"yup": "^1.2.0",
"ufo": "^1.3.1",
"v-calendar": "^3.1.0",
"valibot": "^0.17.1",
"yup": "^1.3.1",
"zod": "^3.22.2"
}
}

View File

@@ -7,7 +7,7 @@
<UDivider v-if="surround?.length" />
<UDocsSurround :surround="removePrefixFromFiles(surround)" />
<UDocsSurround :surround="(surround as ParsedContent[])" />
</UPageBody>
<template v-if="page.body?.toc?.links?.length" #right>
@@ -25,25 +25,34 @@
</template>
<script setup lang="ts">
import { withoutTrailingSlash } from 'ufo'
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
const route = useRoute()
const { prefix, removePrefixFromFiles } = useContentSource()
const { findPageHeadline } = useElementsHelpers()
const { branch } = useContentSource()
definePageMeta({
layout: 'docs'
})
const path = computed(() => route.path.startsWith(prefix.value) ? route.path : `${prefix.value}${route.path}`)
const { data: page } = await useAsyncData(path.value, () => queryContent(path.value).findOne())
const { data: page } = await useAsyncData(route.path, () => queryContent(route.path).findOne())
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found' })
}
const { data: surround } = await useAsyncData(`${path.value}-surround`, () => {
return queryContent(prefix.value)
.where({ _extension: 'md', navigation: { $ne: false } })
.findSurround((path.value.endsWith('/') ? path.value.slice(0, -1) : path.value))
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
return queryContent()
.where({
_extension: 'md',
_path: {
[branch.value?.name === 'dev' ? '$eq' : '$ne']: new RegExp('^/dev')
},
navigation: {
$ne: false
}
})
.only(['title', 'description', '_path'])
.findSurround(withoutTrailingSlash(route.path))
})
useSeoMeta({
@@ -65,17 +74,27 @@ const headline = computed(() => findPageHeadline(page.value))
const links = computed(() => [{
icon: 'i-heroicons-pencil-square',
label: 'Edit this page',
to: `https://github.com/nuxt/ui/edit/dev/docs/content/${page?.value?._file.split('/').slice(1).join('/')}`,
to: `https://github.com/nuxt/ui/edit/dev/docs/content/${branch.value?.name === 'dev' ? page?.value?._file.split('/').slice(1).join('/') : page?.value?._file}`,
target: '_blank'
}, {
icon: 'i-heroicons-star',
label: 'Star on GitHub',
to: 'https://github.com/nuxt/ui',
target: '_blank'
}, {
icon: 'i-heroicons-chat-bubble-bottom-center-text',
label: 'Chat on Discord',
to: 'https://discord.com/channels/473401852243869706/1153996761426300948',
target: '_blank'
}, {
icon: 'i-heroicons-book-open',
label: 'Nuxt documentation',
label: 'Nuxt docs',
to: 'https://nuxt.com',
target: '_blank'
}, {
icon: 'i-simple-icons-figma',
label: 'Figma Kit',
to: 'https://www.figma.com/community/file/1288455405058138934/nuxt-ui',
target: '_blank'
}])
</script>

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div>
<ULandingHero v-bind="page.hero" :ui="{ base: 'relative z-[1]', container: 'max-w-3xl' }" class="mb-[calc(var(--header-height)*2)]">
@@ -19,6 +20,7 @@
autocomplete="off"
icon="i-heroicons-command-line"
input-class="select-none"
aria-label="Install @nuxt/ui"
size="lg"
:ui="{ base: 'disabled:cursor-default', icon: { trailing: { pointer: '' } } }"
>
@@ -148,8 +150,9 @@
width="40"
height="40"
size="md"
loading="lazy"
>
<NuxtLink :to="`https://github.com/${contributor.username}`" target="_blank" class="focus:outline-none" tabindex="-1">
<NuxtLink :to="`https://github.com/${contributor.username}`" :aria-label="contributor.username" target="_blank" class="focus:outline-none" tabindex="-1">
<span class="absolute inset-0" aria-hidden="true" />
</NuxtLink>
</UAvatar>
@@ -179,10 +182,9 @@
</template>
<script setup lang="ts">
import { pick } from 'lodash-es'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
const { data: page } = await useAsyncData('index', () => queryContent('/').findOne())
const { data: page } = await useAsyncData('index', () => queryContent('/dev').findOne())
const { data: module } = await useFetch<{
stats: {
downloads: number
@@ -192,7 +194,7 @@ const { data: module } = await useFetch<{
username: string
}[]
}>('https://api.nuxt.com/modules/ui', {
transform: (module) => pick(module, ['stats', 'contributors'])
transform: ({ stats, contributors }) => ({ stats, contributors })
})
const source = ref('npm i @nuxt/ui')

24
docs/pages/playground.vue Normal file
View File

@@ -0,0 +1,24 @@
<script setup>
const title = 'Playground'
const description = 'Play online with our interactive Nuxt Image playground.'
useSeoMeta({
title,
ogTitle: 'Nuxt UI Playground',
description
})
defineOgImage({
component: 'Docs',
title,
description
})
</script>
<template>
<div class="h-[calc(100vh-var(--header-height))]">
<ClientOnly>
<iframe :src="`https://stackblitz.com/edit/nuxt-ui?embed=1&file=app.config.ts,app.vue&theme=${$colorMode.preference}`" width="100%" height="100%" />
</ClientOnly>
</div>
</template>

View File

@@ -2,11 +2,6 @@ import type { Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
export default <Partial<Config>>{
content: {
files: [
'content/**/*.yml'
]
},
theme: {
extend: {
fontFamily: {

View File

@@ -1,6 +1,6 @@
{
"name": "@nuxt/ui",
"version": "2.8.1",
"version": "2.9.0",
"repository": "https://github.com/nuxt/ui",
"license": "MIT",
"exports": {
@@ -25,15 +25,16 @@
"build:docs": "nuxi generate docs",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "nuxi typecheck",
"prepare": "nuxi prepare docs",
"typecheck": "vue-tsc --noEmit && nuxi typecheck docs",
"dev:prepare": "nuxt-module-build --stub && nuxt-module-build prepare && nuxi prepare docs",
"release": "release-it"
},
"dependencies": {
"@egoist/tailwindcss-icons": "^1.1.0",
"@egoist/tailwindcss-icons": "^1.2.0",
"@headlessui/tailwindcss": "^0.2.0",
"@headlessui/vue": "^1.7.16",
"@iconify-json/heroicons": "^1.1.12",
"@nuxt/kit": "^3.7.1",
"@nuxt/kit": "^3.7.4",
"@nuxtjs/color-mode": "^3.3.0",
"@nuxtjs/tailwindcss": "^6.8.0",
"@popperjs/core": "^2.11.8",
@@ -46,28 +47,25 @@
"@vueuse/math": "^10.4.1",
"defu": "^6.1.2",
"fuse.js": "^6.6.2",
"lodash-es": "^4.17.21",
"ohash": "^1.1.3",
"pathe": "^1.1.1",
"scule": "^1.0.0",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.2.0",
"@nuxt/module-builder": "^0.5.1",
"@release-it/conventional-changelog": "^7.0.1",
"eslint": "^8.48.0",
"joi": "^17.10.1",
"nuxt": "^3.7.1",
"release-it": "^16.1.5",
"@nuxt/module-builder": "^0.5.2",
"@release-it/conventional-changelog": "^7.0.2",
"eslint": "^8.50.0",
"joi": "^17.10.2",
"nuxt": "^3.7.4",
"release-it": "^16.2.1",
"typescript": "^5.2.2",
"unbuild": "^2.0.0",
"vue-tsc": "^1.8.10",
"yup": "^1.2.0",
"zod": "^3.22.2",
"valibot": "^0.13.1"
},
"pnpm": {
"patchedDependencies": {
"nuxt-component-meta@0.5.3": "patches/nuxt-component-meta@0.5.3.patch"
}
"valibot": "^0.17.1",
"vue-tsc": "^1.8.15",
"yup": "^1.3.1",
"zod": "^3.22.2"
}
}

View File

@@ -1,15 +0,0 @@
diff --git a/dist/module.mjs b/dist/module.mjs
index 286319046560f8ead5a26f811e7643fe990f9ee6..14be2f9551c24cd1e5c35dadaad9f83009f4c5a0 100644
--- a/dist/module.mjs
+++ b/dist/module.mjs
@@ -142,10 +142,6 @@ const module = defineNuxtModule({
references.push({
path: join(nuxt.options.buildDir, "component-meta.d.ts")
});
- tsConfig.compilerOptions = tsConfig.compilerOptions || {};
- tsConfig.compilerOptions.paths = tsConfig.compilerOptions.paths || {};
- tsConfig.compilerOptions.paths["#nuxt-component-meta"] = [withoutLeadingSlash(join(nuxt.options.buildDir, "/component-meta.mjs").replace(nuxt.options.rootDir, ""))];
- tsConfig.compilerOptions.paths["#nuxt-component-meta/types"] = [withoutLeadingSlash(join(nuxt.options.buildDir, "/component-meta.d.ts").replace(nuxt.options.rootDir, ""))];
});
nuxt.hook("nitro:config", (nitroConfig) => {
nitroConfig.handlers = nitroConfig.handlers || [];

1
playground/.nuxtrc Normal file
View File

@@ -0,0 +1 @@
imports.autoImport=true

3229
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,6 @@ git restore -s@ -SW -- .
# Bump versions to edge
pnpm jiti ./scripts/bump-edge
# Resolve pnpm
pnpm install
# Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc

View File

@@ -3,9 +3,6 @@
# Restore all git changes
git restore -s@ -SW -- .
# Resolve pnpm
pnpm install
# Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc

View File

@@ -1,4 +1,5 @@
import { omit, kebabCase, camelCase, upperFirst } from 'lodash-es'
import { omit } from './runtime/utils/lodash'
import { kebabCase, camelCase, upperFirst } from 'scule'
const colorsToExclude = [
'inherit',

View File

@@ -1,12 +1,12 @@
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, createResolver, addPlugin, resolvePath } from '@nuxt/kit'
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, createResolver, addPlugin } from '@nuxt/kit'
import defaultColors from 'tailwindcss/colors.js'
import { defaultExtractor as createDefaultExtractor } from 'tailwindcss/lib/lib/defaultExtractor.js'
import { iconsPlugin, getIconCollections } from '@egoist/tailwindcss-icons'
import { name, version } from '../package.json'
import { generateSafelist, excludeColors, customSafelistExtractor } from './colors'
import appConfig from './runtime/app.config'
type DeepPartial<T> = Partial<{ [P in keyof T]: DeepPartial<T[P]> | { [key: string]: string } }>
import createTemplates from './templates'
import * as config from './runtime/ui.config'
import type { DeepPartial, Strategy } from './runtime/types/utils'
const defaultExtractor = createDefaultExtractor({ tailwindConfig: { separator: ':' } })
@@ -16,13 +16,22 @@ delete defaultColors.trueGray
delete defaultColors.coolGray
delete defaultColors.blueGray
type UI = {
primary?: string
gray?: string
colors?: string[]
strategy?: Strategy
[key: string]: any
} & DeepPartial<typeof config>
declare module 'nuxt/schema' {
interface AppConfigInput {
ui?: UI
}
}
declare module '@nuxt/schema' {
interface AppConfigInput {
ui?: {
primary?: string
gray?: string
colors?: string[]
} & DeepPartial<typeof appConfig.ui>
ui?: UI
}
}
@@ -64,12 +73,9 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.build.transpile.push(runtimeDir)
nuxt.options.build.transpile.push('@popperjs/core', '@headlessui/vue')
nuxt.options.css.push(resolve(runtimeDir, 'ui.css'))
nuxt.options.alias['#ui'] = runtimeDir
const appConfigFile = await resolvePath(resolve(runtimeDir, 'app.config'))
nuxt.hook('app:resolve', (app) => {
app.configs.push(appConfigFile)
})
nuxt.options.css.push(resolve(runtimeDir, 'ui.css'))
nuxt.hook('tailwindcss:config', function (tailwindConfig) {
const globalColors: any = {
@@ -116,11 +122,12 @@ export default defineNuxtModule<ModuleOptions>({
const colors = excludeColors(globalColors)
// @ts-ignore
nuxt.options.appConfig.ui = {
...nuxt.options.appConfig.ui,
primary: 'green',
gray: 'cool',
colors
colors,
strategy: 'merge'
}
tailwindConfig.safelist = tailwindConfig.safelist || []
@@ -130,6 +137,8 @@ export default defineNuxtModule<ModuleOptions>({
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
})
createTemplates(nuxt)
// Modules
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
@@ -141,7 +150,8 @@ export default defineNuxtModule<ModuleOptions>({
require('@tailwindcss/forms')({ strategy: 'class' }),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/typography'),
require('@tailwindcss/container-queries')
require('@tailwindcss/container-queries'),
require('@headlessui/tailwindcss')
],
content: {
files: [

View File

@@ -1,10 +1,10 @@
<template>
<div :class="wrapperClass" v-bind="attrs">
<div :class="ui.wrapper" v-bind="attrs">
<table :class="[ui.base, ui.divide]">
<thead :class="ui.thead">
<tr :class="ui.tr.base">
<th v-if="modelValue" scope="col" class="ps-4">
<UCheckbox :checked="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" @change="onChange" />
<th v-if="modelValue" scope="col" :class="ui.checkbox.padding">
<UCheckbox :checked="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" aria-label="Select all" @change="onChange" />
</th>
<th v-for="(column, index) in columns" :key="index" scope="col" :class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size, column.class]">
@@ -49,12 +49,12 @@
</tr>
<template v-else>
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected, $attrs.onSelect && ui.tr.active]" @click="() => onSelect(row)">
<td v-if="modelValue" class="ps-4">
<UCheckbox v-model="selected" :value="row" @click.stop />
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected, $attrs.onSelect && ui.tr.active, row?.class]" @click="() => onSelect(row)">
<td v-if="modelValue" :class="ui.checkbox.padding">
<UCheckbox v-model="selected" :value="row" aria-label="Select row" @click.stop />
</td>
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]">
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size, row[column.key]?.class]">
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" :get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)">
{{ getRowData(row, column.key) }}
</slot>
@@ -67,22 +67,21 @@
</template>
<script lang="ts">
import { ref, computed, defineComponent, toRaw } from 'vue'
import { ref, computed, defineComponent, toRaw, toRef } from 'vue'
import type { PropType } from 'vue'
import { omit, capitalize, orderBy, get } from 'lodash-es'
import { upperFirst } from 'scule'
import { defu } from 'defu'
import { twMerge } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import UButton from '../elements/Button.vue'
import UIcon from '../elements/Icon.vue'
import UCheckbox from '../forms/Checkbox.vue'
import type { Button } from '../../types/button'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig, omit, get } from '../../utils'
import type { Strategy, Button } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { table } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof table>(appConfig.ui.strategy, appConfig.ui.table, table)
function defaultComparator<T> (a: T, z: T): boolean {
return a === z
@@ -122,15 +121,15 @@ export default defineComponent({
},
sortButton: {
type: Object as PropType<Button>,
default: () => appConfig.ui.table.default.sortButton
default: () => config.default.sortButton as Button
},
sortAscIcon: {
type: String,
default: () => appConfig.ui.table.default.sortAscIcon
default: () => config.default.sortAscIcon
},
sortDescIcon: {
type: String,
default: () => appConfig.ui.table.default.sortDescIcon
default: () => config.default.sortDescIcon
},
loading: {
type: Boolean,
@@ -138,27 +137,26 @@ export default defineComponent({
},
loadingState: {
type: Object as PropType<{ icon: string, label: string }>,
default: () => appConfig.ui.table.default.loadingState
default: () => config.default.loadingState
},
emptyState: {
type: Object as PropType<{ icon: string, label: string }>,
default: () => appConfig.ui.table.default.emptyState
default: () => config.default.emptyState
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.table>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue'],
setup (props, { emit, attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
setup (props, { emit, attrs: $attrs }) {
const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class'))
const ui = computed<Partial<typeof appConfig.ui.table>>(() => defuTwMerge({}, props.ui, appConfig.ui.table))
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const columns = computed(() => props.columns ?? Object.keys(omit(props.rows[0] ?? {}, ['click'])).map((key) => ({ key, label: capitalize(key), sortable: false })))
const columns = computed(() => props.columns ?? Object.keys(omit(props.rows[0] ?? {}, ['click'])).map((key) => ({ key, label: upperFirst(key), sortable: false })))
const sort = ref(defu({}, props.sort, { column: null, direction: 'asc' }))
@@ -169,7 +167,20 @@ export default defineComponent({
const { column, direction } = sort.value
return orderBy(props.rows, column, direction)
return props.rows.slice().sort((a, b) => {
const aValue = a[column]
const bValue = b[column]
if (aValue === bValue) {
return 0
}
if (direction === 'asc') {
return aValue < bValue ? -1 : 1
} else {
return aValue > bValue ? -1 : 1
}
})
})
const selected = computed({
@@ -224,12 +235,12 @@ export default defineComponent({
}
function onSelect (row) {
if (!attrs.onSelect) {
if (!$attrs.onSelect) {
return
}
// @ts-ignore
attrs.onSelect(row)
$attrs.onSelect(row)
}
function selectAllRows () {
@@ -239,7 +250,8 @@ export default defineComponent({
return
}
onSelect(row)
// @ts-ignore
$attrs.onSelect ? $attrs.onSelect(row) : selected.value.push(row)
})
}
@@ -256,10 +268,9 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
wrapperClass,
attrs,
// eslint-disable-next-line vue/no-dupe-keys
sort,
// eslint-disable-next-line vue/no-dupe-keys

View File

@@ -1,5 +1,5 @@
<template>
<div :class="wrapperClass">
<div :class="ui.wrapper">
<HDisclosure v-for="(item, index) in items" v-slot="{ open, close }" :key="index" :default-open="defaultOpen || item.defaultOpen">
<HDisclosureButton :ref="() => buttonRefs[index] = close" as="template" :disabled="item.disabled">
<slot :item="item" :index="index" :open="open" :close="close">
@@ -40,20 +40,22 @@
</template>
<script lang="ts">
import { ref, computed, defineComponent } from 'vue'
import { ref, computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { Disclosure as HDisclosure, DisclosureButton as HDisclosureButton, DisclosurePanel as HDisclosurePanel } from '@headlessui/vue'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import { defuTwMerge } from '../../utils'
import { useUI } from '../../composables/useUI'
import { mergeConfig, omit } from '../../utils'
import StateEmitter from '../../utils/StateEmitter'
import type { AccordionItem } from '../../types/accordion'
import { useAppConfig } from '#imports'
// TODO: Remove
import type { AccordionItem, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { accordion, button } from '#ui/ui.config'
const config = mergeConfig<typeof accordion>(appConfig.ui.strategy, appConfig.ui.accordion, accordion)
const configButton = mergeConfig<typeof button>(appConfig.ui.strategy, appConfig.ui.button, button)
export default defineComponent({
components: {
@@ -76,43 +78,47 @@ export default defineComponent({
},
openIcon: {
type: String,
default: () => appConfig.ui.accordion.default.openIcon
default: () => config.default.openIcon
},
closeIcon: {
type: String,
default: () => appConfig.ui.accordion.default.closeIcon
default: () => config.default.closeIcon
},
multiple: {
type: Boolean,
default: false
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.accordion>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
setup (props) {
const { ui, attrs } = useUI('accordion', toRef(props, 'ui'), config, toRef(props, 'class'))
const ui = computed<Partial<typeof appConfig.ui.accordion>>(() => defuTwMerge({}, props.ui, appConfig.ui.accordion))
const uiButton = computed<Partial<typeof appConfig.ui.button>>(() => appConfig.ui.button)
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const uiButton = computed<Partial<typeof configButton>>(() => configButton)
const buttonRefs = ref<Function[]>([])
function closeOthers (itemIndex: number) {
if (!props.items[itemIndex].closeOthers && props.multiple) {
function closeOthers (currentIndex: number) {
if (!props.items[currentIndex].closeOthers && props.multiple) {
return
}
buttonRefs.value.forEach((close, index) => {
if (index === itemIndex) return
const totalItems = buttonRefs.value.length
const order = Array.from({ length: totalItems }, (_, i) => (currentIndex + i) % totalItems)
.filter(index => index !== currentIndex)
.reverse()
for (const index of order) {
const close = buttonRefs.value[index]
close()
})
}
}
function onEnter (el: HTMLElement, done) {
@@ -139,11 +145,10 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
uiButton,
wrapperClass,
attrs,
buttonRefs,
closeOthers,
omit,

View File

@@ -25,29 +25,28 @@
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="action.click" />
</div>
<UButton v-if="closeButton" v-bind="{ ...ui.default.closeButton, ...closeButton }" @click.stop="$emit('close')" />
<UButton v-if="closeButton" aria-label="Close" v-bind="{ ...ui.default.closeButton, ...closeButton }" @click.stop="$emit('close')" />
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue'
import type { Avatar } from '../../types/avatar'
import type { Button } from '../../types/button'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import type { Avatar, Button, NestedKeyOf, Strategy } from '../../types'
import { mergeConfig } from '../../utils'
// @ts-expect-error
import appConfig from '#build/app.config'
import { omit } from 'lodash-es'
import { alert } from '#ui/ui.config'
import colors from '#ui-colors'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof alert>(appConfig.ui.strategy, appConfig.ui.alert, alert)
export default defineComponent({
components: {
@@ -67,7 +66,7 @@ export default defineComponent({
},
icon: {
type: String,
default: () => appConfig.ui.alert.default.icon
default: () => config.default.icon
},
avatar: {
type: Object as PropType<Avatar>,
@@ -75,40 +74,41 @@ export default defineComponent({
},
closeButton: {
type: Object as PropType<Button>,
default: () => appConfig.ui.alert.default.closeButton
default: () => config.default.closeButton as Button
},
actions: {
type: Array as PropType<(Button & { click?: Function })[]>,
default: () => []
},
color: {
type: String,
default: () => appConfig.ui.alert.default.color,
type: String as PropType<keyof typeof config.color | typeof colors[number]>,
default: () => config.default.color,
validator (value: string) {
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.alert.color)].includes(value)
return [...appConfig.ui.colors, ...Object.keys(config.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.alert.default.variant,
type: String as PropType<keyof typeof config.variant | NestedKeyOf<typeof config.color>>,
default: () => config.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.alert.variant),
...Object.values(appConfig.ui.alert.color).flatMap(value => Object.keys(value))
...Object.keys(config.variant),
...Object.values(config.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.alert>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['close'],
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.alert>>(() => defuTwMerge({}, props.ui, appConfig.ui.alert))
setup (props) {
const { ui, attrs } = useUI('alert', toRef(props, 'ui'), config)
const alertClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
@@ -119,13 +119,13 @@ export default defineComponent({
ui.value.shadow,
ui.value.padding,
variant?.replaceAll('{color}', props.color)
), attrs.class as string)
), props.class)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
alertClass
}
}

View File

@@ -20,18 +20,18 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch } from 'vue'
import { defineComponent, ref, computed, toRef, watch } from 'vue'
import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { AvatarSize, AvatarChipColor, AvatarChipPosition, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { omit } from 'lodash-es'
import { avatar } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof avatar>(appConfig.ui.strategy, appConfig.ui.avatar, avatar)
export default defineComponent({
components: {
@@ -53,27 +53,27 @@ export default defineComponent({
},
icon: {
type: String,
default: () => appConfig.ui.avatar.default.icon
default: () => config.default.icon
},
size: {
type: String,
default: () => appConfig.ui.avatar.default.size,
type: String as PropType<AvatarSize>,
default: () => config.default.size,
validator (value: string) {
return Object.keys(appConfig.ui.avatar.size).includes(value)
return Object.keys(config.size).includes(value)
}
},
chipColor: {
type: String,
default: () => appConfig.ui.avatar.default.chipColor,
type: String as PropType<AvatarChipColor>,
default: () => config.default.chipColor,
validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value)
}
},
chipPosition: {
type: String,
default: () => appConfig.ui.avatar.default.chipPosition,
type: String as PropType<AvatarChipPosition>,
default: () => config.default.chipPosition,
validator (value: string) {
return Object.keys(appConfig.ui.avatar.chip.position).includes(value)
return Object.keys(config.chip.position).includes(value)
}
},
chipText: {
@@ -84,16 +84,17 @@ export default defineComponent({
type: String,
default: ''
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.avatar>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.avatar>>(() => defuTwMerge({}, props.ui, appConfig.ui.avatar))
setup (props) {
const { ui, attrs } = useUI('avatar', toRef(props, 'ui'), config)
const url = computed(() => {
if (typeof props.src === 'boolean') {
@@ -112,7 +113,7 @@ export default defineComponent({
(error.value || !url.value) && ui.value.background,
ui.value.rounded,
ui.value.size[props.size]
), attrs.class as string)
), props.class)
})
const imgClass = computed(() => {
@@ -151,7 +152,9 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
imgClass,

View File

@@ -1,40 +1,43 @@
import { h, cloneVNode, computed, defineComponent } from 'vue'
import { h, cloneVNode, computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge, getSlotsChildren } from '../../utils'
import Avatar from './Avatar.vue'
import { useAppConfig } from '#imports'
// TODO: Remove
import UAvatar from './Avatar.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig, getSlotsChildren } from '../../utils'
import type { AvatarSize, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { avatar, avatarGroup } from '#ui/ui.config'
// const appConfig = useAppConfig()
const avatarConfig = mergeConfig<typeof avatar>(appConfig.ui.strategy, appConfig.ui.avatar, avatar)
const avatarGroupConfig = mergeConfig<typeof avatarGroup>(appConfig.ui.strategy, appConfig.ui.avatarGroup, avatarGroup)
export default defineComponent({
inheritAttrs: false,
props: {
size: {
type: String,
type: String as PropType<keyof typeof avatarConfig.size>,
default: null,
validator (value: string) {
return Object.keys(appConfig.ui.avatar.size).includes(value)
return Object.keys(avatarConfig.size).includes(value)
}
},
max: {
type: Number,
default: null
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.avatarGroup>>,
default: () => ({})
type: Object as PropType<Partial<typeof avatarGroupConfig & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defuTwMerge({}, props.ui, appConfig.ui.avatarGroup))
setup (props, { slots }) {
const { ui, attrs } = useUI('avatarGroup', toRef(props, 'ui'), avatarGroupConfig, toRef(props, 'class'))
const children = computed(() => getSlotsChildren(slots))
@@ -55,8 +58,8 @@ export default defineComponent({
}
if (max.value !== undefined && index === max.value) {
return h(Avatar, {
size: props.size || appConfig.ui.avatar.default.size,
return h(UAvatar, {
size: props.size || (avatarConfig.default.size as AvatarSize),
text: `+${children.value.length - max.value}`,
class: twJoin(ui.value.ring, ui.value.margin)
})
@@ -65,6 +68,6 @@ export default defineComponent({
return null
}).filter(Boolean).reverse())
return () => h('div', { class: twMerge(ui.value.wrapper, attrs.class as string), ...omit(attrs, ['class']) }, clones.value)
return () => h('div', { class: ui.value.wrapper, ...attrs.value }, clones.value)
}
})

View File

@@ -5,42 +5,42 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { BadgeColor, BadgeSize, BadgeVariant, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { badge } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof badge>(appConfig.ui.strategy, appConfig.ui.badge, badge)
export default defineComponent({
inheritAttrs: false,
props: {
size: {
type: String,
default: () => appConfig.ui.badge.default.size,
type: String as PropType<BadgeSize>,
default: () => config.default.size,
validator (value: string) {
return Object.keys(appConfig.ui.badge.size).includes(value)
return Object.keys(config.size).includes(value)
}
},
color: {
type: String,
default: () => appConfig.ui.badge.default.color,
type: String as PropType<BadgeColor>,
default: () => config.default.color,
validator (value: string) {
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.badge.color)].includes(value)
return [...appConfig.ui.colors, ...Object.keys(config.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.badge.default.variant,
type: String as PropType<BadgeVariant>,
default: () => config.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.badge.variant),
...Object.values(appConfig.ui.badge.color).flatMap(value => Object.keys(value))
...Object.keys(config.variant),
...Object.values(config.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
@@ -48,16 +48,17 @@ export default defineComponent({
type: [String, Number],
default: null
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.badge>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defuTwMerge({}, props.ui, appConfig.ui.badge))
setup (props) {
const { ui, attrs } = useUI('badge', toRef(props, 'ui'), config)
const badgeClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
@@ -68,11 +69,11 @@ export default defineComponent({
ui.value.rounded,
ui.value.size[props.size],
variant?.replaceAll('{color}', props.color)
), attrs.class as string)
), props.class)
})
return {
attrs: omit(attrs, ['class']),
attrs,
badgeClass
}
}

View File

@@ -17,19 +17,19 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, defineComponent, toRef } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import ULink from '../elements/Link.vue'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { ButtonColor, ButtonSize, ButtonVariant, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { button } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof button>(appConfig.ui.strategy, appConfig.ui.button, button)
export default defineComponent({
components: {
@@ -63,26 +63,26 @@ export default defineComponent({
default: true
},
size: {
type: String,
default: () => appConfig.ui.button.default.size,
type: String as PropType<ButtonSize>,
default: () => config.default.size,
validator (value: string) {
return Object.keys(appConfig.ui.button.size).includes(value)
return Object.keys(config.size).includes(value)
}
},
color: {
type: String,
default: () => appConfig.ui.button.default.color,
type: String as PropType<ButtonColor>,
default: () => config.default.color,
validator (value: string) {
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.button.color)].includes(value)
return [...appConfig.ui.colors, ...Object.keys(config.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.button.default.variant,
type: String as PropType<ButtonVariant>,
default: () => config.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.button.variant),
...Object.values(appConfig.ui.button.color).flatMap(value => Object.keys(value))
...Object.keys(config.variant),
...Object.values(config.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
@@ -92,7 +92,7 @@ export default defineComponent({
},
loadingIcon: {
type: String,
default: () => appConfig.ui.button.default.loadingIcon
default: () => config.default.loadingIcon
},
leadingIcon: {
type: String,
@@ -118,16 +118,17 @@ export default defineComponent({
type: Boolean,
default: false
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.button>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.button>>(() => defuTwMerge({}, props.ui, appConfig.ui.button))
setup (props, { slots }) {
const { ui, attrs } = useUI('button', toRef(props, 'ui'), config)
const isLeading = computed(() => {
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
@@ -151,7 +152,7 @@ export default defineComponent({
props.padded && ui.value[isSquare.value ? 'square' : 'padding'][props.size],
variant?.replaceAll('{color}', props.color),
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center'
), attrs.class as string)
), props.class)
})
const leadingIconName = computed(() => {
@@ -187,7 +188,7 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
attrs,
isLeading,
isTrailing,
isSquare,

View File

@@ -1,23 +1,24 @@
import { h, cloneVNode, computed, defineComponent } from 'vue'
import { h, cloneVNode, computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge, getSlotsChildren } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig, getSlotsChildren } from '../../utils'
import type { ButtonSize, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { button, buttonGroup } from '#ui/ui.config'
// const appConfig = useAppConfig()
const buttonConfig = mergeConfig<typeof button>(appConfig.ui.strategy, appConfig.ui.button, button)
const buttonGroupConfig = mergeConfig<typeof buttonGroup>(appConfig.ui.strategy, appConfig.ui.buttonGroup, buttonGroup)
export default defineComponent({
inheritAttrs: false,
props: {
size: {
type: String,
type: String as PropType<ButtonSize>,
default: null,
validator (value: string) {
return Object.keys(appConfig.ui.button.size).includes(value)
return Object.keys(buttonConfig.size).includes(value)
}
},
orientation: {
@@ -27,16 +28,17 @@ export default defineComponent({
return ['horizontal', 'vertical'].includes(value)
}
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.buttonGroup>>,
default: () => ({})
type: Object as PropType<Partial<typeof buttonGroupConfig & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defuTwMerge({}, props.ui, appConfig.ui.buttonGroup))
setup (props, { slots }) {
const { ui, attrs } = useUI('buttonGroup', toRef(props, 'ui'), buttonGroupConfig)
const children = computed(() => getSlotsChildren(slots))
@@ -58,12 +60,6 @@ export default defineComponent({
const clones = computed(() => children.value.map((node, index) => {
const vProps: any = {}
if (props.orientation === 'vertical') {
ui.value.wrapper = 'flex flex-col -space-y-px'
} else {
ui.value.wrapper = 'inline-flex -space-x-px'
}
if (props.size) {
vProps.size = props.size
}
@@ -83,6 +79,14 @@ export default defineComponent({
return cloneVNode(node, vProps)
}))
return () => h('div', { class: twMerge(twJoin(ui.value.wrapper, ui.value.rounded, ui.value.shadow), attrs.class as string), ...omit(attrs, ['class']) }, clones.value)
const wrapperClass = computed(() => {
return twMerge(twJoin(
ui.value.wrapper[props.orientation],
ui.value.rounded,
ui.value.shadow
), props.class)
})
return () => h('div', { class: wrapperClass.value, ...attrs.value }, clones.value)
}
})

View File

@@ -1,5 +1,5 @@
<template>
<HMenu v-slot="{ open }" as="div" :class="wrapperClass" v-bind="attrs" @mouseleave="onMouseLeave">
<HMenu v-slot="{ open }" as="div" :class="ui.wrapper" v-bind="attrs" @mouseleave="onMouseLeave">
<HMenuButton
ref="trigger"
as="div"
@@ -45,26 +45,23 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed, onMounted } from 'vue'
import { defineComponent, ref, computed, toRef, onMounted } from 'vue'
import type { PropType } from 'vue'
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue'
import ULink from '../elements/Link.vue'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper'
import { defuTwMerge } from '../../utils'
import type { DropdownItem } from '../../types/dropdown'
import type { PopperOptions } from '../../types/popper'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig, omit } from '../../utils'
import type { DropdownItem, PopperOptions, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { dropdown } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof dropdown>(appConfig.ui.strategy, appConfig.ui.dropdown, dropdown)
export default defineComponent({
components: {
@@ -104,16 +101,17 @@ export default defineComponent({
type: Number,
default: 0
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.dropdown>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.dropdown>>(() => defuTwMerge({}, props.ui, appConfig.ui.dropdown))
setup (props) {
const { ui, attrs } = useUI('dropdown', toRef(props, 'ui'), config, toRef(props, 'class'))
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
@@ -143,8 +141,6 @@ export default defineComponent({
return props.mode === 'hover' ? { paddingTop: `${offsetDistance}px`, paddingBottom: `${offsetDistance}px` } : {}
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
function onMouseOver () {
if (props.mode !== 'hover' || !menuApi.value) {
return
@@ -186,13 +182,12 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
trigger,
container,
containerStyle,
wrapperClass,
onMouseOver,
onMouseLeave,
omit

View File

@@ -5,17 +5,17 @@
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { toRef, defineComponent, computed } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { kbd } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof kbd>(appConfig.ui.strategy, appConfig.ui.kbd, kbd)
export default defineComponent({
inheritAttrs: false,
@@ -25,22 +25,23 @@ export default defineComponent({
default: null
},
size: {
type: String,
default: () => appConfig.ui.kbd.default.size,
type: String as PropType<keyof typeof config.size>,
default: () => config.default.size,
validator (value: string) {
return Object.keys(appConfig.ui.kbd.size).includes(value)
return Object.keys(config.size).includes(value)
}
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.kbd>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.kbd>>(() => defuTwMerge({}, props.ui, appConfig.ui.kbd))
setup (props) {
const { ui, attrs } = useUI('kbd', toRef(props, 'ui'), config)
const kbdClass = computed(() => {
return twMerge(twJoin(
@@ -51,13 +52,13 @@ export default defineComponent({
ui.value.font,
ui.value.background,
ui.value.ring
), attrs.class as string)
), props.class)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
kbdClass
}
}

View File

@@ -1,7 +1,13 @@
<template>
<button v-if="!to" :type="type" :disabled="disabled" v-bind="$attrs" :class="inactiveClass">
<component
:is="as"
v-if="!to"
:disabled="disabled"
v-bind="$attrs"
:class="inactiveClass"
>
<slot />
</button>
</component>
<NuxtLink
v-else
v-slot="{ route, href, target, rel, navigate, isActive, isExactActive, isExternal }"
@@ -24,7 +30,7 @@
</template>
<script lang="ts">
import { isEqual } from 'lodash-es'
import { isEqual } from 'ohash'
import { defineComponent } from 'vue'
import { NuxtLink } from '#components'
@@ -32,14 +38,18 @@ export default defineComponent({
inheritAttrs: false,
props: {
...NuxtLink.props,
type: {
as: {
type: String,
default: null
default: 'button'
},
disabled: {
type: Boolean,
default: null
},
active: {
type: Boolean,
default: false
},
exact: {
type: Boolean,
default: false
@@ -59,6 +69,10 @@ export default defineComponent({
},
setup (props) {
function resolveLinkClass (route, $route, { isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
if (props.active) {
return props.activeClass
}
if (props.exactQuery && !isEqual(route.query, $route.query)) {
return props.inactiveClass
}

View File

@@ -1,8 +1,8 @@
<template>
<div :class="wrapperClass">
<div :class="ui.wrapper">
<div class="flex items-center h-5">
<input
:id="name"
:id="inputId"
v-model="toggle"
:name="name"
:required="required"
@@ -18,7 +18,7 @@
>
</div>
<div v-if="label || $slots.label" class="ms-3 text-sm">
<label :for="name" :class="ui.label">
<label :for="inputId" :class="ui.label">
<slot name="label">{{ label }}</slot>
<span v-if="required" :class="ui.required">*</span>
</label>
@@ -30,22 +30,29 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import { uid } from '../../utils/uid'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { checkbox } from '#ui/ui.config'
import colors from '#ui-colors'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof checkbox>(appConfig.ui.strategy, appConfig.ui.checkbox, checkbox)
export default defineComponent({
inheritAttrs: false,
props: {
id: {
type: String,
// A default value is needed here to bind the label
default: () => uid()
},
value: {
type: [String, Number, Boolean, Object],
default: null
@@ -83,8 +90,8 @@ export default defineComponent({
default: false
},
color: {
type: String,
default: () => appConfig.ui.checkbox.default.color,
type: String as PropType<typeof colors[number]>,
default: () => config.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
}
@@ -93,20 +100,20 @@ export default defineComponent({
type: String,
default: ''
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.checkbox>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue', 'change'],
setup (props, { emit, attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
setup (props, { emit }) {
const { ui, attrs } = useUI('checkbox', toRef(props, 'ui'), config, toRef(props, 'class'))
const ui = computed<Partial<typeof appConfig.ui.checkbox>>(() => defuTwMerge({}, props.ui, appConfig.ui.checkbox))
const { emitFormChange, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
const { emitFormChange, color, name, inputId } = useFormGroup(props)
const toggle = computed({
get () {
@@ -122,8 +129,6 @@ export default defineComponent({
emitFormChange()
}
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const inputClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
@@ -136,11 +141,13 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
toggle,
wrapperClass,
inputId,
// eslint-disable-next-line vue/no-dupe-keys
name,
// eslint-disable-next-line vue/no-dupe-keys
inputClass,
onChange

View File

@@ -12,6 +12,7 @@ import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
import type { ObjectSchemaAsync as ValibotObjectSchema } from 'valibot'
import type { FormError, FormEvent, FormEventType, FormSubmitEvent, Form } from '../../types/form'
import { uid } from '../../utils/uid'
export default defineComponent({
props: {
@@ -40,8 +41,7 @@ export default defineComponent({
},
emits: ['submit'],
setup (props, { expose, emit }) {
const seed = Math.random().toString(36).substring(7)
const bus = useEventBus<FormEvent>(`form-${seed}`)
const bus = useEventBus<FormEvent>(`form-${uid()}`)
bus.on(async (event) => {
if (event.type !== 'submit' && props.validateOn?.includes(event.type)) {

View File

@@ -1,36 +1,54 @@
<template>
<div :class="wrapperClass" v-bind="attrs">
<label>
<div v-if="label" :class="[ui.label.wrapper, size]">
<p :class="[ui.label.base, required ? ui.label.required : '']">{{ label }}</p>
<span v-if="hint" :class="[ui.hint]">{{ hint }}</span>
</div>
<div :class="ui.wrapper" v-bind="attrs">
<div v-if="label || $slots.label" :class="[ui.label.wrapper, size]">
<label :for="inputId" :class="[ui.label.base, required ? ui.label.required : '']">
<slot v-if="$slots.label" name="label" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>{{ label }}</template>
</label>
<span v-if="hint || $slots.hint" :class="[ui.hint]">
<slot v-if="$slots.hint" name="hint" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>{{ hint }}</template>
</span>
</div>
<p v-if="description" :class="[ui.description, size]">{{ description }}</p>
<p v-if="description || $slots.description" :class="[ui.description, size]">
<slot v-if="$slots.description" name="description" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>
{{ description }}
</template>
</p>
<div :class="[label ? ui.container : '']">
<slot v-bind="{ error }" />
<div :class="[label ? ui.container : '']">
<slot v-bind="{ error }" />
<p v-if="error && typeof error !== 'boolean'" :class="[ui.error, size]">{{ error }}</p>
<p v-else-if="help" :class="[ui.help, size]">{{ help }}</p>
</div>
</label>
<p v-if="(typeof error === 'string' && error) || $slots.error" :class="[ui.error, size]">
<slot v-if="$slots.error" name="error" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>
{{ error }}
</template>
</p>
<p v-else-if="help || $slots.help" :class="[ui.help, size]">
<slot v-if="$slots.help" name="help" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>
{{ help }}
</template>
</p>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, provide, inject } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import type { FormError } from '../../types/form'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { computed, defineComponent, provide, inject, ref, toRef } from 'vue'
import type { Ref, PropType } from 'vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { FormError, InjectedFormGroupValue, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { formGroup } from '#ui/ui.config'
import { uid } from '../../utils/uid'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof formGroup>(appConfig.ui.strategy, appConfig.ui.formGroup, formGroup)
export default defineComponent({
inheritAttrs: false,
@@ -40,10 +58,10 @@ export default defineComponent({
default: null
},
size: {
type: String,
type: String as PropType<keyof typeof config.size>,
default: null,
validator (value: string) {
return Object.keys(appConfig.ui.formGroup.size).includes(value)
return Object.keys(config.size).includes(value)
}
},
label: {
@@ -70,18 +88,17 @@ export default defineComponent({
type: String,
default: null
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.formGroup>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.formGroup>>(() => defuTwMerge({}, props.ui, appConfig.ui.formGroup))
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
setup (props) {
const { ui, attrs } = useUI('formGroup', toRef(props, 'ui'), config, toRef(props, 'class'))
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
@@ -91,19 +108,21 @@ export default defineComponent({
: formErrors?.value?.find((error) => error.path === props.name)?.message
})
const size = computed(() => ui.value.size[props.size ?? appConfig.ui.input.default.size])
const size = computed(() => ui.value.size[props.size ?? config.default.size])
const inputId = ref(uid())
provide('form-group', {
provide<InjectedFormGroupValue>('form-group', {
error,
inputId,
name: computed(() => props.name),
size: computed(() => props.size)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
wrapperClass,
attrs,
inputId,
// eslint-disable-next-line vue/no-dupe-keys
size,
// eslint-disable-next-line vue/no-dupe-keys

View File

@@ -1,6 +1,7 @@
<template>
<div :class="wrapperClass">
<div :class="ui.wrapper">
<input
:id="inputId"
ref="input"
:name="name"
:value="modelValue"
@@ -31,19 +32,20 @@
</template>
<script lang="ts">
import { ref, computed, onMounted, defineComponent } from 'vue'
import { ref, computed, toRef, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { defuTwMerge } from '../../utils'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import type { NestedKeyOf, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { input } from '#ui/ui.config'
import colors from '#ui-colors'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof input>(appConfig.ui.strategy, appConfig.ui.input, input)
export default defineComponent({
components: {
@@ -59,6 +61,10 @@ export default defineComponent({
type: String,
default: 'text'
},
id: {
type: String,
default: null
},
name: {
type: String,
default: null
@@ -85,7 +91,7 @@ export default defineComponent({
},
loadingIcon: {
type: String,
default: () => appConfig.ui.input.default.loadingIcon
default: () => config.default.loadingIcon
},
leadingIcon: {
type: String,
@@ -112,26 +118,26 @@ export default defineComponent({
default: true
},
size: {
type: String,
default: () => appConfig.ui.input.default.size,
type: String as PropType<keyof typeof config.size>,
default: null,
validator (value: string) {
return Object.keys(appConfig.ui.input.size).includes(value)
return Object.keys(config.size).includes(value)
}
},
color: {
type: String,
default: () => appConfig.ui.input.default.color,
type: String as PropType<keyof typeof config.color | typeof colors[number]>,
default: () => config.default.color,
validator (value: string) {
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.input.color)].includes(value)
return [...appConfig.ui.colors, ...Object.keys(config.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.input.default.variant,
type: String as PropType<keyof typeof config.variant | NestedKeyOf<typeof config.color>>,
default: () => config.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.input.variant),
...Object.values(appConfig.ui.input.color).flatMap(value => Object.keys(value))
...Object.keys(config.variant),
...Object.values(config.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
@@ -139,21 +145,20 @@ export default defineComponent({
type: String,
default: null
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.input>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue', 'blur'],
setup (props, { emit, attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
setup (props, { emit, slots }) {
const { ui, attrs } = useUI('input', toRef(props, 'ui'), config, toRef(props, 'class'))
const ui = computed<Partial<typeof appConfig.ui.input>>(() => defuTwMerge({}, props.ui, appConfig.ui.input))
const { emitFormBlur, emitFormInput, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
const size = computed(() => formGroup?.size?.value ?? props.size)
const { emitFormBlur, emitFormInput, size, color, inputId, name } = useFormGroup(props, config)
const input = ref<HTMLInputElement | null>(null)
@@ -179,8 +184,6 @@ export default defineComponent({
}, 100)
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const inputClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
@@ -255,13 +258,15 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
// eslint-disable-next-line vue/no-dupe-keys
name,
inputId,
input,
isLeading,
isTrailing,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
inputClass,
leadingIconName,

View File

@@ -1,8 +1,8 @@
<template>
<div :class="wrapperClass">
<div :class="ui.wrapper">
<div class="flex items-center h-5">
<input
:id="`${name}-${value}`"
:id="inputId"
v-model="pick"
:name="name"
:required="required"
@@ -15,7 +15,7 @@
>
</div>
<div v-if="label || $slots.label" class="ms-3 text-sm">
<label :for="`${name}-${value}`" :class="ui.label">
<label :for="inputId" :class="ui.label">
<slot name="label">{{ label }}</slot>
<span v-if="required" :class="ui.required">*</span>
</label>
@@ -27,22 +27,29 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { radio } from '#ui/ui.config'
import colors from '#ui-colors'
import { uid } from '../../utils/uid'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof radio>(appConfig.ui.strategy, appConfig.ui.radio, radio)
export default defineComponent({
inheritAttrs: false,
props: {
id: {
type: String,
// A default value is needed here to bind the label
default: () => uid()
},
value: {
type: [String, Number, Boolean],
default: null
@@ -72,8 +79,8 @@ export default defineComponent({
default: false
},
color: {
type: String,
default: () => appConfig.ui.radio.default.color,
type: String as PropType<typeof colors[number]>,
default: () => config.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
}
@@ -82,20 +89,20 @@ export default defineComponent({
type: String,
default: null
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.radio>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue'],
setup (props, { emit, attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
setup (props, { emit }) {
const { ui, attrs } = useUI('radio', toRef(props, 'ui'), config, toRef(props, 'class'))
const ui = computed<Partial<typeof appConfig.ui.radio>>(() => defuTwMerge({}, props.ui, appConfig.ui.radio))
const { emitFormChange, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
const { emitFormChange, color, name, inputId } = useFormGroup(props)
const pick = computed({
get () {
@@ -109,8 +116,6 @@ export default defineComponent({
}
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const inputClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
@@ -122,11 +127,13 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
inputId,
attrs,
pick,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
name,
// eslint-disable-next-line vue/no-dupe-keys
inputClass
}

View File

@@ -1,6 +1,7 @@
<template>
<div :class="wrapperClass">
<input
:id="inputId"
ref="input"
v-model.number="value"
:name="name"
@@ -19,16 +20,19 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { range } from '#ui/ui.config'
import colors from '#ui-colors'
const config = mergeConfig<typeof range>(appConfig.ui.strategy, appConfig.ui.range, range)
export default defineComponent({
inheritAttrs: false,
@@ -37,6 +41,10 @@ export default defineComponent({
type: Number,
default: 0
},
id: {
type: String,
default: null
},
name: {
type: String,
default: null
@@ -58,15 +66,15 @@ export default defineComponent({
default: 1
},
size: {
type: String,
default: () => appConfig.ui.range.default.size,
type: String as PropType<keyof typeof config.size>,
default: null,
validator (value: string) {
return Object.keys(appConfig.ui.range.size).includes(value)
return Object.keys(config.size).includes(value)
}
},
color: {
type: String,
default: () => appConfig.ui.range.default.color,
type: String as PropType<typeof colors[number]>,
default: () => config.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
}
@@ -75,21 +83,20 @@ export default defineComponent({
type: String,
default: null
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.range>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue', 'change'],
setup (props, { emit, attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
setup (props, { emit }) {
const { ui, attrs } = useUI('range', toRef(props, 'ui'), config)
const ui = computed<Partial<typeof appConfig.ui.range>>(() => defuTwMerge({}, props.ui, appConfig.ui.range))
const { emitFormChange, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
const size = computed(() => formGroup?.size?.value ?? props.size)
const { emitFormChange, inputId, color, size, name } = useFormGroup(props, config)
const value = computed({
get () {
@@ -109,7 +116,7 @@ export default defineComponent({
return twMerge(twJoin(
ui.value.wrapper,
ui.value.size[size.value]
), attrs.class as string)
), props.class)
})
const inputClass = computed(() => {
@@ -161,9 +168,12 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
// eslint-disable-next-line vue/no-dupe-keys
name,
inputId,
value,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys

View File

@@ -1,6 +1,7 @@
<template>
<div :class="wrapperClass">
<div :class="ui.wrapper">
<select
:id="inputId"
:name="name"
:value="modelValue"
:required="required"
@@ -53,19 +54,20 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType, ComputedRef } from 'vue'
import { get, omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { defuTwMerge } from '../../utils'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig, get } from '../../utils'
import type { NestedKeyOf, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { select } from '#ui/ui.config'
import colors from '#ui-colors'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof select>(appConfig.ui.strategy, appConfig.ui.select, select)
export default defineComponent({
components: {
@@ -77,6 +79,10 @@ export default defineComponent({
type: [String, Number, Object],
default: ''
},
id: {
type: String,
default: null
},
name: {
type: String,
default: null
@@ -99,7 +105,7 @@ export default defineComponent({
},
loadingIcon: {
type: String,
default: () => appConfig.ui.input.default.loadingIcon
default: () => config.default.loadingIcon
},
leadingIcon: {
type: String,
@@ -107,7 +113,7 @@ export default defineComponent({
},
trailingIcon: {
type: String,
default: () => appConfig.ui.select.default.trailingIcon
default: () => config.default.trailingIcon
},
trailing: {
type: Boolean,
@@ -130,26 +136,26 @@ export default defineComponent({
default: () => []
},
size: {
type: String,
default: () => appConfig.ui.select.default.size,
type: String as PropType<keyof typeof config.size>,
default: null,
validator (value: string) {
return Object.keys(appConfig.ui.select.size).includes(value)
return Object.keys(config.size).includes(value)
}
},
color: {
type: String,
default: () => appConfig.ui.select.default.color,
type: String as PropType<keyof typeof config.color | typeof colors[number]>,
default: () => config.default.color,
validator (value: string) {
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.select.color)].includes(value)
return [...appConfig.ui.colors, ...Object.keys(config.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.select.default.variant,
type: String as PropType<keyof typeof config.variant | NestedKeyOf<typeof config.color>>,
default: () => config.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.select.variant),
...Object.values(appConfig.ui.select.color).flatMap(value => Object.keys(value))
...Object.keys(config.variant),
...Object.values(config.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
@@ -165,22 +171,20 @@ export default defineComponent({
type: String,
default: null
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.select>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue', 'change'],
setup (props, { emit, attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defuTwMerge({}, props.ui, appConfig.ui.select))
const { emitFormChange, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
const size = computed(() => formGroup?.size?.value ?? props.size)
setup (props, { emit, slots }) {
const { ui, attrs } = useUI('select', toRef(props, 'ui'), config, toRef(props, 'class'))
const { emitFormChange, inputId, color, size, name } = useFormGroup(props, config)
const onInput = (event: InputEvent) => {
emit('update:modelValue', (event.target as HTMLInputElement).value)
@@ -243,8 +247,6 @@ export default defineComponent({
return foundOption[props.valueAttribute]
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const selectClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
@@ -318,14 +320,16 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
// eslint-disable-next-line vue/no-dupe-keys
name,
inputId,
normalizedOptionsWithPlaceholder,
normalizedValue,
isLeading,
isTrailing,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
selectClass,
leadingIconName,

View File

@@ -8,7 +8,7 @@
:multiple="multiple"
:disabled="disabled || loading"
as="div"
:class="wrapperClass"
:class="ui.wrapper"
@update:model-value="onUpdate"
>
<input
@@ -28,7 +28,7 @@
class="inline-flex w-full"
>
<slot :open="open" :disabled="disabled" :loading="loading">
<button :class="selectClass" :disabled="disabled || loading" type="button" v-bind="attrs">
<button :id="inputId" :class="selectClass" :disabled="disabled || loading" type="button" v-bind="attrs">
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
@@ -37,7 +37,7 @@
<slot name="label">
<span v-if="multiple && Array.isArray(modelValue) && modelValue.length" class="block truncate">{{ modelValue.length }} selected</span>
<span v-else-if="!multiple && modelValue" class="block truncate">{{ typeof modelValue === 'string' ? modelValue : modelValue[optionAttribute] }}</span>
<span v-else-if="!multiple && modelValue" class="block truncate">{{ ['string', 'number'].includes(typeof modelValue) ? modelValue : modelValue[optionAttribute] }}</span>
<span v-else class="block truncate" :class="uiMenu.placeholder">{{ placeholder || '&nbsp;' }}</span>
</slot>
@@ -85,7 +85,7 @@
/>
<span v-else-if="option.chip" :class="uiMenu.option.chip.base" :style="{ background: `#${option.chip}` }" />
<span class="truncate">{{ typeof option === 'string' ? option : option[optionAttribute] }}</span>
<span class="truncate">{{ ['string', 'number'].includes(typeof option) ? option : option[optionAttribute] }}</span>
</slot>
</div>
@@ -116,7 +116,7 @@
</template>
<script lang="ts">
import { ref, computed, watch, defineComponent } from 'vue'
import { ref, computed, toRef, watch, defineComponent } from 'vue'
import type { PropType, ComponentPublicInstance } from 'vue'
import {
Combobox as HCombobox,
@@ -131,20 +131,22 @@ import {
} from '@headlessui/vue'
import { computedAsync, useDebounceFn } from '@vueuse/core'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import { defuTwMerge } from '../../utils'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper'
import { useFormGroup } from '../../composables/useFormGroup'
import type { PopperOptions } from '../../types/popper'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import type { PopperOptions, NestedKeyOf, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { select, selectMenu } from '#ui/ui.config'
import colors from '#ui-colors'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof select>(appConfig.ui.strategy, appConfig.ui.select, select)
const configMenu = mergeConfig<typeof selectMenu>(appConfig.ui.strategy, appConfig.ui.selectMenu, selectMenu)
export default defineComponent({
components: {
@@ -174,6 +176,10 @@ export default defineComponent({
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[] | string[]>,
default: () => []
},
id: {
type: String,
default: null
},
name: {
type: String,
default: null
@@ -188,7 +194,7 @@ export default defineComponent({
},
loadingIcon: {
type: String,
default: () => appConfig.ui.input.default.loadingIcon
default: () => config.default.loadingIcon
},
leadingIcon: {
type: String,
@@ -196,7 +202,7 @@ export default defineComponent({
},
trailingIcon: {
type: String,
default: () => appConfig.ui.select.default.trailingIcon
default: () => config.default.trailingIcon
},
trailing: {
type: Boolean,
@@ -212,7 +218,7 @@ export default defineComponent({
},
selectedIcon: {
type: String,
default: () => appConfig.ui.selectMenu.default.selectedIcon
default: () => configMenu.default.selectedIcon
},
disabled: {
type: Boolean,
@@ -247,26 +253,26 @@ export default defineComponent({
default: true
},
size: {
type: String,
default: () => appConfig.ui.select.default.size,
type: String as PropType<keyof typeof config.size>,
default: null,
validator (value: string) {
return Object.keys(appConfig.ui.select.size).includes(value)
return Object.keys(config.size).includes(value)
}
},
color: {
type: String,
default: () => appConfig.ui.select.default.color,
type: String as PropType<keyof typeof config.color | typeof colors[number]>,
default: () => config.default.color,
validator (value: string) {
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.select.color)].includes(value)
return [...appConfig.ui.colors, ...Object.keys(config.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.select.default.variant,
type: String as PropType<keyof typeof config.variant | NestedKeyOf<typeof config.color>>,
default: () => config.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.select.variant),
...Object.values(appConfig.ui.select.color).flatMap(value => Object.keys(value))
...Object.keys(config.variant),
...Object.values(config.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
@@ -290,35 +296,33 @@ export default defineComponent({
type: String,
default: null
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.select>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
},
uiMenu: {
type: Object as PropType<Partial<typeof appConfig.ui.selectMenu>>,
default: () => ({})
type: Object as PropType<Partial<typeof configMenu & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue', 'open', 'close', 'change'],
setup (props, { emit, attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
setup (props, { emit, slots }) {
const { ui, attrs } = useUI('select', toRef(props, 'ui'), config, toRef(props, 'class'))
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defuTwMerge({}, props.ui, appConfig.ui.select))
const uiMenu = computed<Partial<typeof appConfig.ui.selectMenu>>(() => defuTwMerge({}, props.uiMenu, appConfig.ui.selectMenu))
const { ui: uiMenu } = useUI('selectMenu', toRef(props, 'uiMenu'), configMenu)
const popper = computed<PopperOptions>(() => defu({}, props.popper, uiMenu.value.popper as PopperOptions))
const [trigger, container] = usePopper(popper.value)
const { emitFormBlur, emitFormChange, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
const size = computed(() => formGroup?.size?.value ?? props.size)
const { emitFormBlur, emitFormChange, inputId, color, size, name } = useFormGroup(props, config)
const query = ref('')
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const selectClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
@@ -407,7 +411,7 @@ export default defineComponent({
return (props.options as any[]).filter((option: any) => {
return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => {
return typeof option === 'string' ? option.search(new RegExp(query.value, 'i')) !== -1 : (option[searchAttribute] && option[searchAttribute].search(new RegExp(query.value, 'i')) !== -1)
return ['string', 'number'].includes(typeof option) ? option.toString().search(new RegExp(query.value, 'i')) !== -1 : (option[searchAttribute] && option[searchAttribute].search(new RegExp(query.value, 'i')) !== -1)
})
})
})
@@ -437,14 +441,18 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
// eslint-disable-next-line vue/no-dupe-keys
uiMenu,
attrs,
// eslint-disable-next-line vue/no-dupe-keys
name,
inputId,
trigger,
container,
isLeading,
isTrailing,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
selectClass,
leadingIconName,

View File

@@ -1,6 +1,7 @@
<template>
<div :class="wrapperClass">
<div :class="ui.wrapper">
<textarea
:id="inputId"
ref="textarea"
:value="modelValue"
:name="name"
@@ -18,18 +19,19 @@
</template>
<script lang="ts">
import { ref, computed, watch, onMounted, nextTick, defineComponent } from 'vue'
import { ref, computed, toRef, watch, onMounted, nextTick, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import type { NestedKeyOf, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { textarea } from '#ui/ui.config'
import colors from '#ui-colors'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof textarea>(appConfig.ui.strategy, appConfig.ui.textarea, textarea)
export default defineComponent({
inheritAttrs: false,
@@ -38,6 +40,10 @@ export default defineComponent({
type: [String, Number],
default: ''
},
id: {
type: String,
default: null
},
name: {
type: String,
default: null
@@ -75,26 +81,26 @@ export default defineComponent({
default: true
},
size: {
type: String,
default: () => appConfig.ui.textarea.default.size,
type: String as PropType<keyof typeof config.size>,
default: null,
validator (value: string) {
return Object.keys(appConfig.ui.textarea.size).includes(value)
return Object.keys(config.size).includes(value)
}
},
color: {
type: String,
default: () => appConfig.ui.textarea.default.color,
type: String as PropType<keyof typeof config.color | typeof colors[number]>,
default: () => config.default.color,
validator (value: string) {
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.textarea.color)].includes(value)
return [...appConfig.ui.colors, ...Object.keys(config.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.textarea.default.variant,
type: String as PropType<keyof typeof config.variant | NestedKeyOf<typeof config.color>>,
default: () => config.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.textarea.variant),
...Object.values(appConfig.ui.textarea.color).flatMap(value => Object.keys(value))
...Object.keys(config.variant),
...Object.values(config.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
@@ -102,24 +108,23 @@ export default defineComponent({
type: String,
default: null
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.textarea>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue', 'blur'],
setup (props, { emit, attrs }) {
setup (props, { emit }) {
const { ui, attrs } = useUI('textarea', toRef(props, 'ui'), config, toRef(props, 'class'))
const { emitFormBlur, emitFormInput, inputId, color, size, name } = useFormGroup(props, config)
const textarea = ref<HTMLTextAreaElement | null>(null)
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.textarea>>(() => defuTwMerge({}, props.ui, appConfig.ui.textarea))
const { emitFormBlur, emitFormInput, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
const size = computed(() => formGroup?.size?.value ?? props.size)
const autoFocus = () => {
if (props.autofocus) {
textarea.value?.focus()
@@ -177,8 +182,6 @@ export default defineComponent({
}, 100)
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const textareaClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
@@ -194,11 +197,13 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
// eslint-disable-next-line vue/no-dupe-keys
name,
inputId,
textarea,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
textareaClass,
onInput,

View File

@@ -1,5 +1,6 @@
<template>
<HSwitch
:id="inputId"
v-model="active"
:name="name"
:disabled="disabled"
@@ -18,20 +19,21 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { Switch as HSwitch } from '@headlessui/vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { defuTwMerge } from '../../utils'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { toggle } from '#ui/ui.config'
import colors from '#ui-colors'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof toggle>(appConfig.ui.strategy, appConfig.ui.toggle, toggle)
export default defineComponent({
components: {
@@ -40,6 +42,10 @@ export default defineComponent({
},
inheritAttrs: false,
props: {
id: {
type: String,
default: null
},
name: {
type: String,
default: null
@@ -54,33 +60,33 @@ export default defineComponent({
},
onIcon: {
type: String,
default: () => appConfig.ui.toggle.default.onIcon
default: () => config.default.onIcon
},
offIcon: {
type: String,
default: () => appConfig.ui.toggle.default.offIcon
default: () => config.default.offIcon
},
color: {
type: String,
default: () => appConfig.ui.toggle.default.color,
type: String as PropType<typeof colors[number]>,
default: () => config.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
}
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.toggle>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue'],
setup (props, { emit, attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
setup (props, { emit }) {
const { ui, attrs } = useUI('toggle', toRef(props, 'ui'), config)
const ui = computed<Partial<typeof appConfig.ui.toggle>>(() => defuTwMerge({}, props.ui, appConfig.ui.toggle))
const { emitFormChange, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
const { emitFormChange, color, inputId, name } = useFormGroup(props)
const active = computed({
get () {
@@ -98,7 +104,7 @@ export default defineComponent({
ui.value.rounded,
ui.value.ring.replaceAll('{color}', color.value),
(active.value ? ui.value.active : ui.value.inactive).replaceAll('{color}', color.value)
), attrs.class as string)
), props.class)
})
const onIconClass = computed(() => {
@@ -114,9 +120,12 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
// eslint-disable-next-line vue/no-dupe-keys
name,
inputId,
active,
switchClass,
onIconClass,

View File

@@ -17,17 +17,17 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { card } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof card>(appConfig.ui.strategy, appConfig.ui.card, card)
export default defineComponent({
inheritAttrs: false,
@@ -36,16 +36,17 @@ export default defineComponent({
type: String,
default: 'div'
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.card>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.card>>(() => defuTwMerge({}, props.ui, appConfig.ui.card))
setup (props) {
const { ui, attrs } = useUI('card', toRef(props, 'ui'), config)
const cardClass = computed(() => {
return twMerge(twJoin(
@@ -55,13 +56,13 @@ export default defineComponent({
ui.value.ring,
ui.value.shadow,
ui.value.background
), attrs.class as string)
), props.class)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
cardClass
}
}

View File

@@ -5,17 +5,17 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { container } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof container>(appConfig.ui.strategy, appConfig.ui.container, container)
export default defineComponent({
inheritAttrs: false,
@@ -24,29 +24,30 @@ export default defineComponent({
type: String,
default: 'div'
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.container>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.container>>(() => defuTwMerge({}, props.ui, appConfig.ui.container))
setup (props) {
const { ui, attrs } = useUI('container', toRef(props, 'ui'), config)
const containerClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
ui.value.padding,
ui.value.constrained
), attrs.class as string)
), props.class)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
containerClass
}
}

View File

@@ -3,44 +3,45 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { skeleton } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof skeleton>(appConfig.ui.strategy, appConfig.ui.skeleton, skeleton)
export default defineComponent({
inheritAttrs: false,
props: {
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.skeleton>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.skeleton>>(() => defuTwMerge({}, props.ui, appConfig.ui.skeleton))
setup (props) {
const { ui, attrs } = useUI('skeleton', toRef(props, 'ui'), config)
const skeletonClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
ui.value.background,
ui.value.rounded
), attrs.class as string)
), props.class)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
skeletonClass
}
}

View File

@@ -4,7 +4,7 @@
:model-value="modelValue"
:multiple="multiple"
:nullable="nullable"
:class="wrapperClass"
:class="ui.wrapper"
v-bind="attrs"
as="div"
@update:model-value="onSelect"
@@ -16,17 +16,12 @@
:value="query"
:class="[ui.input.base, ui.input.size, ui.input.height, ui.input.padding, icon && ui.input.icon.padding]"
:placeholder="placeholder"
:aria-label="placeholder"
autocomplete="off"
@change="query = $event.target.value"
/>
<UButton
v-if="closeButton"
v-bind="{ ...ui.default.closeButton, ...closeButton }"
:class="ui.input.closeButton"
aria-label="Close"
@click="onClear"
/>
<UButton v-if="closeButton" aria-label="Close" v-bind="{ ...ui.default.closeButton, ...closeButton }" :class="ui.input.closeButton" @click="onClear" />
</div>
<HComboboxOptions
@@ -67,27 +62,25 @@
</template>
<script lang="ts">
import { ref, computed, watch, onMounted, defineComponent } from 'vue'
import { ref, computed, watch, toRef, onMounted, defineComponent } from 'vue'
import { Combobox as HCombobox, ComboboxInput as HComboboxInput, ComboboxOptions as HComboboxOptions } from '@headlessui/vue'
import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import { useFuse } from '@vueuse/integrations/useFuse'
import { twMerge, twJoin } from 'tailwind-merge'
import { groupBy, map, omit } from 'lodash-es'
import { defu } from 'defu'
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import type { Group, Command } from '../../types/command-palette'
import { twJoin } from 'tailwind-merge'
import { defu } from 'defu'
import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import type { Button } from '../../types/button'
import CommandPaletteGroup from './CommandPaletteGroup.vue'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Group, Command, Button, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { commandPalette } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof commandPalette>(appConfig.ui.strategy, appConfig.ui.commandPalette, commandPalette)
export default defineComponent({
components: {
@@ -130,23 +123,23 @@ export default defineComponent({
},
icon: {
type: String,
default: () => appConfig.ui.commandPalette.default.icon
default: () => config.default.icon
},
loadingIcon: {
type: String,
default: () => appConfig.ui.commandPalette.default.loadingIcon
default: () => config.default.loadingIcon
},
selectedIcon: {
type: String,
default: () => appConfig.ui.commandPalette.default.selectedIcon
default: () => config.default.selectedIcon
},
closeButton: {
type: Object as PropType<Button>,
default: () => appConfig.ui.commandPalette.default.closeButton
default: () => config.default.closeButton as Button
},
emptyState: {
type: Object as PropType<{ icon: string, label: string, queryLabel: string }>,
default: () => appConfig.ui.commandPalette.default.emptyState
default: () => config.default.emptyState
},
placeholder: {
type: String,
@@ -176,17 +169,18 @@ export default defineComponent({
type: Object as PropType<UseFuseOptions<Command>>,
default: () => ({})
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.commandPalette>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue', 'close'],
setup (props, { emit, attrs, expose }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.commandPalette>>(() => defuTwMerge({}, props.ui, appConfig.ui.commandPalette))
setup (props, { emit, expose }) {
const { ui, attrs } = useUI('commandPalette', toRef(props, 'ui'), config, toRef(props, 'class'))
const query = ref('')
const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>()
@@ -219,32 +213,50 @@ export default defineComponent({
matchAllWhenSearchEmpty: true
}))
const commands = computed(() => props.groups.filter(group => !group.search).reduce((acc, group) => {
return acc.concat(group.commands.map(command => ({ ...command, group: group.key })))
}, [] as Command[]))
const commands = computed(() => {
const commands: Command[] = []
for (const group of props.groups) {
if (!group.search) {
commands.push(...group.commands.map(command => ({ ...command, group: group.key })))
}
}
return commands
})
const searchResults = ref<{ [key: string]: any }>({})
const { results } = useFuse(query, commands, options)
const groups = computed(() => ([
...map(groupBy(results.value, command => command.item.group), (results, key) => {
const commands = results.map((result) => {
const groups = computed(() => {
const groups: Group[] = []
const groupedCommands: Record<string, typeof results['value']> = {}
for (const command of results.value) {
groupedCommands[command.item.group] ||= []
groupedCommands[command.item.group].push(command)
}
for (const key in groupedCommands) {
const group = props.groups.find(group => group.key === key)
const commands = groupedCommands[key].slice(0, options.value.resultLimit).map((result) => {
const { item, ...data } = result
return {
...item,
...data
}
} as Command
})
return {
...props.groups.find(group => group.key === key),
commands: commands.slice(0, options.value.resultLimit)
} as Group
}),
...props.groups.filter(group => !!group.search).map(group => ({ ...group, commands: (searchResults.value[group.key] || []).slice(0, options.value.resultLimit) })).filter(group => group.commands.length)
]))
groups.push({ ...group, commands })
}
for (const group of props.groups) {
if (group.search && searchResults.value[group.key]?.length) {
groups.push({ ...group, commands: (searchResults.value[group.key] || []).slice(0, options.value.resultLimit) })
}
}
return groups
})
const debouncedSearch = useDebounceFn(async () => {
const searchableGroups = props.groups.filter(group => !!group.search)
@@ -271,8 +283,6 @@ export default defineComponent({
}, 0)
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const iconName = computed(() => {
if ((props.loading || isLoading.value) && props.loadingIcon) {
return props.loadingIcon
@@ -330,14 +340,13 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
// eslint-disable-next-line vue/no-dupe-keys
groups,
comboboxInput,
query,
wrapperClass,
iconName,
iconClass,
// eslint-disable-next-line vue/no-dupe-keys

View File

@@ -76,12 +76,8 @@ import { ComboboxOption as HComboboxOption } from '@headlessui/vue'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue'
import type { Group } from '../../types/command-palette'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
import type { Group } from '../../types'
import { commandPalette } from '#ui/ui.config'
export default defineComponent({
components: {
@@ -112,8 +108,8 @@ export default defineComponent({
required: true
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.commandPalette>>,
default: () => ({})
type: Object as PropType<typeof commandPalette>,
required: true
}
},
setup (props) {

View File

@@ -1,5 +1,5 @@
<template>
<div :class="wrapperClass" v-bind="attrs">
<div :class="ui.wrapper" v-bind="attrs">
<slot name="prev" :on-click="onClickPrev">
<UButton
v-if="prevButton"
@@ -8,6 +8,7 @@
:class="[ui.base, ui.rounded]"
v-bind="{ ...ui.default.prevButton, ...prevButton }"
:ui="{ rounded: '' }"
aria-label="Prev"
@click="onClickPrev"
/>
</slot>
@@ -31,6 +32,7 @@
:class="[ui.base, ui.rounded]"
v-bind="{ ...ui.default.nextButton, ...nextButton }"
:ui="{ rounded: '' }"
aria-label="Next"
@click="onClickNext"
/>
</slot>
@@ -38,19 +40,19 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import UButton from '../elements/Button.vue'
import { defuTwMerge } from '../../utils'
import type { Button } from '../../types/button'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Button, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { pagination, button } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof pagination>(appConfig.ui.strategy, appConfig.ui.pagination, pagination)
const buttonConfig = mergeConfig<typeof button>(appConfig.ui.strategy, appConfig.ui.button, button)
export default defineComponent({
components: {
@@ -74,47 +76,48 @@ export default defineComponent({
type: Number,
default: 7,
validate (value) {
return value >= 7 && value < Number.MAX_VALUE
return value >= 5 && value < Number.MAX_VALUE
}
},
size: {
type: String,
default: () => appConfig.ui.pagination.default.size,
type: String as PropType<keyof typeof buttonConfig.size>,
default: () => config.default.size,
validator (value: string) {
return Object.keys(appConfig.ui.button.size).includes(value)
return Object.keys(buttonConfig.size).includes(value)
}
},
activeButton: {
type: Object as PropType<Button>,
default: () => appConfig.ui.pagination.default.activeButton
default: () => config.default.activeButton as Button
},
inactiveButton: {
type: Object as PropType<Button>,
default: () => appConfig.ui.pagination.default.inactiveButton
default: () => config.default.inactiveButton as Button
},
prevButton: {
type: Object as PropType<Button>,
default: () => appConfig.ui.pagination.default.prevButton
default: () => config.default.prevButton as Button
},
nextButton: {
type: Object as PropType<Button>,
default: () => appConfig.ui.pagination.default.nextButton
default: () => config.default.nextButton as Button
},
divider: {
type: String,
default: '…'
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.pagination>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue'],
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.pagination>>(() => defuTwMerge({}, props.ui, appConfig.ui.pagination))
setup (props, { emit }) {
const { ui, attrs } = useUI('pagination', toRef(props, 'ui'), config, toRef(props, 'class'))
const currentPage = computed({
get () {
@@ -128,60 +131,70 @@ export default defineComponent({
const pages = computed(() => Array.from({ length: Math.ceil(props.total / props.pageCount) }, (_, i) => i + 1))
const displayedPages = computed(() => {
if (!props.max || pages.value.length <= 5) {
return pages.value
} else {
const current = currentPage.value
const max = pages.value.length
const r = Math.floor((Math.min(props.max, max) - 5) / 2)
const r1 = current - r
const r2 = current + r
const beforeWrapped = r1 - 1 > 1
const afterWrapped = r2 + 1 < max
const items: Array<number | string> = [1]
const totalPages = pages.value.length
const current = currentPage.value
const maxDisplayedPages = Math.max(props.max, 5)
if (beforeWrapped) items.push(props.divider)
const r = Math.floor((Math.min(maxDisplayedPages, totalPages) - 5) / 2)
const r1 = current - r
const r2 = current + r
if (!afterWrapped) {
const addedItems = (current + r + 2) - max
for (let i = current - r - addedItems; i <= current - r - 1; i++) {
items.push(i)
}
}
const beforeWrapped = r1 - 1 > 1
const afterWrapped = r2 + 1 < totalPages
for (let i = r1 > 2 ? (r1) : 2; i <= Math.min(max, r2); i++) {
const items: Array<number | string> = []
if (totalPages <= maxDisplayedPages) {
for (let i = 1; i <= totalPages; i++) {
items.push(i)
}
if (!beforeWrapped) {
const addedItems = 1 - (current - r - 2)
for (let i = current + r + 1; i <= current + r + addedItems; i++) {
items.push(i)
}
}
if (afterWrapped) items.push(props.divider)
if (r2 < max) items.push(max)
// Replace divider by number on start edge case [1, '…', 3, ...]
if (items.length >= 3 && items[1] === props.divider && items[2] === 3) {
items[1] = 2
}
// Replace divider by number on end edge case [..., 48, '…', 50]
if (items.length >= 3 && items[items.length - 2] === props.divider && items[items.length - 1] === items.length) {
items[items.length - 2] = items.length - 1
}
return items
}
items.push(1)
if (beforeWrapped) items.push(props.divider)
if (!afterWrapped) {
const addedItems = (current + r + 2) - totalPages
for (let i = current - r - addedItems; i <= current - r - 1; i++) {
items.push(i)
}
}
for (let i = Math.max(2, r1); i <= Math.min(totalPages, r2); i++) {
items.push(i)
}
if (!beforeWrapped) {
const addedItems = 1 - (current - r - 2)
for (let i = current + r + 1; i <= current + r + addedItems; i++) {
items.push(i)
}
}
if (afterWrapped) items.push(props.divider)
if (r2 < totalPages) {
items.push(totalPages)
}
// Replace divider by number on start edge case [1, '…', 3, ...]
if (items.length >= 3 && items[1] === props.divider && items[2] === 3) {
items[1] = 2
}
// Replace divider by number on end edge case [..., 48, '…', 50]
if (items.length >= 3 && items[items.length - 2] === props.divider && items[items.length - 1] === items.length) {
items[items.length - 2] = items.length - 1
}
return items
})
const canGoPrev = computed(() => currentPage.value > 1)
const canGoNext = computed(() => currentPage.value < pages.value.length)
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
function onClickPage (page: number | string) {
if (typeof page === 'string') {
return
@@ -207,15 +220,14 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
currentPage,
pages,
displayedPages,
canGoPrev,
canGoNext,
wrapperClass,
onClickPrev,
onClickNext,
onClickPage

View File

@@ -3,7 +3,7 @@
:vertical="orientation === 'vertical'"
:selected-index="selectedIndex"
as="div"
:class="wrapperClass"
:class="ui.wrapper"
v-bind="attrs"
@change="onChange"
>
@@ -38,6 +38,7 @@
:key="index"
v-slot="{ selected }"
:class="ui.base"
tabindex="-1"
>
<slot :name="item.slot || 'item'" :item="item" :index="index" :selected="selected">
{{ item.content }}
@@ -48,20 +49,18 @@
</template>
<script lang="ts">
import { ref, computed, watch, onMounted, defineComponent } from 'vue'
import { toRef, ref, watch, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { TabGroup as HTabGroup, TabList as HTabList, Tab as HTab, TabPanels as HTabPanels, TabPanel as HTabPanel } from '@headlessui/vue'
import { useResizeObserver } from '@vueuse/core'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import type { TabItem } from '../../types/tabs'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { TabItem, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { tabs } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof tabs>(appConfig.ui.strategy, appConfig.ui.tabs, tabs)
export default defineComponent({
components: {
@@ -90,17 +89,18 @@ export default defineComponent({
type: Array as PropType<TabItem[]>,
default: () => []
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.tabs>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue', 'change'],
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.tabs>>(() => defuTwMerge({}, props.ui, appConfig.ui.tabs))
setup (props, { emit }) {
const { ui, attrs } = useUI('tabs', toRef(props, 'ui'), config, toRef(props, 'class'))
const listRef = ref<HTMLElement>()
const itemRefs = ref<HTMLElement[]>([])
@@ -108,8 +108,6 @@ export default defineComponent({
const selectedIndex = ref(props.modelValue || props.defaultIndex)
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
// Methods
function calcMarkerSize (index: number) {
@@ -149,14 +147,13 @@ export default defineComponent({
onMounted(() => calcMarkerSize(selectedIndex.value))
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
listRef,
itemRefs,
markerRef,
selectedIndex,
wrapperClass,
onChange
}
}

View File

@@ -1,5 +1,5 @@
<template>
<nav :class="wrapperClass" v-bind="attrs">
<nav :class="ui.wrapper" v-bind="attrs">
<ULink
v-for="(link, index) of links"
v-slot="{ isActive }"
@@ -38,21 +38,19 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import ULink from '../elements/Link.vue'
import { defuTwMerge } from '../../utils'
import type { VerticalNavigationLink } from '../../types/vertical-navigation'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig, omit } from '../../utils'
import type { VerticalNavigationLink, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { verticalNavigation } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof verticalNavigation>(appConfig.ui.strategy, appConfig.ui.verticalNavigation, verticalNavigation)
export default defineComponent({
components: {
@@ -66,24 +64,22 @@ export default defineComponent({
type: Array as PropType<VerticalNavigationLink[]>,
default: () => []
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.verticalNavigation>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.verticalNavigation>>(() => defuTwMerge({}, props.ui, appConfig.ui.verticalNavigation))
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
setup (props) {
const { ui, attrs } = useUI('verticalNavigation', toRef(props, 'ui'), config, toRef(props, 'class'))
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
wrapperClass,
attrs,
omit
}
}

View File

@@ -14,17 +14,16 @@ import type { PropType, Ref } from 'vue'
import { defu } from 'defu'
import { onClickOutside } from '@vueuse/core'
import type { VirtualElement } from '@popperjs/core'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper'
import { defuTwMerge } from '../../utils'
import type { PopperOptions } from '../../types/popper'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import type { PopperOptions, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { contextMenu } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof contextMenu>(appConfig.ui.strategy, appConfig.ui.contextMenu, contextMenu)
export default defineComponent({
inheritAttrs: false,
@@ -41,17 +40,18 @@ export default defineComponent({
type: Object as PropType<PopperOptions>,
default: () => ({})
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.contextMenu>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue', 'close'],
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.contextMenu>>(() => defuTwMerge({}, props.ui, appConfig.ui.contextMenu))
setup (props, { emit }) {
const { ui, attrs } = useUI('contextMenu', toRef(props, 'ui'), config)
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
@@ -72,7 +72,7 @@ export default defineComponent({
return twMerge(twJoin(
ui.value.container,
ui.value.width
), attrs.class as string)
), props.class)
})
onClickOutside(container, () => {
@@ -80,9 +80,9 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
isOpen,
wrapperClass,
container

View File

@@ -1,6 +1,6 @@
<template>
<TransitionRoot :appear="appear" :show="isOpen" as="template">
<HDialog :class="wrapperClass" v-bind="attrs" @close="(e) => !preventClose && close(e)">
<HDialog :class="ui.wrapper" v-bind="attrs" @close="(e) => !preventClose && close(e)">
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
<div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild>
@@ -30,18 +30,17 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import { Dialog as HDialog, DialogPanel as HDialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { modal } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof modal>(appConfig.ui.strategy, appConfig.ui.modal, modal)
export default defineComponent({
components: {
@@ -76,17 +75,18 @@ export default defineComponent({
type: Boolean,
default: false
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.modal>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue', 'close'],
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.modal>>(() => defuTwMerge({}, props.ui, appConfig.ui.modal))
setup (props, { emit }) {
const { ui, attrs } = useUI('modal', toRef(props, 'ui'), config, toRef(props, 'class'))
const isOpen = computed({
get () {
@@ -97,8 +97,6 @@ export default defineComponent({
}
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const transitionClass = computed(() => {
if (!props.transition) {
return {}
@@ -116,11 +114,10 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
isOpen,
wrapperClass,
transitionClass,
close
}

View File

@@ -28,7 +28,7 @@
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
</div>
<UButton v-if="closeButton" v-bind="{ ...ui.default.closeButton, ...closeButton }" @click.stop="onClose" />
<UButton v-if="closeButton" aria-label="Close" v-bind="{ ...ui.default.closeButton, ...closeButton }" @click.stop="onClose" />
</div>
</div>
</div>
@@ -39,24 +39,21 @@
</template>
<script lang="ts">
import { ref, computed, onMounted, onUnmounted, watchEffect, defineComponent } from 'vue'
import { ref, computed, toRef, onMounted, onUnmounted, watchEffect, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue'
import { useUI } from '../../composables/useUI'
import { useTimer } from '../../composables/useTimer'
import type { NotificationAction } from '../../types/notification'
import type { Avatar } from '../../types/avatar'
import type { Button } from '../../types/button'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import type { Avatar, Button, NotificationColor, NotificationAction, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { notification } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof notification>(appConfig.ui.strategy, appConfig.ui.notification, notification)
export default defineComponent({
components: {
@@ -80,7 +77,7 @@ export default defineComponent({
},
icon: {
type: String,
default: () => appConfig.ui.notification.default.icon
default: () => config.default.icon
},
avatar: {
type: Object as PropType<Avatar>,
@@ -88,7 +85,7 @@ export default defineComponent({
},
closeButton: {
type: Object as PropType<Button>,
default: () => appConfig.ui.notification.default.closeButton
default: () => config.default.closeButton as Button
},
timeout: {
type: Number,
@@ -103,23 +100,24 @@ export default defineComponent({
default: null
},
color: {
type: String,
default: () => appConfig.ui.notification.default.color,
type: String as PropType<NotificationColor>,
default: () => config.default.color,
validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value)
}
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.notification>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['close'],
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.notification>>(() => defuTwMerge({}, props.ui, appConfig.ui.notification))
setup (props, { emit }) {
const { ui, attrs } = useUI('notification', toRef(props, 'ui'), config)
let timer: any = null
const remaining = ref(props.timeout)
@@ -130,7 +128,7 @@ export default defineComponent({
ui.value.background,
ui.value.rounded,
ui.value.shadow
), attrs.class as string)
), props.class)
})
const progressClass = computed(() => {
@@ -210,9 +208,9 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
wrapperClass,
progressClass,
progressStyle,

View File

@@ -18,20 +18,20 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UNotification from './Notification.vue'
import { useUI } from '../../composables/useUI'
import { useToast } from '../../composables/useToast'
import { defuTwMerge } from '../../utils'
import type { Notification } from '../../types/notification'
import { useState, useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import type { Notification, Strategy } from '../../types'
import { useState } from '#imports'
// @ts-expect-error
import appConfig from '#build/app.config'
import { notifications } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof notifications>(appConfig.ui.strategy, appConfig.ui.notifications, notifications)
export default defineComponent({
components: {
@@ -39,16 +39,17 @@ export default defineComponent({
},
inheritAttrs: false,
props: {
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.notifications>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.notifications>>(() => defuTwMerge({}, props.ui, appConfig.ui.notifications))
setup (props) {
const { ui, attrs } = useUI('notifications', toRef(props, 'ui'), config)
const toast = useToast()
const notifications = useState<Notification[]>('notifications', () => [])
@@ -58,13 +59,13 @@ export default defineComponent({
ui.value.wrapper,
ui.value.position,
ui.value.width
), attrs.class as string)
), props.class)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
toast,
notifications,
wrapperClass

View File

@@ -1,5 +1,5 @@
<template>
<HPopover ref="popover" v-slot="{ open, close }" :class="wrapperClass" v-bind="attrs" @mouseleave="onMouseLeave">
<HPopover ref="popover" v-slot="{ open, close }" :class="ui.wrapper" v-bind="attrs" @mouseleave="onMouseLeave">
<HPopoverButton
ref="trigger"
as="div"
@@ -26,21 +26,19 @@
</template>
<script lang="ts">
import { computed, ref, onMounted, defineComponent } from 'vue'
import { computed, ref, toRef, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import { Popover as HPopover, PopoverButton as HPopoverButton, PopoverPanel as HPopoverPanel } from '@headlessui/vue'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper'
import { defuTwMerge } from '../../utils'
import type { PopperOptions } from '../../types/popper'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import type { PopperOptions, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { popover } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof popover>(appConfig.ui.strategy, appConfig.ui.popover, popover)
export default defineComponent({
components: {
@@ -71,16 +69,17 @@ export default defineComponent({
type: Object as PropType<PopperOptions>,
default: () => ({})
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.popover>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.popover>>(() => defuTwMerge({}, props.ui, appConfig.ui.popover))
setup (props) {
const { ui, attrs } = useUI('popover', toRef(props, 'ui'), config, toRef(props, 'class'))
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
@@ -108,8 +107,6 @@ export default defineComponent({
return props.mode === 'hover' ? { paddingTop: `${offsetDistance}px`, paddingBottom: `${offsetDistance}px` } : {}
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
function onMouseOver () {
if (props.mode !== 'hover' || !popoverApi.value) {
return
@@ -151,14 +148,13 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
popover,
trigger,
container,
containerStyle,
wrapperClass,
onMouseOver,
onMouseLeave
}

View File

@@ -1,6 +1,6 @@
<template>
<TransitionRoot as="template" :appear="appear" :show="isOpen">
<HDialog :class="[wrapperClass, { 'justify-end': side === 'right' }]" v-bind="attrs" @close="(e) => !preventClose && close(e)">
<HDialog :class="[ui.wrapper, { 'justify-end': side === 'right' }]" v-bind="attrs" @close="(e) => !preventClose && close(e)">
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
<div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild>
@@ -15,18 +15,17 @@
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { computed, toRef, defineComponent } from 'vue'
import type { WritableComputedRef, PropType } from 'vue'
import { Dialog as HDialog, DialogPanel as HDialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { slideover } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof slideover>(appConfig.ui.strategy, appConfig.ui.slideover, slideover)
export default defineComponent({
components: {
@@ -62,17 +61,18 @@ export default defineComponent({
type: Boolean,
default: false
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.slideover>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
emits: ['update:modelValue', 'close'],
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.slideover>>(() => defuTwMerge({}, props.ui, appConfig.ui.slideover))
setup (props, { emit }) {
const { ui, attrs } = useUI('slideover', toRef(props, 'ui'), config, toRef(props, 'class'))
const isOpen: WritableComputedRef<boolean> = computed({
get () {
@@ -83,8 +83,6 @@ export default defineComponent({
}
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const transitionClass = computed(() => {
if (!props.transition) {
return {}
@@ -105,11 +103,10 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
isOpen,
wrapperClass,
transitionClass,
close
}

View File

@@ -1,5 +1,5 @@
<template>
<div ref="trigger" :class="wrapperClass" v-bind="attrs" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
<div ref="trigger" :class="ui.wrapper" v-bind="attrs" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
<slot :open="open">
Hover
</slot>
@@ -24,21 +24,19 @@
</template>
<script lang="ts">
import { computed, ref, defineComponent } from 'vue'
import { computed, ref, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import UKbd from '../elements/Kbd.vue'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper'
import { defuTwMerge } from '../../utils'
import type { PopperOptions } from '../../types/popper'
import { useAppConfig } from '#imports'
// TODO: Remove
import { mergeConfig } from '../../utils'
import type { PopperOptions, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { tooltip } from '#ui/ui.config'
// const appConfig = useAppConfig()
const config = mergeConfig<typeof tooltip>(appConfig.ui.strategy, appConfig.ui.tooltip, tooltip)
export default defineComponent({
components: {
@@ -70,16 +68,17 @@ export default defineComponent({
type: Object as PropType<PopperOptions>,
default: () => ({})
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.tooltip>>,
default: () => ({})
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined
}
},
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.tooltip>>(() => defuTwMerge({}, props.ui, appConfig.ui.tooltip))
setup (props) {
const { ui, attrs } = useUI('tooltip', toRef(props, 'ui'), config, toRef(props, 'class'))
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
@@ -90,8 +89,6 @@ export default defineComponent({
let openTimeout: NodeJS.Timeout | null = null
let closeTimeout: NodeJS.Timeout | null = null
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
// Methods
function onMouseOver () {
@@ -127,13 +124,12 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
trigger,
container,
open,
wrapperClass,
onMouseOver,
onMouseLeave
}

View File

@@ -1,10 +1,22 @@
import { inject, ref } from 'vue'
import { inject, ref, computed } from 'vue'
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core'
import type { FormEvent, FormEventType } from '../types/form'
import type { FormEvent, FormEventType, InjectedFormGroupValue } from '../types/form'
export const useFormGroup = () => {
type InputProps = {
id?: string
size?: string
color?: string
name?: string
}
export const useFormGroup = (inputProps?: InputProps, config?: any) => {
const formBus = inject<UseEventBusReturn<FormEvent, string> | undefined>('form-events', undefined)
const formGroup = inject('form-group', undefined)
const formGroup = inject<InjectedFormGroupValue>('form-group', undefined)
if (formGroup) {
// Updates for="..." attribute on label if inputProps.id is provided
formGroup.inputId.value = inputProps?.id ?? formGroup?.inputId.value
}
const blurred = ref(false)
@@ -30,9 +42,12 @@ export const useFormGroup = () => {
}, 300)
return {
inputId: computed(() => inputProps.id ?? formGroup?.inputId.value),
name: computed(() => inputProps?.name ?? formGroup?.name.value),
size: computed(() => inputProps?.size ?? formGroup?.size.value ?? config?.default?.size),
color: computed(() => formGroup?.error?.value ? 'red' : inputProps?.color),
emitFormBlur,
emitFormInput,
emitFormChange,
formGroup
emitFormChange
}
}

View File

@@ -2,7 +2,6 @@ import { ref, onMounted, watchEffect } from 'vue'
import type { Ref } from 'vue'
import { popperGenerator, defaultModifiers, VirtualElement } from '@popperjs/core/lib/popper-lite'
import type { Instance } from '@popperjs/core'
import { omitBy, isUndefined } from 'lodash-es'
import flip from '@popperjs/core/lib/modifiers/flip'
import offset from '@popperjs/core/lib/modifiers/offset'
import preventOverflow from '@popperjs/core/lib/modifiers/preventOverflow'
@@ -43,36 +42,49 @@ export function usePopper ({
if (!(popperEl instanceof HTMLElement)) { return }
if (!referenceEl) { return }
instance.value = createPopper(referenceEl, popperEl, omitBy({
placement,
strategy,
modifiers: [{
name: 'flip',
enabled: !locked
}, {
name: 'preventOverflow',
options: {
padding: overflowPadding
const config: Record<string, any> = {
modifiers: [
{
name: 'flip',
enabled: !locked
},
{
name: 'preventOverflow',
options: {
padding: overflowPadding
}
},
{
name: 'offset',
options: {
offset: [offsetSkid, offsetDistance]
}
},
{
name: 'computeStyles',
options: {
adaptive,
gpuAcceleration
}
},
{
name: 'eventListeners',
options: {
scroll,
resize
}
}
}, {
name: 'offset',
options: {
offset: [offsetSkid, offsetDistance]
}
}, {
name: 'computeStyles',
options: {
adaptive,
gpuAcceleration
}
}, {
name: 'eventListeners',
options: {
scroll,
resize
}
}]
}, isUndefined))
]
}
if (placement) {
config.placement = placement
}
if (strategy) {
config.strategy = strategy
}
instance.value = createPopper(referenceEl, popperEl, config)
onInvalidate(instance.value.destroy)
})

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