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 blank_issues_enabled: false
contact_links: contact_links:
- name: Nuxt Community Discord - name: 📖 Documentation
url: https://discord.nuxtjs.org/ url: https://ui.nuxt.com
about: Consider asking questions about the module here. 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 - name: Install dependencies
run: pnpm install run: pnpm install
- name: Prepare
run: pnpm run dev:prepare
- name: Lint - name: Lint
run: pnpm run lint run: pnpm run lint
- name: Build
run: pnpm run build
- name: Typecheck - name: Typecheck
run: pnpm run typecheck run: pnpm run typecheck
- name: Build
run: pnpm run build
- name: Release Edge - name: Release Edge
if: github.event_name == 'push' if: github.event_name == 'push'
run: ./scripts/release-edge.sh run: ./scripts/release-edge.sh

View File

@@ -49,15 +49,18 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Prepare
run: pnpm run dev:prepare
- name: Lint - name: Lint
run: pnpm run lint run: pnpm run lint
- name: Build
run: pnpm run build
- name: Typecheck - name: Typecheck
run: pnpm run typecheck run: pnpm run typecheck
- name: Build
run: pnpm run build
- name: Version Check - name: Version Check
id: check id: check
uses: EndBug/version-check@v2 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 # 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) ## [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 - Keyboard shortcuts
- Bundled icons - Bundled icons
- Fully typed - Fully typed
- [Figma Kit](https://www.figma.com/community/file/1288455405058138934)
Read more on [ui.nuxt.com](https://ui.nuxt.com) Read more on [ui.nuxt.com](https://ui.nuxt.com)

View File

@@ -1,4 +1,6 @@
# To use Nuxt Elements in production # To link Nuxt UI Pro in development
NUXT_ELEMENTS_TOKEN= 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 # Used when pre-rendering the docs for dynamic OG images
NUXT_PUBLIC_SITE_URL= NUXT_PUBLIC_SITE_URL=

1
docs/.nuxtrc Normal file
View File

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

View File

@@ -10,7 +10,7 @@
<Footer /> <Footer />
<ClientOnly> <ClientOnly>
<LazyUDocsSearch :files="files" :navigation="navigation" /> <LazyUDocsSearch ref="searchRef" :files="files" :navigation="navigation" :groups="groups" />
</ClientOnly> </ClientOnly>
<UNotifications> <UNotifications>
@@ -27,36 +27,52 @@
<script setup lang="ts"> <script setup lang="ts">
import { withoutTrailingSlash } from 'ufo' 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 route = useRoute()
const colorMode = useColorMode() const colorMode = useColorMode()
const { prefix, removePrefixFromNavigation, removePrefixFromFiles } = useContentSource() const { branch, branches } = useContentSource()
const { data: nav } = await useAsyncData('navigation', () => fetchContentNavigation()) const { data: nav } = await useAsyncData('navigation', () => fetchContentNavigation())
const { data: files } = useLazyFetch<ParsedContent[]>('/api/search.json', { default: () => [], server: false })
const { data: search } = useLazyFetch('/api/search.json', {
default: () => [],
server: false
})
// Computed // 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 groups = computed(() => {
const files = search.value.filter(file => file._path.startsWith(prefix.value)) 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') 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 // Head
useHead({ useHead({
meta: [ 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 } { key: 'theme-color', name: 'theme-color', content: color }
], ],
link: [ link: [
@@ -74,6 +90,7 @@ useServerSeoMeta({
}) })
// Provide // Provide
provide('navigation', navigation) provide('navigation', navigation)
provide('files', files) provide('files', files)
</script> </script>

View File

@@ -10,7 +10,7 @@
color="gray" color="gray"
:ui="{ icon: { trailing: { padding: { sm: 'pe-1.5' } } } }" :ui="{ icon: { trailing: { padding: { sm: 'pe-1.5' } } } }"
:ui-menu="{ option: { container: 'gap-1.5' } }" :ui-menu="{ option: { container: 'gap-1.5' } }"
@update:model-value="selectBranch" @update:model-value="select"
> >
<template #label> <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" /> <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> </template>
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute() const { branches, branch, select } = useContentSource()
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', ''))
}
}
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <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"> <div class="bg-white dark:bg-gray-900 px-4">
<LogoOnly class="w-5 h-5" /> <LogoOnly class="w-5 h-5" />
</div> </div>
@@ -24,9 +24,14 @@
</template> </template>
<template #right> <template #right>
<USocialButton aria-label="Nuxt Website" icon="i-simple-icons-nuxtdotjs" to="https://nuxt.com" /> <UButton aria-label="Nuxt Website" icon="i-simple-icons-nuxtdotjs" to="https://nuxt.com" target="_blank" v-bind="($ui.button.secondary as any)" />
<USocialButton aria-label="Nuxt on X" icon="i-simple-icons-x" to="https://x.com/nuxt_js" /> <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)" />
<USocialButton aria-label="Nuxt UI on GitHub" icon="i-simple-icons-github" to="https://github.com/nuxt/ui" /> <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> </template>
</UFooter> </UFooter>
</template> </template>
<script setup lang="ts">
// force typescript
</script>

View File

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

View File

@@ -1,7 +1,7 @@
<template> <template>
<UPopover mode="hover"> <UPopover mode="hover">
<template #default="{ open }"> <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" /> <UIcon name="i-heroicons-swatch-20-solid" class="w-5 h-5 text-primary-500 dark:text-primary-400" />
</UButton> </UButton>
</template> </template>
@@ -30,7 +30,7 @@ const colorMode = useColorMode()
// Computed // 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({ const primary = computed({
get () { get () {
return primaryColors.value.find(option => option.value === appConfig.ui.primary) return primaryColors.value.find(option => option.value === appConfig.ui.primary)

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<template> <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]"> <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" /> <ContentSlot v-if="$slots.default" :use="$slots.default" />
</div> </div>

View File

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

View File

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

View File

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

View File

@@ -5,9 +5,9 @@ const toast = useToast()
const commandPaletteRef = ref() const commandPaletteRef = ref()
const users = [ 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: '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' } }, { 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' } } { 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 = [ 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" /> <UButton icon="i-heroicons-calendar-days-20-solid" :label="label" />
<template #panel="{ close }"> <template #panel="{ close }">
<DatePicker v-model="date" @close="close" /> <LazyDatePicker v-model="date" @close="close" />
</template> </template>
</UPopover> </UPopover>
</template> </template>

View File

@@ -80,7 +80,7 @@ async function submit (event: FormSubmitEvent<Schema>) {
</UFormGroup> </UFormGroup>
<UFormGroup name="checkbox" label="Checkbox"> <UFormGroup name="checkbox" label="Checkbox">
<UCheckbox v-model="state.checkbox" /> <UCheckbox v-model="state.checkbox" label="Check me" />
</UFormGroup> </UFormGroup>
<UFormGroup name="radio" label="Radio"> <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> </script>
<template> <template>
<URange v-model="value" /> <label for="range" class="sr-only" />
<URange id="range" v-model="value" name="range" />
</template> </template>

View File

@@ -6,7 +6,7 @@ const selected = ref(people[3])
<template> <template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people"> <USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton color="gray"> <UButton color="gray" class="flex-1 justify-between">
{{ selected }} {{ 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']" /> <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: { avatar: {
src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/benjamincanac', src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/benjamincanac',
srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac 2x', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac 2x',
alt: 'benjamincanac' alt: ''
}, },
label: 'benjamincanac', label: 'benjamincanac',
to: 'https://github.com/benjamincanac', to: 'https://github.com/benjamincanac',
@@ -12,7 +12,7 @@ const links = [{
avatar: { avatar: {
src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/Atinux', src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/Atinux',
srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux 2x', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux 2x',
alt: 'Atinux' alt: ''
}, },
label: 'Atinux', label: 'Atinux',
to: 'https://github.com/Atinux', to: 'https://github.com/Atinux',
@@ -21,14 +21,12 @@ const links = [{
avatar: { avatar: {
src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/smarroufin', src: 'https://ipx.nuxt.com/s_16x16/gh_avatar/smarroufin',
srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin 2x', srcset: 'https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin 2x',
alt: 'smarroufin' alt: ''
}, },
label: 'smarroufin', label: 'smarroufin',
to: 'https://github.com/smarroufin', to: 'https://github.com/smarroufin',
target: '_blank' target: '_blank'
}] }]
const { ui } = useAppConfig()
</script> </script>
<template> <template>
@@ -36,8 +34,9 @@ const { ui } = useAppConfig()
<template #avatar="{ link }"> <template #avatar="{ link }">
<UAvatar <UAvatar
v-if="link.avatar" v-if="link.avatar"
v-bind="{ size: ui.verticalNavigation.avatar.size, ...link.avatar }" v-bind="link.avatar"
:class="[ui.verticalNavigation.avatar.base]" size="3xs"
loading="lazy"
/> />
<UIcon v-else name="i-heroicons-user-circle-20-solid" class="text-lg" /> <UIcon v-else name="i-heroicons-user-circle-20-solid" class="text-lg" />
</template> </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> <template>
<Transition appear name="fade"> <ULandingGrid ref="section" class="lg:grid-cols-10 lg:gap-8">
<ULandingGrid class="lg:grid-cols-10 lg:gap-8"> <div :ref="(el) => (refs[1] = el)" class="col-span-8 flex items-center animate-top">
<div class="col-span-8 flex items-center"> <RangeExample />
<RangeExample /> </div>
</div>
<div class="col-span-2 row-span-2 flex items-center"> <div :ref="(el) => (refs[2] = el)" class="col-span-2 row-span-2 flex items-center animate-right">
<RadioExample /> <RadioExample />
</div> </div>
<div class="col-span-2"> <div :ref="(el) => (refs[4] = el)" class="col-span-2 animate-left z-10">
<DropdownExampleBasic :popper="{ placement: 'bottom-start', strategy: 'absolute' }" /> <DropdownExampleBasic :popper="{ placement: 'bottom-start', strategy: 'absolute' }" />
</div> </div>
<div class="col-span-6 flex flex-wrap items-center justify-between gap-1"> <div
<UAvatarGroup :max="2"> :ref="(el) => (refs[3] = el)"
<UAvatar class="col-span-6 flex flex-wrap items-center justify-between gap-1 animate-bottom"
src="https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac" >
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/benjamincanac 2x" <UAvatarGroup :max="2">
alt="benjamincanac" <UAvatar
width="40" src="https://ipx.nuxt.com/s_32x32/gh_avatar/benjamincanac"
height="40" srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/benjamincanac 2x"
/> alt="benjamincanac"
<UAvatar width="40"
src="https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux" height="40"
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/Atinux 2x" loading="lazy"
alt="Atinux" />
width="40" <UAvatar
height="40" src="https://ipx.nuxt.com/s_32x32/gh_avatar/Atinux"
/> srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/Atinux 2x"
<UAvatar alt="Atinux"
src="https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin" width="40"
srcset="https://ipx.nuxt.com/s_64x64/gh_avatar/smarroufin 2x" height="40"
alt="smarroufin" loading="lazy"
width="40" />
height="40" <UAvatar
/> src="https://ipx.nuxt.com/s_32x32/gh_avatar/smarroufin"
</UAvatarGroup> 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 /> <PaginationExampleBasic />
</div> </div>
<div class="col-span-3 row-span-8 gap-6 flex flex-col justify-between"> <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" /> <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"> <UCard class="flex-shrink-0">
<div class="flex items-center gap-4 justify-center"> <div class="flex items-center gap-4 justify-center">
<USkeleton class="h-14 w-14 flex-shrink-0" :ui="{ rounded: 'rounded-full' }" /> <USkeleton class="h-14 w-14 flex-shrink-0" :ui="{ rounded: 'rounded-full' }" />
<div class="space-y-3 flex-1"> <div class="space-y-3 flex-1">
<USkeleton class="h-4 w-full" /> <USkeleton class="h-4 w-full" />
<USkeleton class="h-4 w-2/3" /> <USkeleton class="h-4 w-2/3" />
</div>
</div> </div>
</UCard> </div>
</div> </UCard>
</div>
<div class="col-span-5 row-span-2 flex flex-col"> <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"> <UCard
<CommandPaletteExampleGroups /> :ui="{ body: { base: 'flex-1 flex flex-col overflow-y-auto', padding: '' } }"
</UCard> class="col-span-4 row-span-6 flex-1 flex flex-col"
</div> >
<CommandPaletteExampleGroups />
</UCard>
</div>
<div class="col-span-2 row-span-2 gap-6 flex flex-col"> <div :ref="(el) => (refs[7] = el)" class="col-span-2 row-span-2 gap-6 flex flex-col animate-right z-10">
<CheckboxExample /> <CheckboxExample />
<InputExampleClearable /> <InputExampleClearable />
<UFormGroup label="Labels"> <UFormGroup label="Labels">
<SelectMenuExampleCreatable /> <SelectMenuExampleCreatable />
</UFormGroup> </UFormGroup>
<UCard :ui="{ body: { padding: '!p-1' } }"> <UCard :ui="{ body: { padding: '!p-1' } }">
<VerticalNavigationExampleAvatarSlot /> <VerticalNavigationExampleAvatarSlot />
</UCard> </UCard>
</div> </div>
<div class="col-span-7 row-span-6"> <div :ref="(el) => (refs[8] = el)" class="col-span-7 row-span-6 animate-bottom">
<UCard :ui="{ body: { padding: '' } }"> <UCard :ui="{ body: { padding: '' } }">
<TableExampleClickable :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" /> <TableExampleClickable :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" />
</UCard> </UCard>
</div> </div>
</ULandingGrid> </ULandingGrid>
</Transition>
</template> </template>
<style scoped> <style scoped lang="postcss">
.fade-enter-active, .animate-top {
.fade-leave-active { animation: translateDown;
transition: opacity 0.5s ease;
} }
.fade-enter-from, .animate-bottom {
.fade-leave-to { animation: translateUp;
opacity: 0; }
.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> </style>

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
--- ---
description: 'Learn how to customize the look and feel of the components.' description: 'Learn how to customize the look and feel of the components.'
navigation:
badge: New
--- ---
## Overview ## Overview
@@ -79,7 +81,7 @@ This can also happen when you bind a dynamic color to a component: `<UBadge :col
### `app.config.ts` ### `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] ```ts [app.config.ts]
export default defineAppConfig({ 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 ### `ui` prop
Each component has a `ui` prop that allows you to customize everything specifically. 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 ```json
{ {
...
"label": { "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: To change the font of the `label`, you only need to write:
```vue ```vue
<UFormGroup name="email" label="Email" :ui="{ label: { base: 'font-semibold' } }"> <UFormGroup name="email" label="Email" :ui="{ label: { base: 'font-semibold' } }" />
...
</UFormGroup>
``` ```
This will smartly replace the `font-medium` by `font-semibold` and prevent any class duplication and any class priority issue. 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 ### `class` attribute
You can also use the `class` attribute to add classes to the component. 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. 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. 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> <template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people"> <USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton color="gray"> <UButton color="gray" class="flex-1 justify-between">
{{ selected }} {{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" /> <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. 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 ## Props
:component-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 ## Slots
You can use slots to customize the header and data cells of the table. 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', to: 'https://github.com/benjamincanac',
target: '_blank' target: '_blank'
}, ...] }, ...]
const { ui } = useAppConfig()
</script> </script>
<template> <template>
@@ -134,8 +132,8 @@ const { ui } = useAppConfig()
<template #avatar="{ link }"> <template #avatar="{ link }">
<UAvatar <UAvatar
v-if="link.avatar" v-if="link.avatar"
v-bind="{ size: ui.verticalNavigation.avatar.size, ...link.avatar }" v-bind="link.avatar"
:class="[ui.verticalNavigation.avatar.base]" size="3xs"
/> />
<UIcon v-else name="i-heroicons-user-circle-20-solid" class="text-lg" /> <UIcon v-else name="i-heroicons-user-circle-20-solid" class="text-lg" />
</template> </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.' 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: sections:
- slot: demo - 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> - title: Everything you expect from a<br class="hidden lg:block"> <span class="text-primary">UI component library</span>
slot: features 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' 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> </UContainer>
<ClientOnly> <ClientOnly>
<UDocsSearch :files="files" :navigation="navigation" /> <LazyUDocsSearch :files="files" :navigation="navigation" />
</ClientOnly> </ClientOnly>
<UNotifications /> <UNotifications />
@@ -20,8 +20,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NuxtError } from '#app' import type { NuxtError } from '#app'
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
const { prefix, removePrefixFromNavigation, removePrefixFromFiles } = useContentSource()
useSeoMeta({ useSeoMeta({
title: 'Page not found', title: 'Page not found',
@@ -32,22 +31,18 @@ defineProps<{
error: NuxtError error: NuxtError
}>() }>()
const { data: navigation } = await useLazyAsyncData('navigation', () => fetchContentNavigation(), { const { branch } = useContentSource()
default: () => [],
transform: (navigation) => {
navigation = navigation.find(link => link._path === prefix.value)?.children || []
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(), { // Computed
default: () => [],
transform: (files) => {
files = files.filter(file => file._path.startsWith(prefix.value))
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 // Provide

View File

@@ -3,7 +3,7 @@
<UContainer> <UContainer>
<UPage> <UPage>
<template #left> <template #left>
<UAside :links="anchors"> <UAside>
<BranchSelect /> <BranchSelect />
<UNavigationTree :links="mapContentNavigation(navigation)" /> <UNavigationTree :links="mapContentNavigation(navigation)" />
@@ -19,23 +19,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types' import type { NavItem } from '@nuxt/content/dist/runtime/types'
const { mapContentNavigation } = useElementsHelpers()
const navigation = inject<NavItem[]>('navigation') 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> </script>

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template> <template>
<div> <div>
<ULandingHero v-bind="page.hero" :ui="{ base: 'relative z-[1]', container: 'max-w-3xl' }" class="mb-[calc(var(--header-height)*2)]"> <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" autocomplete="off"
icon="i-heroicons-command-line" icon="i-heroicons-command-line"
input-class="select-none" input-class="select-none"
aria-label="Install @nuxt/ui"
size="lg" size="lg"
:ui="{ base: 'disabled:cursor-default', icon: { trailing: { pointer: '' } } }" :ui="{ base: 'disabled:cursor-default', icon: { trailing: { pointer: '' } } }"
> >
@@ -148,8 +150,9 @@
width="40" width="40"
height="40" height="40"
size="md" 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" /> <span class="absolute inset-0" aria-hidden="true" />
</NuxtLink> </NuxtLink>
</UAvatar> </UAvatar>
@@ -179,10 +182,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { pick } from 'lodash-es'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core' 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<{ const { data: module } = await useFetch<{
stats: { stats: {
downloads: number downloads: number
@@ -192,7 +194,7 @@ const { data: module } = await useFetch<{
username: string username: string
}[] }[]
}>('https://api.nuxt.com/modules/ui', { }>('https://api.nuxt.com/modules/ui', {
transform: (module) => pick(module, ['stats', 'contributors']) transform: ({ stats, contributors }) => ({ stats, contributors })
}) })
const source = ref('npm i @nuxt/ui') 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' import defaultTheme from 'tailwindcss/defaultTheme'
export default <Partial<Config>>{ export default <Partial<Config>>{
content: {
files: [
'content/**/*.yml'
]
},
theme: { theme: {
extend: { extend: {
fontFamily: { fontFamily: {

View File

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

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 # Bump versions to edge
pnpm jiti ./scripts/bump-edge pnpm jiti ./scripts/bump-edge
# Resolve pnpm
pnpm install
# Update token # Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc

View File

@@ -3,9 +3,6 @@
# Restore all git changes # Restore all git changes
git restore -s@ -SW -- . git restore -s@ -SW -- .
# Resolve pnpm
pnpm install
# Update token # Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc 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 = [ const colorsToExclude = [
'inherit', '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 defaultColors from 'tailwindcss/colors.js'
import { defaultExtractor as createDefaultExtractor } from 'tailwindcss/lib/lib/defaultExtractor.js' import { defaultExtractor as createDefaultExtractor } from 'tailwindcss/lib/lib/defaultExtractor.js'
import { iconsPlugin, getIconCollections } from '@egoist/tailwindcss-icons' import { iconsPlugin, getIconCollections } from '@egoist/tailwindcss-icons'
import { name, version } from '../package.json' import { name, version } from '../package.json'
import { generateSafelist, excludeColors, customSafelistExtractor } from './colors' import { generateSafelist, excludeColors, customSafelistExtractor } from './colors'
import appConfig from './runtime/app.config' import createTemplates from './templates'
import * as config from './runtime/ui.config'
type DeepPartial<T> = Partial<{ [P in keyof T]: DeepPartial<T[P]> | { [key: string]: string } }> import type { DeepPartial, Strategy } from './runtime/types/utils'
const defaultExtractor = createDefaultExtractor({ tailwindConfig: { separator: ':' } }) const defaultExtractor = createDefaultExtractor({ tailwindConfig: { separator: ':' } })
@@ -16,13 +16,22 @@ delete defaultColors.trueGray
delete defaultColors.coolGray delete defaultColors.coolGray
delete defaultColors.blueGray 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' { declare module '@nuxt/schema' {
interface AppConfigInput { interface AppConfigInput {
ui?: { ui?: UI
primary?: string
gray?: string
colors?: string[]
} & DeepPartial<typeof appConfig.ui>
} }
} }
@@ -64,12 +73,9 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.build.transpile.push(runtimeDir) nuxt.options.build.transpile.push(runtimeDir)
nuxt.options.build.transpile.push('@popperjs/core', '@headlessui/vue') 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.options.css.push(resolve(runtimeDir, 'ui.css'))
nuxt.hook('app:resolve', (app) => {
app.configs.push(appConfigFile)
})
nuxt.hook('tailwindcss:config', function (tailwindConfig) { nuxt.hook('tailwindcss:config', function (tailwindConfig) {
const globalColors: any = { const globalColors: any = {
@@ -116,11 +122,12 @@ export default defineNuxtModule<ModuleOptions>({
const colors = excludeColors(globalColors) const colors = excludeColors(globalColors)
// @ts-ignore
nuxt.options.appConfig.ui = { nuxt.options.appConfig.ui = {
...nuxt.options.appConfig.ui,
primary: 'green', primary: 'green',
gray: 'cool', gray: 'cool',
colors colors,
strategy: 'merge'
} }
tailwindConfig.safelist = tailwindConfig.safelist || [] tailwindConfig.safelist = tailwindConfig.safelist || []
@@ -130,6 +137,8 @@ export default defineNuxtModule<ModuleOptions>({
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) })) tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
}) })
createTemplates(nuxt)
// Modules // Modules
await installModule('@nuxtjs/color-mode', { classSuffix: '' }) await installModule('@nuxtjs/color-mode', { classSuffix: '' })
@@ -141,7 +150,8 @@ export default defineNuxtModule<ModuleOptions>({
require('@tailwindcss/forms')({ strategy: 'class' }), require('@tailwindcss/forms')({ strategy: 'class' }),
require('@tailwindcss/aspect-ratio'), require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/typography'), require('@tailwindcss/typography'),
require('@tailwindcss/container-queries') require('@tailwindcss/container-queries'),
require('@headlessui/tailwindcss')
], ],
content: { content: {
files: [ files: [

View File

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

View File

@@ -1,5 +1,5 @@
<template> <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"> <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"> <HDisclosureButton :ref="() => buttonRefs[index] = close" as="template" :disabled="item.disabled">
<slot :item="item" :index="index" :open="open" :close="close"> <slot :item="item" :index="index" :open="open" :close="close">
@@ -40,20 +40,22 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, computed, defineComponent } from 'vue' import { ref, computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { Disclosure as HDisclosure, DisclosureButton as HDisclosureButton, DisclosurePanel as HDisclosurePanel } from '@headlessui/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 UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.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 StateEmitter from '../../utils/StateEmitter'
import type { AccordionItem } from '../../types/accordion' import type { AccordionItem, Strategy } from '../../types'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' 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({ export default defineComponent({
components: { components: {
@@ -76,43 +78,47 @@ export default defineComponent({
}, },
openIcon: { openIcon: {
type: String, type: String,
default: () => appConfig.ui.accordion.default.openIcon default: () => config.default.openIcon
}, },
closeIcon: { closeIcon: {
type: String, type: String,
default: () => appConfig.ui.accordion.default.closeIcon default: () => config.default.closeIcon
}, },
multiple: { multiple: {
type: Boolean, type: Boolean,
default: false default: false
}, },
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.accordion>>, type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: () => ({}) default: undefined
} }
}, },
setup (props, { attrs }) { setup (props) {
// TODO: Remove const { ui, attrs } = useUI('accordion', toRef(props, 'ui'), config, toRef(props, 'class'))
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.accordion>>(() => defuTwMerge({}, props.ui, appConfig.ui.accordion)) const uiButton = computed<Partial<typeof configButton>>(() => configButton)
const uiButton = computed<Partial<typeof appConfig.ui.button>>(() => appConfig.ui.button)
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const buttonRefs = ref<Function[]>([]) const buttonRefs = ref<Function[]>([])
function closeOthers (itemIndex: number) { function closeOthers (currentIndex: number) {
if (!props.items[itemIndex].closeOthers && props.multiple) { if (!props.items[currentIndex].closeOthers && props.multiple) {
return return
} }
buttonRefs.value.forEach((close, index) => { const totalItems = buttonRefs.value.length
if (index === itemIndex) return
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() close()
}) }
} }
function onEnter (el: HTMLElement, done) { function onEnter (el: HTMLElement, done) {
@@ -139,11 +145,10 @@ export default defineComponent({
} }
return { return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
uiButton, uiButton,
wrapperClass, attrs,
buttonRefs, buttonRefs,
closeOthers, closeOthers,
omit, 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" /> <UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="action.click" />
</div> </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> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import type { Avatar } from '../../types/avatar' import { useUI } from '../../composables/useUI'
import type { Button } from '../../types/button' import type { Avatar, Button, NestedKeyOf, Strategy } from '../../types'
import { defuTwMerge } from '../../utils' import { mergeConfig } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' 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({ export default defineComponent({
components: { components: {
@@ -67,7 +66,7 @@ export default defineComponent({
}, },
icon: { icon: {
type: String, type: String,
default: () => appConfig.ui.alert.default.icon default: () => config.default.icon
}, },
avatar: { avatar: {
type: Object as PropType<Avatar>, type: Object as PropType<Avatar>,
@@ -75,40 +74,41 @@ export default defineComponent({
}, },
closeButton: { closeButton: {
type: Object as PropType<Button>, type: Object as PropType<Button>,
default: () => appConfig.ui.alert.default.closeButton default: () => config.default.closeButton as Button
}, },
actions: { actions: {
type: Array as PropType<(Button & { click?: Function })[]>, type: Array as PropType<(Button & { click?: Function })[]>,
default: () => [] default: () => []
}, },
color: { color: {
type: String, type: String as PropType<keyof typeof config.color | typeof colors[number]>,
default: () => appConfig.ui.alert.default.color, default: () => config.default.color,
validator (value: string) { 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: { variant: {
type: String, type: String as PropType<keyof typeof config.variant | NestedKeyOf<typeof config.color>>,
default: () => appConfig.ui.alert.default.variant, default: () => config.default.variant,
validator (value: string) { validator (value: string) {
return [ return [
...Object.keys(appConfig.ui.alert.variant), ...Object.keys(config.variant),
...Object.values(appConfig.ui.alert.color).flatMap(value => Object.keys(value)) ...Object.values(config.color).flatMap(value => Object.keys(value))
].includes(value) ].includes(value)
} }
}, },
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.alert>>, type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: () => ({}) default: undefined
} }
}, },
emits: ['close'], emits: ['close'],
setup (props, { attrs }) { setup (props) {
// TODO: Remove const { ui, attrs } = useUI('alert', toRef(props, 'ui'), config)
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.alert>>(() => defuTwMerge({}, props.ui, appConfig.ui.alert))
const alertClass = computed(() => { const alertClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant] 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.shadow,
ui.value.padding, ui.value.padding,
variant?.replaceAll('{color}', props.color) variant?.replaceAll('{color}', props.color)
), attrs.class as string) ), props.class)
}) })
return { return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
attrs,
alertClass alertClass
} }
} }

View File

@@ -20,18 +20,18 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, computed, watch } from 'vue' import { defineComponent, ref, computed, toRef, watch } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import { defuTwMerge } from '../../utils' import { useUI } from '../../composables/useUI'
import { useAppConfig } from '#imports' import { mergeConfig } from '../../utils'
// TODO: Remove import type { AvatarSize, AvatarChipColor, AvatarChipPosition, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' 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({ export default defineComponent({
components: { components: {
@@ -53,27 +53,27 @@ export default defineComponent({
}, },
icon: { icon: {
type: String, type: String,
default: () => appConfig.ui.avatar.default.icon default: () => config.default.icon
}, },
size: { size: {
type: String, type: String as PropType<AvatarSize>,
default: () => appConfig.ui.avatar.default.size, default: () => config.default.size,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.avatar.size).includes(value) return Object.keys(config.size).includes(value)
} }
}, },
chipColor: { chipColor: {
type: String, type: String as PropType<AvatarChipColor>,
default: () => appConfig.ui.avatar.default.chipColor, default: () => config.default.chipColor,
validator (value: string) { validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value) return ['gray', ...appConfig.ui.colors].includes(value)
} }
}, },
chipPosition: { chipPosition: {
type: String, type: String as PropType<AvatarChipPosition>,
default: () => appConfig.ui.avatar.default.chipPosition, default: () => config.default.chipPosition,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.avatar.chip.position).includes(value) return Object.keys(config.chip.position).includes(value)
} }
}, },
chipText: { chipText: {
@@ -84,16 +84,17 @@ export default defineComponent({
type: String, type: String,
default: '' default: ''
}, },
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.avatar>>, type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: () => ({}) default: undefined
} }
}, },
setup (props, { attrs }) { setup (props) {
// TODO: Remove const { ui, attrs } = useUI('avatar', toRef(props, 'ui'), config)
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.avatar>>(() => defuTwMerge({}, props.ui, appConfig.ui.avatar))
const url = computed(() => { const url = computed(() => {
if (typeof props.src === 'boolean') { if (typeof props.src === 'boolean') {
@@ -112,7 +113,7 @@ export default defineComponent({
(error.value || !url.value) && ui.value.background, (error.value || !url.value) && ui.value.background,
ui.value.rounded, ui.value.rounded,
ui.value.size[props.size] ui.value.size[props.size]
), attrs.class as string) ), props.class)
}) })
const imgClass = computed(() => { const imgClass = computed(() => {
@@ -151,7 +152,9 @@ export default defineComponent({
} }
return { return {
attrs: omit(attrs, ['class']), // eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
wrapperClass, wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
imgClass, 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 type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge, getSlotsChildren } from '../../utils' import UAvatar from './Avatar.vue'
import Avatar from './Avatar.vue' import { useUI } from '../../composables/useUI'
import { useAppConfig } from '#imports' import { mergeConfig, getSlotsChildren } from '../../utils'
// TODO: Remove import type { AvatarSize, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' 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({ export default defineComponent({
inheritAttrs: false, inheritAttrs: false,
props: { props: {
size: { size: {
type: String, type: String as PropType<keyof typeof avatarConfig.size>,
default: null, default: null,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.avatar.size).includes(value) return Object.keys(avatarConfig.size).includes(value)
} }
}, },
max: { max: {
type: Number, type: Number,
default: null default: null
}, },
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.avatarGroup>>, type: Object as PropType<Partial<typeof avatarGroupConfig & { strategy?: Strategy }>>,
default: () => ({}) default: undefined
} }
}, },
setup (props, { attrs, slots }) { setup (props, { slots }) {
// TODO: Remove const { ui, attrs } = useUI('avatarGroup', toRef(props, 'ui'), avatarGroupConfig, toRef(props, 'class'))
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defuTwMerge({}, props.ui, appConfig.ui.avatarGroup))
const children = computed(() => getSlotsChildren(slots)) const children = computed(() => getSlotsChildren(slots))
@@ -55,8 +58,8 @@ export default defineComponent({
} }
if (max.value !== undefined && index === max.value) { if (max.value !== undefined && index === max.value) {
return h(Avatar, { return h(UAvatar, {
size: props.size || appConfig.ui.avatar.default.size, size: props.size || (avatarConfig.default.size as AvatarSize),
text: `+${children.value.length - max.value}`, text: `+${children.value.length - max.value}`,
class: twJoin(ui.value.ring, ui.value.margin) class: twJoin(ui.value.ring, ui.value.margin)
}) })
@@ -65,6 +68,6 @@ export default defineComponent({
return null return null
}).filter(Boolean).reverse()) }).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> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils' import { useUI } from '../../composables/useUI'
import { useAppConfig } from '#imports' import { mergeConfig } from '../../utils'
// TODO: Remove import type { BadgeColor, BadgeSize, BadgeVariant, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' 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({ export default defineComponent({
inheritAttrs: false, inheritAttrs: false,
props: { props: {
size: { size: {
type: String, type: String as PropType<BadgeSize>,
default: () => appConfig.ui.badge.default.size, default: () => config.default.size,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.badge.size).includes(value) return Object.keys(config.size).includes(value)
} }
}, },
color: { color: {
type: String, type: String as PropType<BadgeColor>,
default: () => appConfig.ui.badge.default.color, default: () => config.default.color,
validator (value: string) { 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: { variant: {
type: String, type: String as PropType<BadgeVariant>,
default: () => appConfig.ui.badge.default.variant, default: () => config.default.variant,
validator (value: string) { validator (value: string) {
return [ return [
...Object.keys(appConfig.ui.badge.variant), ...Object.keys(config.variant),
...Object.values(appConfig.ui.badge.color).flatMap(value => Object.keys(value)) ...Object.values(config.color).flatMap(value => Object.keys(value))
].includes(value) ].includes(value)
} }
}, },
@@ -48,16 +48,17 @@ export default defineComponent({
type: [String, Number], type: [String, Number],
default: null default: null
}, },
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.badge>>, type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: () => ({}) default: undefined
} }
}, },
setup (props, { attrs }) { setup (props) {
// TODO: Remove const { ui, attrs } = useUI('badge', toRef(props, 'ui'), config)
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defuTwMerge({}, props.ui, appConfig.ui.badge))
const badgeClass = computed(() => { const badgeClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant] 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.rounded,
ui.value.size[props.size], ui.value.size[props.size],
variant?.replaceAll('{color}', props.color) variant?.replaceAll('{color}', props.color)
), attrs.class as string) ), props.class)
}) })
return { return {
attrs: omit(attrs, ['class']), attrs,
badgeClass badgeClass
} }
} }

View File

@@ -17,19 +17,19 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue' import { computed, defineComponent, toRef } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import ULink from '../elements/Link.vue' import ULink from '../elements/Link.vue'
import { defuTwMerge } from '../../utils' import { useUI } from '../../composables/useUI'
import { useAppConfig } from '#imports' import { mergeConfig } from '../../utils'
// TODO: Remove import type { ButtonColor, ButtonSize, ButtonVariant, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' 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({ export default defineComponent({
components: { components: {
@@ -63,26 +63,26 @@ export default defineComponent({
default: true default: true
}, },
size: { size: {
type: String, type: String as PropType<ButtonSize>,
default: () => appConfig.ui.button.default.size, default: () => config.default.size,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.button.size).includes(value) return Object.keys(config.size).includes(value)
} }
}, },
color: { color: {
type: String, type: String as PropType<ButtonColor>,
default: () => appConfig.ui.button.default.color, default: () => config.default.color,
validator (value: string) { 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: { variant: {
type: String, type: String as PropType<ButtonVariant>,
default: () => appConfig.ui.button.default.variant, default: () => config.default.variant,
validator (value: string) { validator (value: string) {
return [ return [
...Object.keys(appConfig.ui.button.variant), ...Object.keys(config.variant),
...Object.values(appConfig.ui.button.color).flatMap(value => Object.keys(value)) ...Object.values(config.color).flatMap(value => Object.keys(value))
].includes(value) ].includes(value)
} }
}, },
@@ -92,7 +92,7 @@ export default defineComponent({
}, },
loadingIcon: { loadingIcon: {
type: String, type: String,
default: () => appConfig.ui.button.default.loadingIcon default: () => config.default.loadingIcon
}, },
leadingIcon: { leadingIcon: {
type: String, type: String,
@@ -118,16 +118,17 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.button>>, type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: () => ({}) default: undefined
} }
}, },
setup (props, { attrs, slots }) { setup (props, { slots }) {
// TODO: Remove const { ui, attrs } = useUI('button', toRef(props, 'ui'), config)
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.button>>(() => defuTwMerge({}, props.ui, appConfig.ui.button))
const isLeading = computed(() => { const isLeading = computed(() => {
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon 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], props.padded && ui.value[isSquare.value ? 'square' : 'padding'][props.size],
variant?.replaceAll('{color}', props.color), variant?.replaceAll('{color}', props.color),
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center' props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center'
), attrs.class as string) ), props.class)
}) })
const leadingIconName = computed(() => { const leadingIconName = computed(() => {
@@ -187,7 +188,7 @@ export default defineComponent({
}) })
return { return {
attrs: omit(attrs, ['class']), attrs,
isLeading, isLeading,
isTrailing, isTrailing,
isSquare, 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 type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge, getSlotsChildren } from '../../utils' import { useUI } from '../../composables/useUI'
import { useAppConfig } from '#imports' import { mergeConfig, getSlotsChildren } from '../../utils'
// TODO: Remove import type { ButtonSize, Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' 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({ export default defineComponent({
inheritAttrs: false, inheritAttrs: false,
props: { props: {
size: { size: {
type: String, type: String as PropType<ButtonSize>,
default: null, default: null,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.button.size).includes(value) return Object.keys(buttonConfig.size).includes(value)
} }
}, },
orientation: { orientation: {
@@ -27,16 +28,17 @@ export default defineComponent({
return ['horizontal', 'vertical'].includes(value) return ['horizontal', 'vertical'].includes(value)
} }
}, },
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.buttonGroup>>, type: Object as PropType<Partial<typeof buttonGroupConfig & { strategy?: Strategy }>>,
default: () => ({}) default: undefined
} }
}, },
setup (props, { attrs, slots }) { setup (props, { slots }) {
// TODO: Remove const { ui, attrs } = useUI('buttonGroup', toRef(props, 'ui'), buttonGroupConfig)
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defuTwMerge({}, props.ui, appConfig.ui.buttonGroup))
const children = computed(() => getSlotsChildren(slots)) const children = computed(() => getSlotsChildren(slots))
@@ -58,12 +60,6 @@ export default defineComponent({
const clones = computed(() => children.value.map((node, index) => { const clones = computed(() => children.value.map((node, index) => {
const vProps: any = {} 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) { if (props.size) {
vProps.size = props.size vProps.size = props.size
} }
@@ -83,6 +79,14 @@ export default defineComponent({
return cloneVNode(node, vProps) 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> <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 <HMenuButton
ref="trigger" ref="trigger"
as="div" as="div"
@@ -45,26 +45,23 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, computed, onMounted } from 'vue' import { defineComponent, ref, computed, toRef, onMounted } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue' import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue'
import { defu } from 'defu' import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue' import UKbd from '../elements/Kbd.vue'
import ULink from '../elements/Link.vue' import ULink from '../elements/Link.vue'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper' import { usePopper } from '../../composables/usePopper'
import { defuTwMerge } from '../../utils' import { mergeConfig, omit } from '../../utils'
import type { DropdownItem } from '../../types/dropdown' import type { DropdownItem, PopperOptions, Strategy } from '../../types'
import type { PopperOptions } from '../../types/popper'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' 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({ export default defineComponent({
components: { components: {
@@ -104,16 +101,17 @@ export default defineComponent({
type: Number, type: Number,
default: 0 default: 0
}, },
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.dropdown>>, type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: () => ({}) default: undefined
} }
}, },
setup (props, { attrs }) { setup (props) {
// TODO: Remove const { ui, attrs } = useUI('dropdown', toRef(props, 'ui'), config, toRef(props, 'class'))
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.dropdown>>(() => defuTwMerge({}, props.ui, appConfig.ui.dropdown))
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions)) 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` } : {} return props.mode === 'hover' ? { paddingTop: `${offsetDistance}px`, paddingBottom: `${offsetDistance}px` } : {}
}) })
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
function onMouseOver () { function onMouseOver () {
if (props.mode !== 'hover' || !menuApi.value) { if (props.mode !== 'hover' || !menuApi.value) {
return return
@@ -186,13 +182,12 @@ export default defineComponent({
} }
return { return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
attrs,
trigger, trigger,
container, container,
containerStyle, containerStyle,
wrapperClass,
onMouseOver, onMouseOver,
onMouseLeave, onMouseLeave,
omit omit

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
<template> <template>
<TransitionRoot :appear="appear" :show="isOpen" as="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"> <TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
<div :class="[ui.overlay.base, ui.overlay.background]" /> <div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild> </TransitionChild>
@@ -30,18 +30,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } 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 { Dialog as HDialog, DialogPanel as HDialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { defuTwMerge } from '../../utils' import { useUI } from '../../composables/useUI'
import { useAppConfig } from '#imports' import { mergeConfig } from '../../utils'
// TODO: Remove import type { Strategy } from '../../types'
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' 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({ export default defineComponent({
components: { components: {
@@ -76,17 +75,18 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.modal>>, type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: () => ({}) default: undefined
} }
}, },
emits: ['update:modelValue', 'close'], emits: ['update:modelValue', 'close'],
setup (props, { attrs, emit }) { setup (props, { emit }) {
// TODO: Remove const { ui, attrs } = useUI('modal', toRef(props, 'ui'), config, toRef(props, 'class'))
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.modal>>(() => defuTwMerge({}, props.ui, appConfig.ui.modal))
const isOpen = computed({ const isOpen = computed({
get () { get () {
@@ -97,8 +97,6 @@ export default defineComponent({
} }
}) })
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const transitionClass = computed(() => { const transitionClass = computed(() => {
if (!props.transition) { if (!props.transition) {
return {} return {}
@@ -116,11 +114,10 @@ export default defineComponent({
} }
return { return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
attrs,
isOpen, isOpen,
wrapperClass,
transitionClass, transitionClass,
close 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)" /> <UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
</div> </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> </div>
</div> </div>
@@ -39,24 +39,21 @@
</template> </template>
<script lang="ts"> <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 type { PropType } from 'vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import { useUI } from '../../composables/useUI'
import { useTimer } from '../../composables/useTimer' import { useTimer } from '../../composables/useTimer'
import type { NotificationAction } from '../../types/notification' import { mergeConfig } from '../../utils'
import type { Avatar } from '../../types/avatar' import type { Avatar, Button, NotificationColor, NotificationAction, Strategy } from '../../types'
import type { Button } from '../../types/button'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error // @ts-expect-error
import appConfig from '#build/app.config' 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({ export default defineComponent({
components: { components: {
@@ -80,7 +77,7 @@ export default defineComponent({
}, },
icon: { icon: {
type: String, type: String,
default: () => appConfig.ui.notification.default.icon default: () => config.default.icon
}, },
avatar: { avatar: {
type: Object as PropType<Avatar>, type: Object as PropType<Avatar>,
@@ -88,7 +85,7 @@ export default defineComponent({
}, },
closeButton: { closeButton: {
type: Object as PropType<Button>, type: Object as PropType<Button>,
default: () => appConfig.ui.notification.default.closeButton default: () => config.default.closeButton as Button
}, },
timeout: { timeout: {
type: Number, type: Number,
@@ -103,23 +100,24 @@ export default defineComponent({
default: null default: null
}, },
color: { color: {
type: String, type: String as PropType<NotificationColor>,
default: () => appConfig.ui.notification.default.color, default: () => config.default.color,
validator (value: string) { validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value) return ['gray', ...appConfig.ui.colors].includes(value)
} }
}, },
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.notification>>, type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: () => ({}) default: undefined
} }
}, },
emits: ['close'], emits: ['close'],
setup (props, { attrs, emit }) { setup (props, { emit }) {
// TODO: Remove const { ui, attrs } = useUI('notification', toRef(props, 'ui'), config)
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.notification>>(() => defuTwMerge({}, props.ui, appConfig.ui.notification))
let timer: any = null let timer: any = null
const remaining = ref(props.timeout) const remaining = ref(props.timeout)
@@ -130,7 +128,7 @@ export default defineComponent({
ui.value.background, ui.value.background,
ui.value.rounded, ui.value.rounded,
ui.value.shadow ui.value.shadow
), attrs.class as string) ), props.class)
}) })
const progressClass = computed(() => { const progressClass = computed(() => {
@@ -210,9 +208,9 @@ export default defineComponent({
}) })
return { return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
attrs,
wrapperClass, wrapperClass,
progressClass, progressClass,
progressStyle, progressStyle,

View File

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

View File

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

View File

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

View File

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

View File

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

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