Compare commits

..

88 Commits

Author SHA1 Message Date
HugoRCD
f6b984e475 up 2025-05-31 23:45:37 +02:00
HugoRCD
ac696433b7 feat(SelectMenu): implement virtualization 2025-05-31 23:26:24 +02:00
Eugen Istoc
546df572fc fix(useOverlay)!: correct spelling of unmount function (#4051)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-30 17:27:22 +02:00
Tankosin
37abcc6a5b fix(Form): conditionally type form data via transform prop (#4188)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-30 15:29:01 +02:00
Rajeev R Sharma
accf69046c docs(input): update button group example (#4252) 2025-05-30 15:27:28 +02:00
J-Michalek
80177679f2 feat(Timeline): new component (#4215)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
Co-authored-by: Jakub <jakub.michalek@freelo.io>
2025-05-30 15:27:11 +02:00
Benjamin Canac
536b7afcc1 chore(github): add CODEOWNERS file 2025-05-30 10:58:05 +02:00
Benjamin Canac
483e473e3f fix(Select/SelectMenu): prevent empty string display when multiple
Regression of 7df7ee336a
2025-05-28 17:33:59 +02:00
Joseph Anson
5835eb5f0f feat(Modal/Slideover): add close method in slots (#4219)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-28 15:57:12 +02:00
renovate[bot]
ca507c6a0d chore(deps): update dependency @octokit/rest to v22 (v3) (#4222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 12:47:16 +02:00
Benjamin Canac
03ac395164 feat(Avatar): add chip prop (#4224) 2025-05-28 12:46:30 +02:00
renovate[bot]
f761369888 chore(deps): update dependency reka-ui to ^2.3.0 (v3) (#4234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-28 11:55:12 +02:00
Benjamin Canac
7df7ee336a fix(Select/SelectMenu): display falsy values 2025-05-27 13:00:30 +02:00
J-Michalek
2ee1c5ac2e feat(Carousel): allow customization of active dot color (#4229) 2025-05-26 17:48:30 +02:00
Benjamin Canac
62bc7b25a2 fix(NavigationMenu): set content max-height in horizontal orientation
Resolves #4208
2025-05-26 17:46:46 +02:00
renovate[bot]
66f6c7743c chore(deps): update dependency vue to ^3.5.15 (v3) (#4227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 16:09:06 +02:00
Benjamin Canac
dec2b9fd6a chore(deps): update @nuxt/ui-pro 2025-05-26 15:55:06 +02:00
Benjamin Canac
4604da0f16 docs(content): update badges 2025-05-26 15:02:39 +02:00
Benjamin Canac
a9d693095b docs(navigation-menu): update type field 2025-05-26 15:02:30 +02:00
Benjamin Canac
cddcb95ed4 chore(release): v3.1.3 2025-05-26 14:49:46 +02:00
renovate[bot]
967968e02e chore(deps): lock file maintenance (v3) (#4225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 12:43:35 +02:00
renovate[bot]
f8e560525f chore(deps): update all non-major dependencies (v3) (#4184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 12:08:50 +02:00
Benjamin Canac
8216b59d4f playground: add default-value for combobox components 2025-05-26 11:15:09 +02:00
Byambajav
44ea02c0d6 feat(locale): add Mongolian language (#4214)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-26 10:26:35 +02:00
Benjamin Canac
f95abf8d1d fix(InputMenu/Select/SelectMenu): manual viewport to display scrollbars
Resolves #4069
2025-05-23 17:41:30 +02:00
Benjamin Canac
dcf34a7ac2 fix(ContextMenu/DropdownMenu): wrap groups in a viewport
Resolves #3315
2025-05-23 17:39:17 +02:00
Benjamin Canac
2ba94db09e fix(CommandPalette): add presentation role to viewport 2025-05-23 17:32:18 +02:00
Eugen Istoc
d9e9fea35e feat(Modal/Slideover): add after:enter event (#4187) 2025-05-23 12:39:53 +02:00
Hugo Richard
dae9f0b863 fix(theme): define old-neutral color as static (#4193) 2025-05-23 12:38:33 +02:00
Benjamin Canac
0a72024361 docs(drawer): add interactive background example
Resolves #4199
2025-05-23 12:24:03 +02:00
Benjamin Canac
41087d4c95 fix(Drawer): improve title & description accessibility
Related to #4199

Follow-up e419dcbe61
2025-05-23 11:46:56 +02:00
Scott Carlton
6aab62ec30 fix(Textarea): missing imports (#4207)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-22 21:52:37 +02:00
renovate[bot]
742a37201e chore(deps): update dependency @iconify/vue to v5 (v3) (#4068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-22 17:55:00 +02:00
J-Michalek
473513c246 feat(Popover): add anchor slot (#4119)
Co-authored-by: Jakub <jakub.michalek@freelo.io>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-22 17:04:17 +02:00
Alec Armbruster
fe4e1f859d fix(icons): update loading icon (#4163)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-22 16:51:46 +02:00
Hugo Richard
3243fb88f7 fix(Input/Textarea): define model modifiers types (#4195)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-22 14:51:24 +02:00
Alireza Alibeiki
43d281f6d1 fix(CheckboxGroup/RadioGroup): variant table borders in RTL mode (#4192) 2025-05-22 14:36:44 +02:00
aydin
405304775e feat(locale): add Kyrgyz language (#4189) 2025-05-22 14:35:30 +02:00
renovate[bot]
0559beb365 chore(deps): lock file maintenance (v3) (#4200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 14:34:46 +02:00
renovate[bot]
56fc757244 chore(deps): update nuxt framework to ^3.17.4 (v3) (#4205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 13:00:32 +02:00
Benjamin Canac
9cf9f25f44 feat(NavigationMenu): add trigger type in items 2025-05-22 12:51:44 +02:00
Benjamin Canac
02363994d6 fix(NavigationMenu): remove font-medium in popover children 2025-05-22 11:56:49 +02:00
Benjamin Canac
f2682fd2ae feat(NavigationMenu): add tooltip and popover props
Resolves #4186
2025-05-21 18:39:02 +02:00
Benjamin Canac
0634a756a4 fix(Tooltip): increase padding for consistency 2025-05-21 15:24:20 +02:00
Benjamin Canac
44f536fd00 fix(NavigationMenu): only display tooltip when collapsed 2025-05-20 17:06:20 +02:00
Benjamin Canac
d0be59946b fix(NavigationMenu): incorrect hover when disabled and active 2025-05-20 17:06:20 +02:00
Benjamin Canac
1e2a10b4bd feat(NavigationMenu): handle vertical orientation with Accordion instead of Collapsible
Resolves #4072, resolves #3911
2025-05-20 17:06:20 +02:00
Benjamin Canac
3c78e2fd98 fix(NavigationMenu)!: revert new collapsible field
Reverts 2be60cddfe
2025-05-20 14:36:57 +02:00
Benjamin Canac
6887e33aae chore(deps): update @nuxt/ui-pro 2025-05-20 14:22:31 +02:00
renovate[bot]
28e869e8aa chore(deps): update all non-major dependencies (v3) (#4178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 11:05:40 +02:00
zikju
d86956e1d5 feat(locale): add Lithuanian language (#4171)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-19 10:53:02 +02:00
renovate[bot]
23e4f0ec4d chore(deps): update tailwindcss to ^4.1.7 (v3) (#4179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 10:52:00 +02:00
Muhammad Syahmi Mohd Ikram
c00f6e8cdf feat(locale): add Malay language (#4160) 2025-05-16 12:14:21 +02:00
Benjamin Canac
d29e1481f2 chore(deps): update @nuxt/ui-pro 2025-05-15 15:57:54 +02:00
Benjamin Canac
79aa161c6d docs(showcase): remove submit button 2025-05-15 15:42:25 +02:00
Benjamin Canac
94ea75f441 docs(content): update badges 2025-05-15 15:30:02 +02:00
Benjamin Canac
0c368c8ab8 chore(release): v3.1.2 2025-05-15 15:26:25 +02:00
renovate[bot]
c5796c4f82 chore(deps): update all non-major dependencies (v3) (#4129)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-15 15:10:05 +02:00
Benjamin Canac
204953b780 fix(ButtonGroup): add z-index on focused element 2025-05-15 14:30:56 +02:00
Benjamin Canac
2e4c3082a1 fix(InputNumber): handle inside button group
Resolves #4155
2025-05-15 14:26:32 +02:00
Benjamin Canac
f2fd778c0a fix(Checkbox/RadioGroup): render correct element without variant
Resolves #3998
2025-05-15 11:32:15 +02:00
Romain Hamel
d79da9d7b6 fix(FormField): block form field injection after use (#4150) 2025-05-14 19:24:47 +02:00
Benjamin Canac
a4429eee09 docs(app): add theme query in examples page 2025-05-14 19:05:06 +02:00
Benjamin Canac
0905b2b3d5 chore(components): move back item.class on link
Partial revert of #4060
2025-05-14 18:18:21 +02:00
Hugo Richard
c7fba2e0eb feat(InputNumber): add increment-disabled / decrement-disabled props (#4141) 2025-05-14 17:41:34 +02:00
Maxime Pauvert
4167f04205 docs(theme): add note about color definition (#3976)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-14 14:32:19 +02:00
Benjamin Canac
276268d311 fix(module): configure @nuxt/fonts with default weights 2025-05-14 13:18:09 +02:00
Benjamin Canac
717e35f098 docs(app): improve slug page metas 2025-05-14 12:55:58 +02:00
Benjamin Canac
459a0410ab fix(FormField): use div for error and help slots 2025-05-14 12:55:58 +02:00
J-Michalek
b9adc83e78 feat(components): add ui field in items (#4060)
Co-authored-by: Jakub <jakub.michalek@freelo.io>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-13 17:40:29 +02:00
Benjamin Canac
d7a4d029b7 fix(Slider): handle generic types 2025-05-13 17:21:42 +02:00
Benjamin Canac
3c8d6cd01d fix(Input/Textarea): handle generic types
Resolves nuxt/ui-pro#887
2025-05-13 17:18:06 +02:00
Benjamin Canac
67da90a2f6 fix(Link): consistent behavior between nuxt, vue and inertia (#4134) 2025-05-13 14:45:18 +02:00
Daniele Nicosia
894e8a61b6 feat(Badge): add square prop (#4008)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-13 14:44:23 +02:00
TribeWeb
1b6ab271ea feat(CheckboxGroup): add table variant (#3997)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-13 14:28:15 +02:00
Vachmara
0dc4678c68 fix(NavigationMenu): arrow position conflict (#4137)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-13 14:02:02 +02:00
kacijan
e86dc79e51 feat(locale): add Slovenian language (#4140)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-13 12:45:52 +02:00
renovate[bot]
35997377a6 chore(deps): update nuxt framework to ^3.17.3 (v3) (#4133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 17:17:58 +02:00
Benjamin Canac
12303a87be fix(inertia): make useAppConfig reactive
Follow-up 869c0708bd
2025-05-12 17:11:19 +02:00
renovate[bot]
f84ccddcd6 chore(deps): lock file maintenance (v3) (#4081)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: HugoRCD <hugo.richard@epitech.eu>
2025-05-12 16:42:17 +02:00
Benjamin Canac
869c0708bd fix(vue): make useAppConfig reactive
Resolves #3952

Co-Authored-By: Hugo Richard <hugo.richard@epitech.eu>
2025-05-12 15:05:25 +02:00
Benjamin Canac
c63d2f380a fix(Toaster): allow base slot override 2025-05-12 14:53:09 +02:00
Hugo Richard
92632e969e feat(Toast): add progress prop to hide progress bar (#4125)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-12 14:47:02 +02:00
renovate[bot]
f6d7994a55 chore(deps): update all non-major dependencies (v3) (#4127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 11:56:05 +02:00
Benjamin Canac
f738f68f76 docs(navigation-menu): improve 2025-05-12 11:31:23 +02:00
Benjamin Canac
17d6803329 docs(ComponentProps): improve schema collapsible display 2025-05-12 11:31:22 +02:00
Benjamin Canac
732a67aa88 docs(app): remove banner 2025-05-12 11:31:22 +02:00
renovate[bot]
bdf129fc38 chore(deps): update tailwindcss to ^4.1.6 (v3) (#4128)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 11:18:16 +02:00
242 changed files with 16461 additions and 14053 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @benjamincanac

View File

@@ -1,5 +1,81 @@
# Changelog
## [3.1.3](https://github.com/nuxt/ui/compare/v3.1.2...v3.1.3) (2025-05-26)
### ⚠ BREAKING CHANGES
* **NavigationMenu:** revert new `collapsible` field
### Features
* **locale:** add Kyrgyz language ([#4189](https://github.com/nuxt/ui/issues/4189)) ([4053047](https://github.com/nuxt/ui/commit/405304775e4b2b4e8b37a2364f3e5ee34b46036e))
* **locale:** add Lithuanian language ([#4171](https://github.com/nuxt/ui/issues/4171)) ([d86956e](https://github.com/nuxt/ui/commit/d86956e1d57482b3e98eef2d34bff13544284b0b))
* **locale:** add Malay language ([#4160](https://github.com/nuxt/ui/issues/4160)) ([c00f6e8](https://github.com/nuxt/ui/commit/c00f6e8cdfd88eeba58812b78d94a2326c13f164))
* **locale:** add Mongolian language ([#4214](https://github.com/nuxt/ui/issues/4214)) ([44ea02c](https://github.com/nuxt/ui/commit/44ea02c0d64322ef0cfda63b234369c00d3d0180))
* **Modal/Slideover:** add `after:enter` event ([#4187](https://github.com/nuxt/ui/issues/4187)) ([d9e9fea](https://github.com/nuxt/ui/commit/d9e9fea35e4b22d68324c9e85b3aa221a7987d0f))
* **NavigationMenu:** add `tooltip` and `popover` props ([f2682fd](https://github.com/nuxt/ui/commit/f2682fd2ae8abb7807977727fc22ef34cb5752e5)), closes [#4186](https://github.com/nuxt/ui/issues/4186)
* **NavigationMenu:** add `trigger` type in items ([9cf9f25](https://github.com/nuxt/ui/commit/9cf9f25f4424447691e03e9034155d1541badd43))
* **NavigationMenu:** handle `vertical` orientation with Accordion instead of Collapsible ([1e2a10b](https://github.com/nuxt/ui/commit/1e2a10b4bdebaef12316ac60f98a956dad21c1ec)), closes [#4072](https://github.com/nuxt/ui/issues/4072) [#3911](https://github.com/nuxt/ui/issues/3911)
* **Popover:** add `anchor` slot ([#4119](https://github.com/nuxt/ui/issues/4119)) ([473513c](https://github.com/nuxt/ui/commit/473513c2460d4329d7d2e0a0ea69bf1310a072d1))
### Bug Fixes
* **CheckboxGroup/RadioGroup:** variant `table` borders in RTL mode ([#4192](https://github.com/nuxt/ui/issues/4192)) ([43d281f](https://github.com/nuxt/ui/commit/43d281f6d1d8b0017ed61d929c5e311fb5b03447))
* **CommandPalette:** add `presentation` role to viewport ([2ba94db](https://github.com/nuxt/ui/commit/2ba94db09e1ba86020d5d289f1ca1e24ef706299))
* **ContextMenu/DropdownMenu:** wrap groups in a viewport ([dcf34a7](https://github.com/nuxt/ui/commit/dcf34a7ac236b96b1302ec2eae155b8f2d3784ef)), closes [#3315](https://github.com/nuxt/ui/issues/3315)
* **Drawer:** improve title & description accessibility ([41087d4](https://github.com/nuxt/ui/commit/41087d4c9569eb00c04bd748e055cd151c2f762c)), closes [#4199](https://github.com/nuxt/ui/issues/4199)
* **icons:** update `loading` icon ([#4163](https://github.com/nuxt/ui/issues/4163)) ([fe4e1f8](https://github.com/nuxt/ui/commit/fe4e1f859d42aa3c32bb7b75302e84a280abe525))
* **Input/Textarea:** define model modifiers types ([#4195](https://github.com/nuxt/ui/issues/4195)) ([3243fb8](https://github.com/nuxt/ui/commit/3243fb88f71c5475824bfdc4d7c4f303b2d6790b))
* **InputMenu/Select/SelectMenu:** manual viewport to display scrollbars ([f95abf8](https://github.com/nuxt/ui/commit/f95abf8d1d7b9149e400d7dc6f96f93f5154da7a)), closes [#4069](https://github.com/nuxt/ui/issues/4069)
* **NavigationMenu:** incorrect hover when disabled and active ([d0be599](https://github.com/nuxt/ui/commit/d0be59946bfe30c79a6f75476385ab8538aa51b8))
* **NavigationMenu:** only display `tooltip` when collapsed ([44f536f](https://github.com/nuxt/ui/commit/44f536fd0034facb3550d910fae71d4f9442ed19))
* **NavigationMenu:** remove `font-medium` in popover children ([0236399](https://github.com/nuxt/ui/commit/02363994d66d3c2d11b9913f31167fa25f5c5de2))
* **NavigationMenu:** revert new `collapsible` field ([3c78e2f](https://github.com/nuxt/ui/commit/3c78e2fd983f19b5cec65b4a94a8a8b14e548e5e))
* **Textarea:** missing imports ([#4207](https://github.com/nuxt/ui/issues/4207)) ([6aab62e](https://github.com/nuxt/ui/commit/6aab62ec30e266c5f0da0cd24aefbb7c53f447ac))
* **theme:** define `old-neutral` color as static ([#4193](https://github.com/nuxt/ui/issues/4193)) ([dae9f0b](https://github.com/nuxt/ui/commit/dae9f0b8631b3b9fb60ef47753f7aded0c36c4a2))
* **Tooltip:** increase padding for consistency ([0634a75](https://github.com/nuxt/ui/commit/0634a756a496f5131841abafd218ae7e4aaa61e5))
## [3.1.2](https://github.com/nuxt/ui/compare/v3.1.1...v3.1.2) (2025-05-15)
### Features
* **Badge:** add `square` prop ([#4008](https://github.com/nuxt/ui/issues/4008)) ([894e8a6](https://github.com/nuxt/ui/commit/894e8a61b6fea3618fc863bd77678385e9d021c2))
* **CheckboxGroup:** add `table` variant ([#3997](https://github.com/nuxt/ui/issues/3997)) ([1b6ab27](https://github.com/nuxt/ui/commit/1b6ab271ea3875a7c77ffe9367c7c341083dd53c))
* **components:** add `ui` field in items ([#4060](https://github.com/nuxt/ui/issues/4060)) ([b9adc83](https://github.com/nuxt/ui/commit/b9adc83e787db02507e6e7bb1aabc684eccc197b))
* **InputNumber:** add `increment-disabled` / `decrement-disabled` props ([#4141](https://github.com/nuxt/ui/issues/4141)) ([c7fba2e](https://github.com/nuxt/ui/commit/c7fba2e0ebfb7153f3bfb727165d653bbd3dbe54))
* **locale:** add Slovenian language ([#4140](https://github.com/nuxt/ui/issues/4140)) ([e86dc79](https://github.com/nuxt/ui/commit/e86dc79e51b2773a77ada5f12d4f0964fbc83354))
* **NavigationMenu:** add `collapsible` field in items ([2be60cd](https://github.com/nuxt/ui/commit/2be60cddfe10fd1e2466900fd53e21ee0c877227)), closes [#3353](https://github.com/nuxt/ui/issues/3353) [#3911](https://github.com/nuxt/ui/issues/3911)
* **NavigationMenu:** handle `tooltip` in items ([46c2987](https://github.com/nuxt/ui/commit/46c2987ebfd30b2b071a96a745b7270e852e96de)), closes [#4050](https://github.com/nuxt/ui/issues/4050)
* **Slider:** handle `tooltip` around thumbs ([d140acc](https://github.com/nuxt/ui/commit/d140acc608c6ae11c0a0531fe443588776ea7807)), closes [#1469](https://github.com/nuxt/ui/issues/1469)
* **Toast:** add `progress` prop to hide progress bar ([#4125](https://github.com/nuxt/ui/issues/4125)) ([92632e9](https://github.com/nuxt/ui/commit/92632e969eaa11521a166e50e346753929b7f523))
### Bug Fixes
* **Badge/Button:** handle zero value in label correctly ([#4108](https://github.com/nuxt/ui/issues/4108)) ([f244d15](https://github.com/nuxt/ui/commit/f244d15b96d97cd8ba34ba9c18f23965e17e3cef))
* **ButtonGroup:** add `z-index` on focused element ([204953b](https://github.com/nuxt/ui/commit/204953b780bde08dbfde230fc8887674449227b7))
* **Calendar:** wrong color for today date with `neutral` color ([7d51a9e](https://github.com/nuxt/ui/commit/7d51a9e479cb6105ea37759c5cd67ff9f7702c49)), closes [#4084](https://github.com/nuxt/ui/issues/4084) [#3629](https://github.com/nuxt/ui/issues/3629)
* **Checkbox/RadioGroup:** render correct element without `variant` ([f2fd778](https://github.com/nuxt/ui/commit/f2fd778c0a604f2d65aec9f3fe2d54b6d4e8c3a2)), closes [#3998](https://github.com/nuxt/ui/issues/3998)
* **CheckboxGroup:** relative `UCheckbox` import ([7551a85](https://github.com/nuxt/ui/commit/7551a85ad2d92b59e2909396affb862403d5b27a)), closes [#4090](https://github.com/nuxt/ui/issues/4090)
* **ColorPicker:** make thumb touch draggable ([#4101](https://github.com/nuxt/ui/issues/4101)) ([cc20a26](https://github.com/nuxt/ui/commit/cc20a26f07268d19119ab4c7c254033143bb63f4))
* **components:** `class` should have priority over `ui` prop ([e6e510b](https://github.com/nuxt/ui/commit/e6e510b848d995a286a51d50a120d67483e11232))
* **FormField:** block form field injection after use ([#4150](https://github.com/nuxt/ui/issues/4150)) ([d79da9d](https://github.com/nuxt/ui/commit/d79da9d7b60c9972af64acd8e6eef4ae7d6bc3eb))
* **FormField:** use `div` for `error` and `help` slots ([459a041](https://github.com/nuxt/ui/commit/459a0410ab729fde60865e84632b36903465f57e))
* **inertia:** link always render as anchor tag ([#3989](https://github.com/nuxt/ui/issues/3989)) ([e81464a](https://github.com/nuxt/ui/commit/e81464a43ede4e63ce3dc92429bbfef48614f731))
* **inertia:** make `useAppConfig` reactive ([12303a8](https://github.com/nuxt/ui/commit/12303a87be62dae84ef774e3a9795deb0ac90cc7))
* **Input/Textarea:** handle generic types ([3c8d6cd](https://github.com/nuxt/ui/commit/3c8d6cd01dfafed5844c376f52adbdda0c814420)), closes [nuxt/ui-pro#887](https://github.com/nuxt/ui-pro/issues/887)
* **InputNumber:** handle inside button group ([2e4c308](https://github.com/nuxt/ui/commit/2e4c3082a1e66fa597086dc3431fec37fa29ef62)), closes [#4155](https://github.com/nuxt/ui/issues/4155)
* **Link:** consistent behavior between nuxt, vue and inertia ([#4134](https://github.com/nuxt/ui/issues/4134)) ([67da90a](https://github.com/nuxt/ui/commit/67da90a2f638124f640c4271d3376c5ff3fab6a1))
* **module:** configure `@nuxt/fonts` with default weights ([276268d](https://github.com/nuxt/ui/commit/276268d311f57715cec47bc600a0ccc3d3885682))
* **NavigationMenu:** arrow position conflict ([#4137](https://github.com/nuxt/ui/issues/4137)) ([0dc4678](https://github.com/nuxt/ui/commit/0dc4678c68e4b500be49c38336dc75b73843e38d))
* **Select:** support more primitive types in `value` field ([#4105](https://github.com/nuxt/ui/issues/4105)) ([09b4699](https://github.com/nuxt/ui/commit/09b4699aeadaa195ea081509f8e237bb2c346238))
* **Slider:** handle generic types ([d7a4d02](https://github.com/nuxt/ui/commit/d7a4d029b77d2dfa0b8efcd2755d482fa5e31fd3))
* **Stepper:** use `div` tag for `title` & `description` ([a57844e](https://github.com/nuxt/ui/commit/a57844e41676c13ed1af861424961b88cee7b4da)), closes [#4096](https://github.com/nuxt/ui/issues/4096)
* **Tabs:** prevent trigger truncate without parent width ([06e5689](https://github.com/nuxt/ui/commit/06e5689da80b36205d0548d5d6b58510938e4a6e)), closes [#4056](https://github.com/nuxt/ui/issues/4056)
* **Tabs:** set `focus:outline-none` with `link` variant ([999a0f8](https://github.com/nuxt/ui/commit/999a0f84671fad20fa3dc50c6774af2e0200b32e))
* **templates:** dont write unused variants in theme files ([d3df3bb](https://github.com/nuxt/ui/commit/d3df3bb929fe6732f27b182d1664213884a662ec))
* **Toaster:** allow `base` slot override ([c63d2f3](https://github.com/nuxt/ui/commit/c63d2f380aac16f1d1e812516df3dca7fa7c8034))
* **vue:** make `useAppConfig` reactive ([869c070](https://github.com/nuxt/ui/commit/869c0708bd351c7be44e5e430c348b19dd316db9)), closes [#3952](https://github.com/nuxt/ui/issues/3952)
## [3.1.1](https://github.com/nuxt/ui/compare/v3.1.0...v3.1.1) (2025-05-02)
### Features

View File

@@ -53,7 +53,7 @@ provide('navigation', mappedNavigation)
<NuxtLoadingIndicator color="var(--ui-primary)" :height="2" />
<template v-if="!route.path.startsWith('/examples')">
<Banner />
<!-- <Banner /> -->
<Header :links="links" />
</template>

View File

@@ -38,7 +38,7 @@ const schemaProps = computed(() => {
</script>
<template>
<ProseCollapsible v-if="schemaProps?.length" class="mt-1">
<ProseCollapsible v-if="schemaProps?.length" class="mt-1 mb-0">
<ProseUl>
<ProseLi v-for="schemaProp in schemaProps" :key="schemaProp.name">
<HighlightInlineType :type="`${schemaProp.name}${schemaProp.required === false ? '?' : ''}: ${schemaProp.type}`" />

View File

@@ -25,7 +25,10 @@ function getEmojiFlag(locale: string): string {
kk: 'kz', // Kazakh -> Kazakhstan
km: 'kh', // Khmer -> Cambodia
ko: 'kr', // Korean -> South Korea
ky: 'kg', // Kyrgyz -> Kyrgyzstan
ms: 'my', // Malay -> Malaysia
nb: 'no', // Norwegian Bokmål -> Norway
sl: 'si', // Slovenian -> Slovenia
sv: 'se', // Swedish -> Sweden
uk: 'ua', // Ukrainian -> Ukraine
ur: 'pk', // Urdu -> Pakistan

View File

@@ -28,7 +28,7 @@ const items = [
</template>
<template #refresh-trailing>
<UIcon v-if="loading" name="i-lucide-refresh-cw" class="shrink-0 size-5 text-primary animate-spin" />
<UIcon v-if="loading" name="i-lucide-loader-circle" class="shrink-0 size-5 text-primary animate-spin" />
</template>
</UContextMenu>
</template>

View File

@@ -3,7 +3,7 @@ const open = ref(false)
</script>
<template>
<UDrawer v-model:open="open" :dismissible="false" :ui="{ header: 'flex items-center justify-between' }">
<UDrawer v-model:open="open" :dismissible="false" :handle="false" :ui="{ header: 'flex items-center justify-between' }">
<UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
<template #header>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
const open = ref(false)
</script>
<template>
<UDrawer
v-model:open="open"
:dismissible="false"
:overlay="false"
:handle="false"
:modal="false"
:ui="{ header: 'flex items-center justify-between' }"
>
<UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
<template #header>
<h2 class="text-highlighted font-semibold">
Drawer non-dismissible
</h2>
<UButton color="neutral" variant="ghost" icon="i-lucide-x" @click="open = false" />
</template>
<template #body>
<Placeholder class="h-48" />
</template>
</UDrawer>
</template>

View File

@@ -15,6 +15,9 @@ const schema = z.object({
select: z.string().refine(value => value === 'option-2', {
message: 'Select Option 2'
}),
selectMultiple: z.array(z.string()).refine(values => values.includes('option-2'), {
message: 'Include Option 2'
}),
selectMenu: z.any().refine(option => option?.value === 'option-2', {
message: 'Select Option 2'
}),
@@ -81,6 +84,10 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
<USelect v-model="state.select" :items="items" class="w-full" />
</UFormField>
<UFormField name="selectMultiple" label="Select (Multiple)">
<USelect v-model="state.selectMultiple" multiple :items="items" class="w-full" />
</UFormField>
<UFormField name="selectMenu" label="Select Menu">
<USelectMenu v-model="state.selectMenu" :items="items" class="w-full" />
</UFormField>

View File

@@ -10,7 +10,7 @@ const domain = ref(domains[0])
v-model="value"
placeholder="nuxt"
:ui="{
base: 'pl-[57px]',
base: 'pl-14.5',
leading: 'pointer-events-none'
}"
>

View File

@@ -10,8 +10,8 @@ const open = ref(false)
<Placeholder class="h-48" />
</template>
<template #footer>
<UButton label="Cancel" color="neutral" variant="outline" @click="open = false" />
<template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<UButton label="Submit" color="neutral" />
</template>
</UModal>

View File

@@ -0,0 +1,19 @@
<script lang="ts" setup>
const open = ref(false)
</script>
<template>
<UPopover
v-model:open="open"
:dismissible="false"
:ui="{ content: 'w-(--reka-popper-anchor-width) p-4' }"
>
<template #anchor>
<UInput placeholder="Focus to open" @focus="open = true" @blur="open = false" />
</template>
<template #content>
<Placeholder class="w-full aspect-square" />
</template>
</UPopover>
</template>

View File

@@ -10,8 +10,8 @@ const open = ref(false)
<Placeholder class="h-full" />
</template>
<template #footer>
<UButton label="Cancel" color="neutral" variant="outline" @click="open = false" />
<template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<UButton label="Submit" color="neutral" />
</template>
</USlideover>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'
const items: TimelineItem[] = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
icon: 'i-lucide-code',
value: 'development'
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
icon: 'i-lucide-check-circle',
value: 'deployment'
}]
</script>
<template>
<UTimeline
:items="items"
:ui="{ item: 'even:flex-row-reverse even:-translate-x-[calc(100%-2rem)] even:text-right' }"
:default-value="2"
class="w-full translate-x-[calc(50%-2rem)]"
/>
</template>

View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'
const items = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
subtitle: 'Project Initiation',
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
icon: 'i-lucide-code',
value: 'development',
slot: 'development' as const,
developers: [
{
src: 'https://github.com/J-Michalek.png'
}, {
src: 'https://github.com/benjamincanac.png'
}
]
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
description: 'QA testing and performance optimization. Deployed the application to production.',
icon: 'i-lucide-check-circle',
value: 'deployment'
}] satisfies TimelineItem[]
</script>
<template>
<UTimeline :items="items" :default-value="2" class="w-96">
<template #development-title="{ item }">
<div class="flex items-center gap-1">
<span>{{ item.title }}</span>
<UAvatarGroup size="2xs">
<UAvatar v-for="(developer, index) of item.developers" :key="index" v-bind="developer" />
</UAvatarGroup>
</div>
</template>
</UTimeline>
</template>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'
const items: TimelineItem[] = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
icon: 'i-lucide-code',
value: 'development'
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
description: 'QA testing and performance optimization. Deployed the application to production.',
icon: 'i-lucide-check-circle',
value: 'deployment'
}]
const active = ref(0)
// Note: This is for demonstration purposes only. Don't do this at home.
onMounted(() => {
setInterval(() => {
active.value = (active.value + 1) % items.length
}, 2000)
})
</script>
<template>
<UTimeline v-model="active" :items="items" class="w-96" />
</template>

View File

@@ -0,0 +1,60 @@
<script lang="ts" setup>
import type { TimelineItem } from '@nuxt/ui'
import { useTimeAgo } from '@vueuse/core'
const items = [{
username: 'J-Michalek',
date: '2025-05-24T14:58:55Z',
action: 'opened this',
avatar: {
src: 'https://github.com/J-Michalek.png'
}
}, {
username: 'J-Michalek',
date: '2025-05-26T19:30:14+02:00',
action: 'marked this pull request as ready for review',
icon: 'i-lucide-check-circle'
}, {
username: 'benjamincanac',
date: '2025-05-27T11:01:20Z',
action: 'commented on this',
description: 'I\'ve made a few changes, let me know what you think! Basically I updated the design, removed unnecessary divs, used Avatar component for the indicator since it supports icon already.',
avatar: {
src: 'https://github.com/benjamincanac.png'
}
}, {
username: 'J-Michalek',
date: '2025-05-27T11:01:20Z',
action: 'commented on this',
description: 'Looks great! Good job on cleaning it up.',
avatar: {
src: 'https://github.com/J-Michalek.png'
}
}, {
username: 'benjamincanac',
date: '2025-05-27T11:01:20Z',
action: 'merged this',
icon: 'i-lucide-git-merge'
}] satisfies TimelineItem[]
</script>
<template>
<UTimeline
:items="items"
size="xs"
class="w-96"
:ui="{
date: 'float-end ms-1',
description: 'px-3 py-2 ring ring-default mt-2 rounded-md text-default'
}"
>
<template #title="{ item }">
<span>{{ item.username }}</span>
<span class="font-normal text-muted">&nbsp;{{ item.action }}</span>
</template>
<template #date="{ item }">
{{ useTimeAgo(new Date(item.date)) }}
</template>
</UTimeline>
</template>

View File

@@ -59,7 +59,7 @@ provide('navigation', mappedNavigation)
<UApp>
<NuxtLoadingIndicator color="#FFF" />
<Banner />
<!-- <Banner /> -->
<Header :links="links" />

View File

@@ -65,13 +65,17 @@ if (!import.meta.prerender) {
})
}
const type = page.value?.path.includes('components') ? 'Vue Component ' : page.value?.path.includes('composables') ? 'Vue Composable ' : ''
const title = page.value?.navigation?.title ? page.value.navigation.title : page.value?.title
const prefix = page.value?.path.includes('components') || page.value?.path.includes('composables') ? 'Vue ' : ''
const suffix = page.value?.path.includes('components') ? 'Component ' : page.value?.path.includes('composables') ? 'Composable ' : ''
const description = page.value?.description
useSeoMeta({
titleTemplate: `%s ${type}- Nuxt UI ${page.value.module === 'ui-pro' ? 'Pro' : ''} ${page.value.framework === 'vue' ? ' for Vue' : ''}`,
title: page.value.navigation?.title ? page.value.navigation.title : page.value.title,
ogTitle: `${page.value.navigation?.title ? page.value.navigation.title : page.value.title} ${type}- Nuxt UI ${page.value.module === 'ui-pro' ? 'Pro' : ''} ${page.value.framework === 'vue' ? ' for Vue' : ''}`,
description: page.value.description,
ogDescription: page.value.description
titleTemplate: `${prefix}%s ${suffix}- Nuxt UI ${page.value?.module === 'ui-pro' ? 'Pro' : ''} ${page.value?.framework === 'vue' ? ' for Vue' : ''}`,
title,
ogTitle: `${prefix}${title} ${suffix}- Nuxt UI ${page.value?.module === 'ui-pro' ? 'Pro' : ''} ${page.value?.framework === 'vue' ? ' for Vue' : ''}`,
description,
ogDescription: description
})
if (route.path.startsWith('/components')) {

View File

@@ -1,8 +1,20 @@
<script setup lang="ts">
const route = useRoute()
const colorMode = useColorMode()
const appConfig = useAppConfig()
const name = route.params.slug?.[0]
if (route.query.theme) {
colorMode.preference = route.query.theme === 'light' ? 'light' : 'dark'
}
if (route.query.neutral) {
appConfig.ui.colors.neutral = route.query.neutral as string
}
if (route.query.primary) {
appConfig.ui.colors.primary = route.query.primary as string
}
const width = computed(() => route.query.width && Number.parseInt(route.query.width as string) > 0 ? `${Number.parseInt(route.query.width as string) - 2}px` : '864px')
</script>

View File

@@ -24,32 +24,41 @@ onMounted(async () => {
const nuxtWordPosition = document.querySelector('#nuxt')?.getBoundingClientRect()
const initialScrollX = window.scrollX
const initialScrollY = window.scrollY
if (figmaWordPosition && nuxtWordPosition) {
animate('#cursor1', { left: Math.round(Math.random() * window.outerWidth), top: Math.round(Math.random() * window.outerHeight) }, { duration: 0.1 })
.then(() => animate('#cursor1', { opacity: 1 }, { duration: 0.3 }))
.then(() => {
return animate('#cursor1', {
left: Math.round(figmaWordPosition.left + initialScrollX + figmaWordPosition.width / 2),
top: Math.round(figmaWordPosition.top + initialScrollY - figmaWordPosition.height / 4)
}, { duration: 1.5, delay: 0.2, ease: 'easeInOut' })
})
.then(() => animate('#cursor1', { scale: 0.8 }, { duration: 0.1, ease: 'easeOut' }))
.then(() => animate('#cursor1', { scale: 1 }, { duration: 0.1, ease: 'easeOut' }))
.then(() => animate('#figma', { color: 'var(--ui-info)' }, { duration: 0.3, ease: 'easeOut' }))
.then(() => animate('#cursor1', { left: Math.round(figmaWordPosition.left + initialScrollX + figmaWordPosition.width), top: Math.round(figmaWordPosition.top + initialScrollY) }, { duration: 0.6, ease: 'easeInOut' }))
animate('#cursor2', { left: Math.round(Math.random() * window.outerWidth), top: Math.round(Math.random() * window.outerHeight) }, { duration: 0.1, delay: 0.6 })
.then(() => animate('#cursor2', { opacity: 1 }, { duration: 0.3 }))
.then(() => {
return animate('#cursor2', {
left: Math.round(nuxtWordPosition.left + initialScrollX + nuxtWordPosition.width / 2),
top: Math.round(nuxtWordPosition.top + initialScrollY - nuxtWordPosition.height / 4)
}, { duration: 1.5, delay: 0.2, ease: 'easeInOut' })
})
.then(() => animate('#cursor2', { scale: 0.8 }, { duration: 0.1, ease: 'easeOut' }))
.then(() => animate('#cursor2', { scale: 1 }, { duration: 0.1, ease: 'easeOut' }))
.then(() => animate('#nuxt', { color: 'var(--ui-success)' }, { duration: 0.3, ease: 'easeOut' }))
.then(() => animate('#cursor2', { left: Math.round(nuxtWordPosition.left + initialScrollX + nuxtWordPosition.width), top: Math.round(nuxtWordPosition.top + initialScrollY) }, { duration: 0.6, ease: 'easeInOut' }))
if (figmaWordPosition && nuxtWordPosition) {
const cursor1Sequence = async () => {
await animate('#cursor1', { left: Math.round(Math.random() * window.outerWidth), top: Math.round(Math.random() * window.outerHeight) }, { duration: 0.1 }).finished
await animate('#cursor1', { opacity: 1 }, { duration: 0.3 }).finished
await animate('#cursor1', {
left: Math.round(figmaWordPosition.left + initialScrollX + figmaWordPosition.width / 2),
top: Math.round(figmaWordPosition.top + initialScrollY - figmaWordPosition.height / 4)
}, { duration: 1.5, delay: 0.2, ease: 'easeInOut' }).finished
await animate('#cursor1', { scale: 0.8 }, { duration: 0.1, ease: 'easeOut' }).finished
await animate('#cursor1', { scale: 1 }, { duration: 0.1, ease: 'easeOut' }).finished
await animate('#figma', { color: 'var(--ui-info)' }, { duration: 0.3, ease: 'easeOut' }).finished
await animate('#cursor1', {
left: Math.round(figmaWordPosition.left + initialScrollX + figmaWordPosition.width),
top: Math.round(figmaWordPosition.top + initialScrollY)
}, { duration: 0.6, ease: 'easeInOut' }).finished
}
const cursor2Sequence = async () => {
await animate('#cursor2', { left: Math.round(Math.random() * window.outerWidth), top: Math.round(Math.random() * window.outerHeight) }, { duration: 0.1, delay: 0.6 }).finished
await animate('#cursor2', { opacity: 1 }, { duration: 0.3 }).finished
await animate('#cursor2', {
left: Math.round(nuxtWordPosition.left + initialScrollX + nuxtWordPosition.width / 2),
top: Math.round(nuxtWordPosition.top + initialScrollY - nuxtWordPosition.height / 4)
}, { duration: 1.5, delay: 0.2, ease: 'easeInOut' }).finished
await animate('#cursor2', { scale: 0.8 }, { duration: 0.1, ease: 'easeOut' }).finished
await animate('#cursor2', { scale: 1 }, { duration: 0.1, ease: 'easeOut' }).finished
await animate('#nuxt', { color: 'var(--ui-success)' }, { duration: 0.3, ease: 'easeOut' }).finished
await animate('#cursor2', {
left: Math.round(nuxtWordPosition.left + initialScrollX + nuxtWordPosition.width),
top: Math.round(nuxtWordPosition.top + initialScrollY)
}, { duration: 0.6, ease: 'easeInOut' }).finished
}
await Promise.all([cursor1Sequence(), cursor2Sequence()])
}
})
</script>

View File

@@ -67,17 +67,6 @@ defineOgImageComponent('Docs', {
</li>
</ul>
</div>
<div class="flex justify-center -mb-[36px]">
<UButton
label="Submit your project"
trailing-icon="i-lucide-plus"
color="neutral"
size="lg"
to="https://github.com/nuxt/ui/edit/v3/docs/content/showcase.yml"
target="_blank"
/>
</div>
</UPageHero>
</UMain>
</template>

View File

@@ -229,6 +229,10 @@ export default defineConfig({
::
::caution
When configuring your theme colors, you must use either color names from the [default Tailwind palette](https://tailwindcss.com/docs/colors) (like 'blue', 'green', etc.) or reference custom colors that you've previously defined in your [CSS file](#theme).
::
### Extend colors
::framework-only

View File

@@ -225,7 +225,7 @@ pnpm run test:vue # for Vue
```
::tip
If you have to update the snapshots, press `u` when running the tests.
If you have to update the snapshots, press `u` after the tests have finished running.
::
### Commit Conventions

View File

@@ -62,7 +62,7 @@ Update an overlay using its `id`
- `id`: The identifier of the overlay
- `props`: An object of props to update on the rendered component.
### `unMount(id: symbol): void`
### `unmount(id: symbol): void`
Removes the overlay from the DOM using its `id`

View File

@@ -23,6 +23,8 @@ Use the `items` prop as an array of objects with the following properties:
- `value?: string`{lang="ts-type"}
- `disabled?: boolean`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, header?: ClassNameValue, trigger?: ClassNameValue, leadingIcon?: ClassNameValue, label?: ClassNameValue, trailingIcon?: ClassNameValue, content?: ClassNameValue, body?: ClassNameValue }`{lang="ts-type"}
::component-code
---

View File

@@ -16,8 +16,9 @@ Use the `items` prop as an array of objects with the following properties:
- `label?: string`{lang="ts-type"}
- `icon?: string`{lang="ts-type"}
- `avatar?: AvatarProps`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, link?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLeadingAvatar?: ClassNameValue, linkLabel?: ClassNameValue, separator?: ClassNameValue, separatorIcon?: ClassNameValue }`{lang="ts-type"}
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.

View File

@@ -258,13 +258,13 @@ This also works with the [Form](/components/form) component.
### Loading Icon
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-refresh-cw`.
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
::component-code
---
props:
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
slots:
default: Button
---

View File

@@ -27,6 +27,11 @@ class: 'p-8'
---
::
You can also pass an array of objects with the following properties:
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue }`{lang="ts-type"}
You can control how many items are visible by using the [`basis`](https://tailwindcss.com/docs/flex-basis) / [`width`](https://tailwindcss.com/docs/width) utility classes on the `item`:
::component-example

View File

@@ -49,6 +49,8 @@ You can also pass an array of objects with the following properties:
- `description?: string`{lang="ts-type"}
- [`value?: string`{lang="ts-type"}](#value-key)
- `disabled?: boolean`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, base?: ClassNameValue, 'indicator'?: ClassNameValue, icon?: ClassNameValue, wrapper?: ClassNameValue, label?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"}
::component-code
---
@@ -199,6 +201,7 @@ items:
variant:
- list
- card
- table
props:
color: 'primary'
variant: 'card'
@@ -229,6 +232,7 @@ items:
variant:
- list
- card
- table
props:
size: 'xl'
variant: 'list'
@@ -259,6 +263,7 @@ items:
variant:
- list
- card
- table
props:
orientation: 'horizontal'
variant: 'list'
@@ -293,6 +298,7 @@ items:
variant:
- list
- card
- table
props:
indicator: 'end'
variant: 'card'

View File

@@ -53,6 +53,8 @@ Each group contains an `items` array of objects that define the commands. Each i
- `disabled?: boolean`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `onSelect?(e?: Event): void`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelPrefix?: ClassNameValue, itemLabelBase?: ClassNameValue, itemLabelSuffix?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue, itemTrailingHighlightedIcon?: ClassNameValue, itemTrailingIcon?: ClassNameValue,}`{lang="ts-type"}
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
@@ -277,7 +279,7 @@ props:
### Loading Icon
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-refresh-cw`.
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
::component-code
---
@@ -293,7 +295,7 @@ class: '!p-0'
props:
autofocus: false
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
groups:
- id: 'apps'
items:

View File

@@ -28,11 +28,12 @@ Use the `items` prop as an array of objects with the following properties:
- [`color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{lang="ts-type"}](#with-color-items)
- [`checked?: boolean`{lang="ts-type"}](#with-checkbox-items)
- `disabled?: boolean`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `onSelect?(e: Event): void`{lang="ts-type"}
- [`onUpdateChecked?(checked: boolean): void`{lang="ts-type"}](#with-checkbox-items)
- `children?: ContextMenuItem[] | ContextMenuItem[][]`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelExternalIcon?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue }`{lang="ts-type"}
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.

View File

@@ -291,7 +291,7 @@ In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts),
This allows you to move the trigger outside of the Drawer or remove it entirely.
::
### Prevent closing
### Disable dismissal
Set the `dismissible` prop to `false` to prevent the Drawer from being closed when clicking outside of it or pressing escape.
@@ -306,6 +306,17 @@ name: 'drawer-dismissible-example'
In this example, the `header` slot is used to add a close button which is not done by default.
::
### With interactive background
Set the `overlay` and `modal` props to `false` alongside the `dismissible` prop to make the Drawer's background interactive without closing the Drawer.
::component-example
---
prettier: true
name: 'drawer-modal-example'
---
::
### Responsive drawer
You can render a [Modal](/components/modal) component on desktop and a Drawer on mobile for example.

View File

@@ -28,11 +28,12 @@ Use the `items` prop as an array of objects with the following properties:
- [`color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{lang="ts-type"}](#with-color-items)
- [`checked?: boolean`{lang="ts-type"}](#with-checkbox-items)
- `disabled?: boolean`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `onSelect?(e: Event): void`{lang="ts-type"}
- [`onUpdateChecked?(checked: boolean): void`{lang="ts-type"}](#with-checkbox-items)
- `children?: DropdownMenuItem[] | DropdownMenuItem[][]`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelExternalIcon?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue }`{lang="ts-type"}
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.

View File

@@ -55,6 +55,8 @@ You can also pass an array of objects with the following properties:
- [`chip?: ChipProps`{lang="ts-type"}](#with-chip-in-items)
- `disabled?: boolean`{lang="ts-type"}
- `onSelect?(e: Event): void`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { tagsItem?: ClassNameValue, tagsItemText?: ClassNameValue, tagsItemDelete?: ClassNameValue, tagsItemDeleteIcon?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{lang="ts-type"}
::component-code
---
@@ -516,7 +518,7 @@ props:
### Loading Icon
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-refresh-cw`.
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
::component-code
---
@@ -530,7 +532,7 @@ external:
props:
modelValue: 'Backlog'
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
items:
- Backlog
- Todo
@@ -610,7 +612,7 @@ props:
---
::
### With icons in items
### With icon in items
You can use the `icon` property to display an [Icon](/components/icon) inside the items.

View File

@@ -172,7 +172,7 @@ props:
### Loading Icon
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-refresh-cw`.
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
::component-code
---
@@ -180,7 +180,7 @@ ignore:
- placeholder
props:
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
placeholder: 'Search...'
---
::

View File

@@ -274,7 +274,7 @@ In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts),
This allows you to move the trigger outside of the Modal or remove it entirely.
::
### Prevent closing
### Disable dismissal
Set the `dismissible` prop to `false` to prevent the Modal from being closed when clicking outside of it or pressing escape. A `close:prevent` event will be emitted when the user tries to close it.
@@ -305,13 +305,13 @@ slots:
### Programmatic usage
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Modal programatically.
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Modal programmatically.
::warning
Make sure to wrap your app with the [`App`](/components/app) component which uses the [`OverlayProvider`](https://github.com/nuxt/ui/blob/v3/src/runtime/components/OverlayProvider.vue) component.
::
First, create a modal component that will be opened programatically:
First, create a modal component that will be opened programmatically:
::component-example
---

View File

@@ -23,16 +23,16 @@ Use the `items` prop as an array of objects with the following properties:
- `badge?: string | number | BadgeProps`{lang="ts-type"}
- `tooltip?: TooltipProps`{lang="ts-type"}
- `trailingIcon?: string`{lang="ts-type"}
- `type?: 'label' | 'link'`{lang="ts-type"}
- `collapsible?: boolean`{lang="ts-type"}
- `type?: 'label' | 'trigger' | 'link'`{lang="ts-type"}
- `defaultOpen?: boolean`{lang="ts-type"}
- `open?: boolean`{lang="ts-type"}
- `value?: string`{lang="ts-type"}
- `disabled?: boolean`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `onSelect?(e: Event): void`{lang="ts-type"}
- `children?: NavigationMenuChildItem[]`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { linkLeadingAvatarSize?: ClassNameValue, linkLeadingAvatar?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLabel?: ClassNameValue, linkLabelExternalIcon?: ClassNameValue, linkTrailing?: ClassNameValue, linkTrailingBadgeSize?: ClassNameValue, linkTrailingBadge?: ClassNameValue, linkTrailingIcon?: ClassNameValue, label?: ClassNameValue, link?: ClassNameValue, content?: ClassNameValue, childList?: ClassNameValue, childLabel?: ClassNameValue, childItem?: ClassNameValue, childLink?: ClassNameValue, childLinkIcon?: ClassNameValue, childLinkWrapper?: ClassNameValue, childLinkLabel?: ClassNameValue, childLinkLabelExternalIcon?: ClassNameValue, childLinkDescription?: ClassNameValue }`{lang="ts-type"}
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
@@ -134,8 +134,8 @@ Each item can take a `children` array of objects with the following properties t
- `label: string`
- `description?: string`
- `icon?: string`
- `class?: any`
- `onSelect?(e: Event): void`
- `class?: any`
::
@@ -144,7 +144,7 @@ Each item can take a `children` array of objects with the following properties t
Use the `orientation` prop to change the orientation of the NavigationMenu.
::note
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children. You can control the open state of each item using the `open` and `defaultOpen` properties. You can also use the `collapsible` property to control if the item is collapsible.
When orientation is `vertical`, an [Accordion](/components/accordion) component is used to display each group. You can control the open state of each item using the `open` and `defaultOpen` properties and change the behavior using the [`collapsible`](/components/accordion#collapsible) and [`type`](/components/accordion#multiple) props.
::
::component-code
@@ -241,6 +241,113 @@ props:
Groups will be spaced when orientation is `horizontal` and separated when orientation is `vertical`.
::
### Collapsed
In `vertical` orientation, use the `collapsed` prop to collapse the NavigationMenu, this can be useful in a sidebar for example.
::note
You can use the [`tooltip`](#with-tooltip-in-items) and [`popover`](#with-popover-in-items) props to display more information on the collapsed items.
::
::component-code
---
collapse: true
ignore:
- items
- orientation
- class
external:
- items
externalTypes:
- NavigationMenuItem[][]
items:
tooltip:
- true
- false
popover:
- true
- false
props:
collapsed: true
tooltip: false
popover: false
orientation: 'vertical'
items:
- - label: Links
type: 'label'
- label: Guide
icon: i-lucide-book-open
children:
- label: Introduction
description: Fully styled and customizable components for Nuxt.
icon: i-lucide-house
- label: Installation
description: Learn how to install and configure Nuxt UI in your application.
icon: i-lucide-cloud-download
- label: 'Icons'
icon: 'i-lucide-smile'
description: 'You have nothing to do, @nuxt/icon will handle it automatically.'
- label: 'Colors'
icon: 'i-lucide-swatch-book'
description: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
- label: 'Theme'
icon: 'i-lucide-cog'
description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
- label: Composables
icon: i-lucide-database
children:
- label: defineShortcuts
icon: i-lucide-file-text
description: Define shortcuts for your application.
to: /composables/define-shortcuts
- label: useOverlay
icon: i-lucide-file-text
description: Display a modal/slideover within your application.
to: /composables/use-overlay
- label: useToast
icon: i-lucide-file-text
description: Display a toast within your application.
to: /composables/use-toast
- label: Components
icon: i-lucide-box
to: /components
active: true
children:
- label: Link
icon: i-lucide-file-text
description: Use NuxtLink with superpowers.
to: /components/link
- label: Modal
icon: i-lucide-file-text
description: Display a modal within your application.
to: /components/modal
- label: NavigationMenu
icon: i-lucide-file-text
description: Display a list of links.
to: /components/navigation-menu
- label: Pagination
icon: i-lucide-file-text
description: Display a list of pages.
to: /components/pagination
- label: Popover
icon: i-lucide-file-text
description: Display a non-modal dialog that floats around a trigger element.
to: /components/popover
- label: Progress
icon: i-lucide-file-text
description: Show a horizontal bar to indicate task progression.
to: /components/progress
- - label: GitHub
icon: i-simple-icons-github
badge: 3.8k
to: https://github.com/nuxt/ui
target: _blank
- label: Help
icon: i-lucide-circle-help
disabled: true
---
::
### Highlight
Use the `highlight` prop to display a highlighted border for the active item.
@@ -782,6 +889,222 @@ You can inspect the DOM to see each item's content being rendered.
## Examples
### With tooltip in items :badge{label="New" class="align-text-top"}
When orientation is `vertical` and the menu is `collapsed`, you can set the `tooltip` prop to `true` to display a [Tooltip](/components/tooltip) around items with their label but you can also use the `tooltip` property on each item to override the default tooltip.
You can pass any property from the [Tooltip](/components/tooltip) component globally or on each item.
::component-code
---
collapse: true
ignore:
- items
- orientation
- class
external:
- items
externalTypes:
- NavigationMenuItem[][]
items:
tooltip:
- true
- false
props:
tooltip: true
collapsed: true
orientation: 'vertical'
items:
- - label: Links
type: 'label'
- label: Guide
icon: i-lucide-book-open
children:
- label: Introduction
description: Fully styled and customizable components for Nuxt.
icon: i-lucide-house
- label: Installation
description: Learn how to install and configure Nuxt UI in your application.
icon: i-lucide-cloud-download
- label: 'Icons'
icon: 'i-lucide-smile'
description: 'You have nothing to do, @nuxt/icon will handle it automatically.'
- label: 'Colors'
icon: 'i-lucide-swatch-book'
description: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
- label: 'Theme'
icon: 'i-lucide-cog'
description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
- label: Composables
icon: i-lucide-database
children:
- label: defineShortcuts
icon: i-lucide-file-text
description: Define shortcuts for your application.
to: /composables/define-shortcuts
- label: useOverlay
icon: i-lucide-file-text
description: Display a modal/slideover within your application.
to: /composables/use-overlay
- label: useToast
icon: i-lucide-file-text
description: Display a toast within your application.
to: /composables/use-toast
- label: Components
icon: i-lucide-box
to: /components
active: true
children:
- label: Link
icon: i-lucide-file-text
description: Use NuxtLink with superpowers.
to: /components/link
- label: Modal
icon: i-lucide-file-text
description: Display a modal within your application.
to: /components/modal
- label: NavigationMenu
icon: i-lucide-file-text
description: Display a list of links.
to: /components/navigation-menu
- label: Pagination
icon: i-lucide-file-text
description: Display a list of pages.
to: /components/pagination
- label: Popover
icon: i-lucide-file-text
description: Display a non-modal dialog that floats around a trigger element.
to: /components/popover
- label: Progress
icon: i-lucide-file-text
description: Show a horizontal bar to indicate task progression.
to: /components/progress
- - label: GitHub
icon: i-simple-icons-github
badge: 3.8k
to: https://github.com/nuxt/ui
target: _blank
tooltip:
text: 'Open on GitHub'
kbds:
- 3.8k
- label: Help
icon: i-lucide-circle-help
disabled: true
---
::
### With popover in items :badge{label="New" class="align-text-top"}
When orientation is `vertical` and the menu is `collapsed`, you can set the `popover` prop to `true` to display a [Popover](/components/popover) around items with their children but you can also use the `popover` property on each item to override the default popover.
You can pass any property from the [Popover](/components/popover) component globally or on each item.
::component-code
---
collapse: true
ignore:
- items
- orientation
- class
external:
- items
externalTypes:
- NavigationMenuItem[][]
items:
popover:
- true
- false
props:
popover: true
collapsed: true
orientation: 'vertical'
items:
- - label: Links
type: 'label'
- label: Guide
icon: i-lucide-book-open
children:
- label: Introduction
description: Fully styled and customizable components for Nuxt.
icon: i-lucide-house
- label: Installation
description: Learn how to install and configure Nuxt UI in your application.
icon: i-lucide-cloud-download
- label: 'Icons'
icon: 'i-lucide-smile'
description: 'You have nothing to do, @nuxt/icon will handle it automatically.'
- label: 'Colors'
icon: 'i-lucide-swatch-book'
description: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
- label: 'Theme'
icon: 'i-lucide-cog'
description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
- label: Composables
icon: i-lucide-database
popover:
mode: 'click'
children:
- label: defineShortcuts
icon: i-lucide-file-text
description: Define shortcuts for your application.
to: /composables/define-shortcuts
- label: useOverlay
icon: i-lucide-file-text
description: Display a modal/slideover within your application.
to: /composables/use-overlay
- label: useToast
icon: i-lucide-file-text
description: Display a toast within your application.
to: /composables/use-toast
- label: Components
icon: i-lucide-box
to: /components
active: true
children:
- label: Link
icon: i-lucide-file-text
description: Use NuxtLink with superpowers.
to: /components/link
- label: Modal
icon: i-lucide-file-text
description: Display a modal within your application.
to: /components/modal
- label: NavigationMenu
icon: i-lucide-file-text
description: Display a list of links.
to: /components/navigation-menu
- label: Pagination
icon: i-lucide-file-text
description: Display a list of pages.
to: /components/pagination
- label: Popover
icon: i-lucide-file-text
description: Display a non-modal dialog that floats around a trigger element.
to: /components/popover
- label: Progress
icon: i-lucide-file-text
description: Show a horizontal bar to indicate task progression.
to: /components/progress
- - label: GitHub
icon: i-simple-icons-github
badge: 3.8k
to: https://github.com/nuxt/ui
target: _blank
tooltip:
text: 'Open on GitHub'
kbds:
- 3.8k
- label: Help
icon: i-lucide-circle-help
disabled: true
---
::
::tip{to="#with-content-slot"}
You can use the `#content` slot to customize the content of the popover in the `vertical` orientation.
::
### Control active item
You can control the active item by using the `default-value` prop or the `v-model` directive with the index of the item.
@@ -829,6 +1152,7 @@ Use the `#item-content` slot or the `slot` property (`#{{ item.slot }}-content`)
::component-example
---
collapse: true
name: 'navigation-menu-content-slot-example'
---
::

View File

@@ -181,7 +181,7 @@ name: 'popover-open-example'
In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts), you can toggle the Popover by pressing :kbd{value="O"}.
::
### Prevent closing
### Disable dismissal
Set the `dismissible` prop to `false` to prevent the Popover from being closed when clicking outside of it or pressing escape. A `close:prevent` event will be emitted when the user tries to close it.
@@ -202,6 +202,21 @@ name: 'popover-command-palette-example'
---
::
### With anchor slot :badge{label="New" class="align-text-top"}
You can use the `#anchor` slot to position the Popover against a custom element.
::warning
This slot only works when `mode` is `click`.
::
::component-example
---
collapse: true
name: 'popover-anchor-slot-example'
---
::
## API
### Props

View File

@@ -46,6 +46,8 @@ You can also pass an array of objects with the following properties:
- `description?: string`{lang="ts-type"}
- [`value?: string`{lang="ts-type"}](#value-key)
- `disabled?: boolean`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, base?: ClassNameValue, 'indicator'?: ClassNameValue, wrapper?: ClassNameValue, label?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"}
::component-code
---

View File

@@ -57,6 +57,8 @@ You can also pass an array of objects with the following properties:
- [`chip?: ChipProps`{lang="ts-type"}](#with-chip-in-items)
- `disabled?: boolean`{lang="ts-type"}
- `onSelect?(e: Event): void`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{lang="ts-type"}
::component-code
---
@@ -553,7 +555,7 @@ props:
### Loading Icon
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-refresh-cw`.
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
::component-code
---
@@ -568,7 +570,7 @@ external:
props:
modelValue: 'Backlog'
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
items:
- Backlog
- Todo
@@ -653,7 +655,7 @@ props:
---
::
### With icons in items
### With icon in items
You can use the `icon` property to display an [Icon](/components/icon) inside the items.

View File

@@ -48,6 +48,8 @@ You can also pass an array of objects with the following properties:
- [`avatar?: AvatarProps`{lang="ts-type"}](#with-avatar-in-items)
- [`chip?: ChipProps`{lang="ts-type"}](#with-chip-in-items)
- `disabled?: boolean`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{lang="ts-type"}
::component-code
---
@@ -505,7 +507,7 @@ props:
### Loading Icon
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-refresh-cw`.
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
::component-code
---
@@ -520,7 +522,7 @@ external:
props:
modelValue: 'Backlog'
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
items:
- Backlog
- Todo
@@ -605,7 +607,7 @@ props:
---
::
### With icons in items
### With icon in items
You can use the `icon` property to display an [Icon](/components/icon) inside the items.

View File

@@ -273,7 +273,7 @@ In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts),
This allows you to move the trigger outside of the Slideover or remove it entirely.
::
### Prevent closing
### Disable dismissal
Set the `dismissible` prop to `false` to prevent the Slideover from being closed when clicking outside of it or pressing escape. A `close:prevent` event will be emitted when the user tries to close it.
@@ -304,13 +304,13 @@ slots:
### Programmatic usage
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Slideover programatically.
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Slideover programmatically.
::warning
Make sure to wrap your app with the [`App`](/components/app) component which uses the [`OverlayProvider`](https://github.com/nuxt/ui/blob/v3/src/runtime/components/OverlayProvider.vue) component.
::
First, create a slideover component that will be opened programatically:
First, create a slideover component that will be opened programmatically:
::component-example
---

View File

@@ -136,7 +136,7 @@ props:
---
::
### Tooltip :badge{label="Soon" class="align-text-top"}
### Tooltip :badge{label="New" class="align-text-top"}
Use the `tooltip` prop to display a [Tooltip](/components/tooltip) around the Slider thumbs with the current value. You can set it to `true` for default behavior or pass an object to customize it with any property from the [Tooltip](/components/tooltip#props) component.

View File

@@ -23,6 +23,8 @@ Use the `items` prop as an array of objects with the following properties:
- `value?: string | number`{lang="ts-type"}
- `disabled?: boolean`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, trigger?: ClassNameValue, indicator?: ClassNameValue, icon?: ClassNameValue, separator?: ClassNameValue, wrapper?: ClassNameValue, title?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"}
::component-code
---
@@ -198,6 +200,10 @@ Use the `#content` slot to customize the content of each item.
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}`{lang="ts-type"}
:component-example{name="stepper-custom-slot-example"}
## API

View File

@@ -109,7 +109,7 @@ props:
### Loading Icon
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-refresh-cw`.
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
::component-code
---
@@ -118,7 +118,7 @@ ignore:
- defaultValue
props:
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
defaultValue: true
label: Check me
---

View File

@@ -23,6 +23,8 @@ Use the `items` prop as an array of objects with the following properties:
- `value?: string | number`{lang="ts-type"}
- `disabled?: boolean`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `class?: any`{lang="ts-type"}
- `ui?: { trigger?: ClassNameValue, leadingIcon?: ClassNameValue, leadingAvatar?: ClassNameValue, label?: ClassNameValue, content?: ClassNameValue }`{lang="ts-type"}
::component-code
---
@@ -220,6 +222,10 @@ Use the `#content` slot to customize the content of each item.
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}`{lang="ts-type"}
:component-example{name="tabs-custom-slot-example"}
## API

View File

@@ -194,7 +194,7 @@ props:
### Loading Icon :badge{label="New" class="align-text-top"}
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-refresh-cw`.
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
::component-code
---
@@ -202,7 +202,7 @@ ignore:
- placeholder
props:
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
placeholder: 'Search...'
rows: 1
---

View File

@@ -0,0 +1,228 @@
---
title: Timeline
description: 'A component that displays a sequence of events with dates, titles, icons or avatars.'
category: data
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Timeline.vue
navigation.badge: Soon
---
## Usage
### Items
Use the `items` prop as an array of objects with the following properties:
- `date?: string`{lang="ts-type"}
- `title?: string`{lang="ts-type"}
- `description?: AvatarProps`{lang="ts-type"}
- `icon?: string`{lang="ts-type"}
- `avatar?: AvatarProps`{lang="ts-type"}
- `value?: string | number`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, indicator?: ClassNameValue, separator?: ClassNameValue, wrapper?: ClassNameValue, separator?: ClassNameValue, date?: ClassNameValue, title?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"}
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
defaultValue: 2
items:
- date: 'Mar 15, 2025'
title: 'Project Kickoff'
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.'
icon: 'i-lucide-rocket'
- date: 'Mar 22 2025'
title: 'Design Phase'
description: 'User research and design workshops. Created wireframes and prototypes for user testing.'
icon: 'i-lucide-palette'
- date: 'Mar 29 2025'
title: 'Development Sprint'
description: 'Frontend and backend development. Implemented core features and integrated with APIs.'
icon: 'i-lucide-code'
- date: 'Apr 5 2025'
title: 'Testing & Deployment'
description: 'QA testing and performance optimization. Deployed the application to production.'
icon: 'i-lucide-check-circle'
class: 'w-96'
---
::
### Color
Use the `color` prop to change the color of the active items in a Timeline.
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
color: neutral
defaultValue: 2
items:
- date: 'Mar 15, 2025'
title: 'Project Kickoff'
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.'
icon: 'i-lucide-rocket'
- date: 'Mar 22 2025'
title: 'Design Phase'
description: 'User research and design workshops. Created wireframes and prototypes for user testing.'
icon: 'i-lucide-palette'
- date: 'Mar 29 2025'
title: 'Development Sprint'
description: 'Frontend and backend development. Implemented core features and integrated with APIs.'
icon: 'i-lucide-code'
- date: 'Apr 5 2025'
title: 'Testing & Deployment'
description: 'QA testing and performance optimization. Deployed the application to production.'
icon: 'i-lucide-check-circle'
class: 'w-96'
---
::
### Size
Use the `size` prop to change the size of the Timeline.
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
size: xs
defaultValue: 2
items:
- date: 'Mar 15, 2025'
title: 'Project Kickoff'
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.'
icon: 'i-lucide-rocket'
- date: 'Mar 22 2025'
title: 'Design Phase'
description: 'User research and design workshops. Created wireframes and prototypes for user testing.'
icon: 'i-lucide-palette'
- date: 'Mar 29 2025'
title: 'Development Sprint'
description: 'Frontend and backend development. Implemented core features and integrated with APIs.'
icon: 'i-lucide-code'
- date: 'Apr 5 2025'
title: 'Testing & Deployment'
description: 'QA testing and performance optimization. Deployed the application to production.'
icon: 'i-lucide-check-circle'
class: 'w-96'
---
::
### Orientation
Use the `orientation` prop to change the orientation of the Timeline. Defaults to `vertical`.
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
orientation: 'horizontal'
defaultValue: 2
items:
- date: 'Mar 15, 2025'
title: 'Project Kickoff'
description: 'Kicked off the project with team alignment.'
icon: 'i-lucide-rocket'
- date: 'Mar 22 2025'
title: 'Design Phase'
description: 'User research and design workshops.'
icon: 'i-lucide-palette'
- date: 'Mar 29 2025'
title: 'Development Sprint'
description: 'Frontend and backend development.'
icon: 'i-lucide-code'
- date: 'Apr 5 2025'
title: 'Testing & Deployment'
description: 'QA testing and performance optimization.'
icon: 'i-lucide-check-circle'
class: 'w-full'
---
::
## Examples
### Control active item
You can control the active item by using the `default-value` prop or the `v-model` directive with the index of the item.
:component-example{name="timeline-model-value-example" prettier}
::tip
You can also pass the `value` of one of the items if provided.
::
### With alternating layout
Use the `ui` prop to create a Timeline with alternating layout.
:component-example{name="timeline-alternating-layout-example" prettier}
### With custom slot
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}-indicator`{lang="ts-type"}
- `#{{ item.slot }}-date`{lang="ts-type"}
- `#{{ item.slot }}-title`{lang="ts-type"}
- `#{{ item.slot }}-description`{lang="ts-type"}
:component-example{name="timeline-custom-slot-example" prettier}
### With slots
Use the available slots to create a more complex Timeline.
:component-example{name="timeline-slots-example" prettier}
## API
### Props
:component-props
### Slots
:component-slots
### Emits
:component-emits
## Theme
:component-theme

View File

@@ -26,6 +26,8 @@ Use the `items` prop as an array of objects with the following properties:
- `children?: TreeItem[]`{lang="ts-type"}
- `onToggle?(e: Event): void`{lang="ts-type"}
- `onSelect?(e?: Event): void`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, itemWithChildren?: ClassNameValue, link?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLabel?: ClassNameValue, linkTrailing?: ClassNameValue, linkTrailingIcon?: ClassNameValue, listWithChildren?: ClassNameValue }`{lang="ts-type"}
::note
A unique identifier is required for each item. The component will use the `value` prop as identifier, falling back to `label` if `value` is not provided. One of these must be provided for the component to work properly.
@@ -405,7 +407,14 @@ This lets you select a parent item without expanding or collapsing its children.
### With custom slot
Use the `item.slot` property to customize a specific item.
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}`{lang="ts-type"}
- `#{{ item.slot }}-leading`{lang="ts-type"}
- `#{{ item.slot }}-label`{lang="ts-type"}
- `#{{ item.slot }}-trailing`{lang="ts-type"}
::component-example
---

View File

@@ -3,40 +3,40 @@
"name": "@nuxt/ui-docs",
"type": "module",
"dependencies": {
"@ai-sdk/vue": "^1.2.11",
"@ai-sdk/vue": "^1.2.12",
"@iconify-json/logos": "^1.2.4",
"@iconify-json/lucide": "^1.2.41",
"@iconify-json/simple-icons": "^1.2.33",
"@iconify-json/vscode-icons": "^1.2.20",
"@iconify-json/lucide": "^1.2.44",
"@iconify-json/simple-icons": "^1.2.35",
"@iconify-json/vscode-icons": "^1.2.21",
"@nuxt/content": "^3.5.1",
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@a30de4d",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@f06b49c",
"@nuxthub/core": "^0.8.27",
"@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^21.1.1",
"@octokit/rest": "^22.0.0",
"@rollup/plugin-yaml": "^4.1.2",
"@vueuse/integrations": "^13.1.0",
"@vueuse/nuxt": "^13.1.0",
"ai": "^4.3.15",
"@vueuse/integrations": "^13.2.0",
"@vueuse/nuxt": "^13.2.0",
"ai": "^4.3.16",
"capture-website": "^4.2.0",
"joi": "^17.13.3",
"motion-v": "^1.0.2",
"nuxt": "^3.17.2",
"motion-v": "^1.1.1",
"nuxt": "^3.17.4",
"nuxt-component-meta": "^0.11.0",
"nuxt-llms": "^0.1.2",
"nuxt-og-image": "^5.1.3",
"nuxt-og-image": "^5.1.4",
"prettier": "^3.5.3",
"shiki-transformer-color-highlight": "^1.0.0",
"sortablejs": "^1.15.6",
"superstruct": "^2.0.2",
"ufo": "^1.6.1",
"valibot": "^1.1.0",
"workers-ai-provider": "^0.3.1",
"workers-ai-provider": "^0.5.2",
"yup": "^1.6.1",
"zod": "^3.24.4"
"zod": "^3.25.28"
},
"devDependencies": {
"wrangler": "^4.14.4"
"wrangler": "^4.16.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,8 +1,8 @@
{
"name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "3.1.1",
"packageManager": "pnpm@10.10.0",
"version": "3.1.3",
"packageManager": "pnpm@10.11.0",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/ui.git"
@@ -112,21 +112,21 @@
"release": "release-it"
},
"dependencies": {
"@iconify/vue": "^4.3.0",
"@internationalized/date": "^3.8.0",
"@internationalized/number": "^3.6.1",
"@nuxt/fonts": "^0.11.2",
"@nuxt/icon": "^1.12.0",
"@nuxt/kit": "^3.17.2",
"@nuxt/schema": "^3.17.2",
"@iconify/vue": "^5.0.0",
"@internationalized/date": "^3.8.1",
"@internationalized/number": "^3.6.2",
"@nuxt/fonts": "^0.11.4",
"@nuxt/icon": "^1.13.0",
"@nuxt/kit": "^3.17.4",
"@nuxt/schema": "^3.17.4",
"@nuxtjs/color-mode": "^3.5.2",
"@standard-schema/spec": "^1.0.0",
"@tailwindcss/postcss": "^4.1.5",
"@tailwindcss/vite": "^4.1.5",
"@tailwindcss/postcss": "^4.1.7",
"@tailwindcss/vite": "^4.1.7",
"@tanstack/vue-table": "^8.21.3",
"@unhead/vue": "^2.0.8",
"@vueuse/core": "^13.1.0",
"@vueuse/integrations": "^13.1.0",
"@unhead/vue": "^2.0.10",
"@vueuse/core": "^13.2.0",
"@vueuse/integrations": "^13.2.0",
"colortranslator": "^4.1.0",
"consola": "^3.4.2",
"defu": "^6.1.4",
@@ -144,29 +144,29 @@
"mlly": "^1.7.4",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"reka-ui": "^2.2.1",
"reka-ui": "^2.3.0",
"scule": "^1.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.5",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.2",
"unplugin-auto-import": "^19.2.0",
"unplugin-vue-components": "^28.5.0",
"tailwindcss": "^4.1.7",
"tinyglobby": "^0.2.14",
"unplugin": "^2.3.4",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.7.0",
"vaul-vue": "^0.4.1",
"vue-component-type-helpers": "^2.2.10"
},
"devDependencies": {
"@nuxt/eslint-config": "^1.3.0",
"@nuxt/eslint-config": "^1.4.1",
"@nuxt/module-builder": "^1.0.1",
"@nuxt/test-utils": "^3.18.0",
"@nuxt/test-utils": "^3.19.1",
"@release-it/conventional-changelog": "^10.0.1",
"@vue/test-utils": "^2.4.6",
"embla-carousel": "^8.6.0",
"eslint": "^9.26.0",
"happy-dom": "^17.4.6",
"nuxt": "^3.17.2",
"eslint": "^9.27.0",
"happy-dom": "^17.4.7",
"nuxt": "^3.17.4",
"release-it": "^19.0.2",
"vitest": "^3.1.3",
"vitest": "^3.1.4",
"vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.2.10"
},
@@ -209,7 +209,7 @@
"debug": "4.3.7",
"rollup": "4.34.9",
"unimport": "4.1.1",
"unplugin": "^2.3.2"
"unplugin": "^2.3.4"
},
"pnpm": {
"onlyBuiltDependencies": [

View File

@@ -11,12 +11,12 @@
},
"dependencies": {
"@nuxt/ui": "latest",
"vue": "^3.5.13",
"vue": "^3.5.15",
"vue-router": "^4.5.1",
"zod": "^3.24.4"
"zod": "^3.25.28"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue": "^5.2.4",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vue-tsc": "^2.2.10"

View File

@@ -61,6 +61,7 @@ const components = [
'tabs',
'table',
'textarea',
'timeline',
'toast',
'tooltip',
'tree'

View File

@@ -61,6 +61,7 @@ const components = [
'tabs',
'table',
'textarea',
'timeline',
'toast',
'tooltip',
'tree'

View File

@@ -36,14 +36,27 @@ const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme
color="neutral"
/>
</div>
<div class="flex items-center gap-2 ms-[-56px]">
<div class="flex items-center gap-2 ms-[-90px]">
<UBadge v-for="size in sizes" :key="size" label="Badge" :size="size" />
</div>
<div class="flex items-center gap-2 ms-[-86px]">
<div class="flex items-center gap-2 ms-[-122px]">
<UBadge v-for="size in sizes" :key="size" icon="i-lucide-rocket" label="Badge" :size="size" />
</div>
<div class="flex items-center gap-2 ms-[-86px]">
<div class="flex items-center gap-2 ms-[-130px]">
<UBadge v-for="size in sizes" :key="size" :avatar="{ src: 'https://github.com/benjamincanac.png' }" label="Badge" :size="size" />
</div>
<div class="flex items-center gap-2 ms-[-52px]">
<UBadge v-for="size in sizes" :key="size" icon="i-lucide-rocket" :size="size" />
</div>
<div class="flex items-center gap-2 ms-[-60px]">
<UBadge
v-for="size in sizes"
:key="size"
:avatar="{ src: 'https://github.com/benjamincanac.png' }"
:size="size"
color="neutral"
variant="outline"
/>
</div>
</div>
</template>

View File

@@ -2,19 +2,23 @@
import { CalendarDate } from '@internationalized/date'
const singleValue = shallowRef(new CalendarDate(2022, 1, 10))
const multipleValue = shallowRef({
const multipleValue = shallowRef([new CalendarDate(2022, 1, 10), new CalendarDate(2022, 1, 20)])
const rangeValue = shallowRef({
start: new CalendarDate(2022, 1, 10),
end: new CalendarDate(2022, 1, 20)
})
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex gap-4">
<div class="flex justify-center gap-2">
<UCalendar v-model="singleValue" />
</div>
<div class="flex justify-center gap-2">
<UCalendar v-model="multipleValue" range />
<UCalendar v-model="multipleValue" multiple />
</div>
<div class="flex justify-center gap-2">
<UCalendar v-model="rangeValue" range />
</div>
</div>
</template>

View File

@@ -1,9 +1,8 @@
<script setup lang="ts">
import themeCheckbox from '#build/ui/checkbox'
import theme from '#build/ui/checkbox-group'
const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size>
const variants = Object.keys(themeCheckbox.variants.variant)
const variants = Object.keys(theme.variants.variant)
const variant = ref('list' as const)
const literalOptions = [

View File

@@ -47,7 +47,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-col gap-4 w-48">
<UInputMenu :items="items" autofocus placeholder="Search..." />
<UInputMenu :items="items" autofocus placeholder="Search..." default-value="Apple" />
</div>
<div class="flex items-center gap-2">
<UInputMenu

View File

@@ -69,5 +69,13 @@ function openModal() {
</UModal>
<UButton label="Open programmatically" color="neutral" variant="outline" @click="openModal" />
<UModal title="First modal">
<UButton color="neutral" variant="outline" label="Close with scoped slot close" />
<template #footer="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
</UModal>
</div>
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'
import theme from '#build/ui/navigation-menu'
const colors = Object.keys(theme.variants.color)
@@ -13,6 +14,9 @@ const orientation = ref('horizontal' as const)
const contentOrientation = ref('horizontal' as const)
const highlight = ref(true)
const collapsed = ref(false)
const tooltip = ref(false)
const popover = ref(false)
const arrow = ref(false)
const items = [
[{
@@ -42,7 +46,8 @@ const items = [
}, {
label: 'Components',
icon: 'i-lucide-box',
to: '/components',
to: '/components/navigation-menu',
type: 'trigger',
active: true,
defaultOpen: true,
children: [{
@@ -87,7 +92,7 @@ const items = [
icon: 'i-lucide-circle-help',
disabled: true
}]
]
] satisfies NavigationMenuItem[][]
</script>
<template>
@@ -100,10 +105,15 @@ const items = [
<USwitch v-model="collapsed" label="Collapsed" />
<USwitch v-model="highlight" label="Highlight" />
<USelect v-model="highlightColor" :items="colors" placeholder="Highlight color" />
<USwitch v-model="tooltip" label="Tooltip" />
<USwitch v-model="popover" label="Popover" />
<USwitch v-model="arrow" label="Arrow" />
</div>
<UNavigationMenu
arrow
:arrow="arrow"
:tooltip="tooltip"
:popover="popover"
:collapsed="collapsed"
:items="items"
:color="color"

View File

@@ -5,7 +5,7 @@ const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.varia
const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme.variants.variant>
const onComplete = (e: string[]) => {
alert(e.join(''))
console.log(e)
}
</script>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
const open = ref(false)
const openCustomAnchor = ref(false)
const loading = ref(false)
function send() {
@@ -51,6 +52,21 @@ function send() {
</div>
</template>
</UPopover>
<div class="mt-8 relative">
<UPopover
v-model:open="openCustomAnchor"
:dismissible="false"
>
<template #anchor>
<UInput placeholder="Search" class="w-56" @focus="openCustomAnchor = true" />
</template>
<template #content>
<Placeholder class="size-48 m-4 inline-flex" />
</template>
</UPopover>
</div>
</div>
<div class="mt-24">

View File

@@ -52,7 +52,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-col gap-4 w-48">
<USelectMenu :items="items" placeholder="Search..." />
<USelectMenu :items="items" placeholder="Search..." default-value="Apple" />
</div>
<div class="flex items-center gap-2">
<USelectMenu

View File

@@ -125,5 +125,21 @@ function openSlideover() {
</USlideover>
<UButton label="Open programmatically" color="neutral" variant="outline" @click="openSlideover" />
<USlideover title="Slideover with scoped slot close" description="This slideover has a scoped slot close that can be used to close the slideover from within the content.">
<UButton color="neutral" variant="subtle" label="Open with scoped slot close" />
<template #header="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
<template #body="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
<template #footer="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
</USlideover>
</div>
</template>

View File

@@ -0,0 +1,60 @@
<script lang="ts" setup>
import type { TimelineItem } from '@nuxt/ui'
import theme from '#build/ui/timeline'
const sizes = Object.keys(theme.variants.size)
const colors = Object.keys(theme.variants.color)
const orientations = Object.keys(theme.variants.orientation)
const orientation = ref('vertical' as const)
const color = ref('primary' as const)
const size = ref('md' as const)
const items = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
description: 'User research and design workshops. Created wireframes and prototypes for user testing',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
icon: 'i-lucide-code',
value: 'development'
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
description: 'QA testing and performance optimization. Deployed the application to production.',
icon: 'i-lucide-check-circle',
value: 'deployment'
}] satisfies TimelineItem[]
const value = ref('kickoff')
</script>
<template>
<div class="flex flex-col gap-10">
<div class="flex items-center justify-center gap-2">
<USelect v-model="color" :items="colors" placeholder="Color" />
<USelect v-model="orientation" :items="orientations" placeholder="Orientation" />
<USelect v-model="size" :items="sizes" placeholder="Size" />
<USelect v-model="value" :items="items.map(item => item.value)" placeholder="Value" />
</div>
<UTimeline
v-model="value"
:color="color"
:orientation="orientation"
:size="size"
:items="items"
class="data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-96"
/>
</div>
</template>

View File

@@ -9,12 +9,12 @@
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.41",
"@iconify-json/simple-icons": "^1.2.33",
"@iconify-json/lucide": "^1.2.44",
"@iconify-json/simple-icons": "^1.2.35",
"@nuxt/ui": "latest",
"@nuxthub/core": "^0.8.27",
"nuxt": "^3.17.2",
"zod": "^3.24.4"
"nuxt": "^3.17.4",
"zod": "^3.25.28"
},
"devDependencies": {
"typescript": "^5.8.3",

5264
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -91,12 +91,21 @@ export default defineNuxtModule<ModuleOptions>({
}
}
await registerModule('@nuxt/icon', 'icon', { cssLayer: 'components' })
await registerModule('@nuxt/icon', 'icon', {
cssLayer: 'components'
})
if (options.fonts) {
await registerModule('@nuxt/fonts', 'fonts', {})
await registerModule('@nuxt/fonts', 'fonts', {
defaults: {
weights: [400, 500, 600, 700]
}
})
}
if (options.colorMode) {
await registerModule('@nuxtjs/color-mode', 'colorMode', { classSuffix: '', disableTransition: true })
await registerModule('@nuxtjs/color-mode', 'colorMode', {
classSuffix: '',
disableTransition: true
})
}
addPlugin({ src: resolve('./runtime/plugins/colors') })

View File

@@ -16,7 +16,6 @@ export default function PluginsPlugin(options: NuxtUIOptions) {
const plugins = globSync(['**/*', '!*.d.ts'], { cwd: join(runtimeDir, 'plugins'), absolute: true })
plugins.unshift(resolvePathSync('../runtime/vue/plugins/head', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url }))
plugins.push(resolvePathSync('../runtime/vue/plugins/colors', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url }))
if (options.colorMode) {
plugins.push(resolvePathSync('../runtime/vue/plugins/color-mode', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url }))
}

View File

@@ -22,6 +22,8 @@ export interface AccordionItem {
/** A unique value for the accordion item. Defaults to the index. */
value?: string
disabled?: boolean
class?: any
ui?: Pick<Accordion['slots'], 'item' | 'header' | 'trigger' | 'leadingIcon' | 'label' | 'trailingIcon' | 'content' | 'body'>
[key: string]: any
}
@@ -96,27 +98,27 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.accordion ||
:key="index"
:value="item.value || String(index)"
:disabled="item.disabled"
:class="ui.item({ class: props.ui?.item })"
:class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class] })"
>
<AccordionHeader as="div" :class="ui.header({ class: props.ui?.header })">
<AccordionTrigger :class="ui.trigger({ class: props.ui?.trigger, disabled: item.disabled })">
<AccordionHeader as="div" :class="ui.header({ class: [props.ui?.header, item.ui?.header] })">
<AccordionTrigger :class="ui.trigger({ class: [props.ui?.trigger, item.ui?.trigger], disabled: item.disabled })">
<slot name="leading" :item="item" :index="index" :open="open">
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: [props.ui?.leadingIcon, item?.ui?.leadingIcon] })" />
</slot>
<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: props.ui?.label })">
<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: [props.ui?.label, item.ui?.label] })">
<slot :item="item" :index="index" :open="open">{{ get(item, props.labelKey as string) }}</slot>
</span>
<slot name="trailing" :item="item" :index="index" :open="open">
<UIcon :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
<UIcon :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.trailingIcon({ class: [props.ui?.trailingIcon, item.ui?.trailingIcon] })" />
</slot>
</AccordionTrigger>
</AccordionHeader>
<AccordionContent v-if="item.content || !!slots.content || (item.slot && !!slots[item.slot as keyof AccordionSlots<T>]) || !!slots.body || (item.slot && !!slots[`${item.slot}-body` as keyof AccordionSlots<T>])" :class="ui.content({ class: props.ui?.content })">
<AccordionContent v-if="item.content || !!slots.content || (item.slot && !!slots[item.slot as keyof AccordionSlots<T>]) || !!slots.body || (item.slot && !!slots[`${item.slot}-body` as keyof AccordionSlots<T>])" :class="ui.content({ class: [props.ui?.content, item.ui?.content] })">
<slot :name="((item.slot || 'content') as keyof AccordionSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index" :open="open">
<div :class="ui.body({ class: props.ui?.body })">
<div :class="ui.body({ class: [props.ui?.body, item.ui?.body] })">
<slot :name="((item.slot ? `${item.slot}-body`: 'body') as keyof AccordionSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index" :open="open">
{{ item.content }}
</slot>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/avatar'
import type { ChipProps } from '../types'
import type { ComponentConfig } from '../types/utils'
type Avatar = ComponentConfig<typeof theme, AppConfig, 'avatar'>
@@ -22,6 +23,7 @@ export interface AvatarProps {
* @defaultValue 'md'
*/
size?: Avatar['variants']['size']
chip?: boolean | ChipProps
class?: any
style?: any
ui?: Avatar['slots']
@@ -40,6 +42,7 @@ import ImageComponent from '#build/ui-image-component'
import { useAvatarGroup } from '../composables/useAvatarGroup'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
import UChip from './Chip.vue'
defineOptions({ inheritAttrs: false })
@@ -81,7 +84,13 @@ function onError() {
</script>
<template>
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })" :style="props.style">
<component
:is="props.chip ? UChip : Primitive"
:as="as"
v-bind="props.chip ? (typeof props.chip === 'object' ? { inset: true, ...props.chip } : { inset: true }) : {}"
:class="ui.root({ class: [props.ui?.root, props.class] })"
:style="props.style"
>
<component
:is="ImageComponent"
v-if="src && !error"
@@ -101,5 +110,5 @@ function onError() {
<span v-else :class="ui.fallback({ class: props.ui?.fallback })">{{ fallback || '&nbsp;' }}</span>
</slot>
</Slot>
</Primitive>
</component>
</template>

View File

@@ -26,6 +26,8 @@ export interface BadgeProps extends Omit<UseComponentIconsProps, 'loading' | 'lo
* @defaultValue 'md'
*/
size?: Badge['variants']['size']
/** Render the badge with equal padding on all sides. */
square?: boolean
class?: any
ui?: Badge['slots']
}
@@ -50,7 +52,7 @@ import UAvatar from './Avatar.vue'
const props = withDefaults(defineProps<BadgeProps>(), {
as: 'span'
})
defineSlots<BadgeSlots>()
const slots = defineSlots<BadgeSlots>()
const appConfig = useAppConfig() as Badge['AppConfig']
const { orientation, size: buttonGroupSize } = useButtonGroup<BadgeProps>(props)
@@ -60,6 +62,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.badge || {})
color: props.color,
variant: props.variant,
size: buttonGroupSize.value || props.size,
square: props.square || (!slots.default && !props.label),
buttonGroup: orientation.value
}))
</script>

View File

@@ -15,6 +15,8 @@ export interface BreadcrumbItem extends Omit<LinkProps, 'raw' | 'custom'> {
icon?: string
avatar?: AvatarProps
slot?: string
class?: any
ui?: Pick<Breadcrumb['slots'], 'item' | 'link' | 'linkLeadingIcon' | 'linkLeadingAvatar' | 'linkLabel' | 'separator' | 'separatorIcon'>
[key: string]: any
}
@@ -84,16 +86,16 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.breadcrumb |
<Primitive :as="as" aria-label="breadcrumb" :class="ui.root({ class: [props.ui?.root, props.class] })">
<ol :class="ui.list({ class: props.ui?.list })">
<template v-for="(item, index) in items" :key="index">
<li :class="ui.item({ class: props.ui?.item })">
<li :class="ui.item({ class: [props.ui?.item, item.ui?.item] })">
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
<ULinkBase v-bind="slotProps" as="span" :aria-current="active && (index === items!.length - 1) ? 'page' : undefined" :class="ui.link({ class: [props.ui?.link, item.class], active: index === items!.length - 1, disabled: !!item.disabled, to: !!item.to })">
<ULinkBase v-bind="slotProps" as="span" :aria-current="active && (index === items!.length - 1) ? 'page' : undefined" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: index === items!.length - 1, disabled: !!item.disabled, to: !!item.to })">
<slot :name="((item.slot || 'item') as keyof BreadcrumbSlots<T>)" :item="item" :index="index">
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index">
<UIcon v-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active: index === items!.length - 1 })" />
<UAvatar v-else-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active: index === items!.length - 1 })" />
<UIcon v-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon], active: index === items!.length - 1 })" />
<UAvatar v-else-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: [props.ui?.linkLeadingAvatar, item.ui?.linkLeadingAvatar], active: index === items!.length - 1 })" />
</slot>
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>]" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>]" :class="ui.linkLabel({ class: [props.ui?.linkLabel, item.ui?.linkLabel] })">
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index">
{{ get(item, props.labelKey as string) }}
</slot>
@@ -105,9 +107,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.breadcrumb |
</ULink>
</li>
<li v-if="index < items!.length - 1" role="presentation" aria-hidden="true" :class="ui.separator({ class: props.ui?.separator })">
<li v-if="index < items!.length - 1" role="presentation" aria-hidden="true" :class="ui.separator({ class: [props.ui?.separator, item.ui?.separator] })">
<slot name="separator">
<UIcon :name="separatorIcon" :class="ui.separatorIcon({ class: props.ui?.separatorIcon })" />
<UIcon :name="separatorIcon" :class="ui.separatorIcon({ class: [props.ui?.separatorIcon, item.ui?.separatorIcon] })" />
</slot>
</li>
</template>

View File

@@ -1,9 +1,8 @@
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/button'
import type { LinkProps } from './Link.vue'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps } from '../types'
import type { LinkProps, AvatarProps } from '../types'
import type { ComponentConfig } from '../types/utils'
type Button = ComponentConfig<typeof theme, AppConfig, 'button'>
@@ -123,14 +122,13 @@ const ui = computed(() => tv({
v-slot="{ active, ...slotProps }"
:type="type"
:disabled="disabled || isLoading"
:class="ui.base({ class: [props.ui?.base, props.class] })"
v-bind="omit(linkProps, ['type', 'disabled', 'onClick'])"
custom
>
<ULinkBase
v-bind="slotProps"
:class="ui.base({
class: [props.class, props.ui?.base],
class: [props.ui?.base, props.class],
active,
...(active && activeVariant ? { variant: activeVariant } : {}),
...(active && activeColor ? { color: activeColor } : {})

View File

@@ -153,8 +153,8 @@ const Calendar = computed(() => props.range ? RangeCalendar : SingleCalendar)
<Calendar.Root
v-slot="{ weekDays, grid }"
v-bind="rootProps"
:model-value="modelValue"
:default-value="defaultValue"
:model-value="(modelValue as DateValue | DateValue[])"
:default-value="(defaultValue as DateValue)"
:locale="locale"
:dir="dir"
:class="ui.root({ class: [props.ui?.root, props.class] })"

View File

@@ -15,7 +15,13 @@ import type { ComponentConfig } from '../types/utils'
type Carousel = ComponentConfig<typeof theme, AppConfig, 'carousel'>
export type CarouselItem = AcceptableValue
interface _CarouselItem {
class?: any
ui?: Pick<Carousel['slots'], 'item'>
[key: string]: any
}
export type CarouselItem = _CarouselItem | AcceptableValue
export interface CarouselProps<T extends CarouselItem = CarouselItem> extends Omit<EmblaOptionsType, 'axis' | 'container' | 'slides' | 'direction'> {
/**
@@ -254,6 +260,10 @@ function onSelect(api: EmblaCarouselType) {
emits('select', selectedIndex.value)
}
function isCarouselItem(item: CarouselItem): item is _CarouselItem {
return typeof item === 'object' && item !== null
}
onMounted(() => {
if (!emblaApi.value) {
return
@@ -288,7 +298,7 @@ defineExpose({
:key="index"
role="group"
aria-roledescription="slide"
:class="ui.item({ class: props.ui?.item })"
:class="ui.item({ class: [props.ui?.item, isCarouselItem(item) && item.ui?.item, isCarouselItem(item) && item.class] })"
>
<slot :item="item" :index="index" />
</div>
@@ -326,6 +336,7 @@ defineExpose({
<button
:aria-label="t('carousel.goto', { slide: index + 1 })"
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
:data-state="selectedIndex === index ? 'active' : undefined"
@click="scrollTo(index)"
/>
</template>

View File

@@ -101,7 +101,7 @@ function onUpdate(value: any) {
<!-- eslint-disable vue/no-template-shadow -->
<template>
<Primitive :as="variant === 'list' ? as : Label" :class="ui.root({ class: [props.ui?.root, props.class] })">
<Primitive :as="(!variant || variant === 'list') ? as : Label" :class="ui.root({ class: [props.ui?.root, props.class] })">
<div :class="ui.container({ class: props.ui?.container })">
<CheckboxRoot
:id="id"
@@ -122,7 +122,7 @@ function onUpdate(value: any) {
</div>
<div v-if="(label || !!slots.label) || (description || !!slots.description)" :class="ui.wrapper({ class: props.ui?.wrapper })">
<component :is="variant === 'list' ? Label : 'p'" v-if="label || !!slots.label" :for="id" :class="ui.label({ class: props.ui?.label })">
<component :is="(!variant || variant === 'list') ? Label : 'p'" v-if="label || !!slots.label" :for="id" :class="ui.label({ class: props.ui?.label })">
<slot name="label" :label="label">
{{ label }}
</slot>

View File

@@ -14,10 +14,12 @@ export type CheckboxGroupItem = {
description?: string
disabled?: boolean
value?: string
class?: any
ui?: Pick<CheckboxGroup['slots'], 'item'> & Omit<Required<CheckboxProps>['ui'], 'root'>
[key: string]: any
} | CheckboxGroupValue
export interface CheckboxGroupProps<T extends CheckboxGroupItem = CheckboxGroupItem> extends Pick<CheckboxGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'>, Pick<CheckboxProps, 'color' | 'variant' | 'indicator' | 'icon'> {
export interface CheckboxGroupProps<T extends CheckboxGroupItem = CheckboxGroupItem> extends Pick<CheckboxGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'>, Pick<CheckboxProps, 'color' | 'indicator' | 'icon'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -44,6 +46,10 @@ export interface CheckboxGroupProps<T extends CheckboxGroupItem = CheckboxGroupI
* @defaultValue 'md'
*/
size?: CheckboxGroup['variants']['size']
/**
* @defaultValue 'list'
*/
variant?: CheckboxGroup['variants']['variant']
/**
* The orientation the checkbox buttons are laid out.
* @defaultValue 'vertical'
@@ -97,7 +103,9 @@ const id = _id.value ?? useId()
const ui = computed(() => tv({ extend: theme, ...(appConfig.ui?.checkboxGroup || {}) })({
size: size.value,
required: props.required,
orientation: props.orientation
orientation: props.orientation,
color: props.color,
variant: props.variant
}))
function normalizeItem(item: any) {
@@ -171,8 +179,8 @@ function onUpdate(value: any) {
:size="size"
:name="name"
:disabled="item.disabled || disabled"
:ui="props.ui ? omit(props.ui, ['root']) : undefined"
:class="ui.item({ class: props.ui?.item })"
:ui="{ ...(props.ui ? omit(props.ui, ['root']) : undefined), ...(item.ui || {}) }"
:class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class] })"
>
<template v-for="(_, name) in proxySlots" #[name]>
<slot :name="(name as keyof CheckboxGroupSlots<T>)" :item="item" />

View File

@@ -27,6 +27,8 @@ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
disabled?: boolean
slot?: string
onSelect?(e?: Event): void
class?: any
ui?: Pick<CommandPalette['slots'], 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemLabelPrefix' | 'itemLabelBase' | 'itemLabelSuffix' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon'>
[key: string]: any
}
@@ -278,7 +280,7 @@ const groups = computed(() => {
</ListboxFilter>
<ListboxContent :class="ui.content({ class: props.ui?.content })">
<div v-if="groups?.length" :class="ui.viewport({ class: props.ui?.viewport })">
<div v-if="groups?.length" role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
<ListboxGroup v-for="group in groups" :key="`group-${group.id}`" :class="ui.group({ class: props.ui?.group })">
<ListboxGroupLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
{{ get(group, props.labelKey as string) }}
@@ -293,42 +295,42 @@ const groups = computed(() => {
@select="item.onSelect"
>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: props.ui?.item, active: active || item.active })">
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class], active: active || item.active })">
<slot :name="((item.slot || group.slot || 'item') as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
<slot :name="((item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, loading: true })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, active: active || item.active })" />
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar, active: active || item.active })" />
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon], loading: true })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon], active: active || item.active })" />
<UAvatar v-else-if="item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [props.ui?.itemLeadingAvatar, item.ui?.itemLeadingAvatar], active: active || item.active })" />
<UChip
v-else-if="item.chip"
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
:size="((item.ui?.itemLeadingChipSize || props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
inset
standalone
v-bind="item.chip"
:class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip, active: active || item.active })"
:class="ui.itemLeadingChip({ class: [props.ui?.itemLeadingChip, item.ui?.itemLeadingChip], active: active || item.active })"
/>
</slot>
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>]" :class="ui.itemLabel({ class: props.ui?.itemLabel, active: active || item.active })">
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>]" :class="ui.itemLabel({ class: [props.ui?.itemLabel, item.ui?.itemLabel], active: active || item.active })">
<slot :name="((item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
<span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
<span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: [props.ui?.itemLabelPrefix, item.ui?.itemLabelPrefix] })">{{ item.prefix }}</span>
<span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase, active: active || item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
<span :class="ui.itemLabelBase({ class: [props.ui?.itemLabelBase, item.ui?.itemLabelBase], active: active || item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
<span :class="ui.itemLabelSuffix({ class: props.ui?.itemLabelSuffix, active: active || item.active })" v-html="item.suffixHtml || item.suffix" />
<span :class="ui.itemLabelSuffix({ class: [props.ui?.itemLabelSuffix, item.ui?.itemLabelSuffix], active: active || item.active })" v-html="item.suffixHtml || item.suffix" />
</slot>
</span>
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, item.ui?.itemTrailing] })">
<slot :name="((item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
<span v-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: props.ui?.itemTrailingKbds })">
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
<span v-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: [props.ui?.itemTrailingKbds, item.ui?.itemTrailingKbds] })">
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((item.ui?.itemTrailingKbdsSize || props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
</span>
<UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: props.ui?.itemTrailingHighlightedIcon })" />
<UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: [props.ui?.itemTrailingHighlightedIcon, item.ui?.itemTrailingHighlightedIcon] })" />
</slot>
<ListboxItemIndicator as-child>
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })" />
</ListboxItemIndicator>
</span>
</slot>

View File

@@ -32,6 +32,8 @@ export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custo
children?: ArrayOrNested<ContextMenuItem>
onSelect?(e: Event): void
onUpdateChecked?(checked: boolean): void
class?: any
ui?: Pick<ContextMenu['slots'], 'item' | 'label' | 'separator' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLabel' | 'itemLabelExternalIcon' | 'itemTrailing' | 'itemTrailingIcon' | 'itemTrailingKbds' | 'itemTrailingKbdsSize'>
[key: string]: any
}

View File

@@ -77,29 +77,29 @@ const groups = computed<ContextMenuItem[][]>(() =>
<DefineItemTemplate v-slot="{ item, active, index }">
<slot :name="((item.slot || 'item') as keyof ContextMenuSlots<T>)" :item="item" :index="index">
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: [uiOverride?.itemLeadingIcon, item.ui?.itemLeadingIcon], color: item?.color, loading: true })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [uiOverride?.itemLeadingIcon, item.ui?.itemLeadingIcon], color: item?.color, active })" />
<UAvatar v-else-if="item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [uiOverride?.itemLeadingAvatar, item.ui?.itemLeadingAvatar], active })" />
</slot>
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>]" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>]" :class="ui.itemLabel({ class: [uiOverride?.itemLabel, item.ui?.itemLabel], active })">
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
{{ get(item, props.labelKey as string) }}
</slot>
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, color: item?.color, active })" />
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: [uiOverride?.itemLabelExternalIcon, item.ui?.itemLabelExternalIcon], color: item?.color, active })" />
</span>
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
<span :class="ui.itemTrailing({ class: [uiOverride?.itemTrailing, item.ui?.itemTrailing] })">
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
<UIcon v-if="item.children?.length" :name="childrenIcon" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
<UIcon v-if="item.children?.length" :name="childrenIcon" :class="ui.itemTrailingIcon({ class: [uiOverride?.itemTrailingIcon, item.ui?.itemTrailingIcon], color: item?.color, active })" />
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: [uiOverride?.itemTrailingKbds, item.ui?.itemTrailingKbds] })">
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((item.ui?.itemTrailingKbdsSize || props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
</span>
</slot>
<ContextMenu.ItemIndicator as-child>
<UIcon :name="checkedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color })" />
<UIcon :name="checkedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [uiOverride?.itemTrailingIcon, item.ui?.itemTrailingIcon], color: item?.color })" />
</ContextMenu.ItemIndicator>
</span>
</slot>
@@ -109,68 +109,70 @@ const groups = computed<ContextMenuItem[][]>(() =>
<component :is="sub ? ContextMenu.SubContent : ContextMenu.Content" :class="props.class" v-bind="contentProps">
<slot name="content-top" />
<ContextMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<ContextMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: uiOverride?.label })">
<ReuseItemTemplate :item="item" :index="index" />
</ContextMenu.Label>
<ContextMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: uiOverride?.separator })" />
<ContextMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
<ContextMenu.SubTrigger
as="button"
type="button"
<div role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
<ContextMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<ContextMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
<ReuseItemTemplate :item="item" :index="index" />
</ContextMenu.Label>
<ContextMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
<ContextMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
<ContextMenu.SubTrigger
as="button"
type="button"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
>
<ReuseItemTemplate :item="item" :index="index" />
</ContextMenu.SubTrigger>
<UContextMenuContent
sub
:class="props.class"
:ui="ui"
:ui-override="uiOverride"
:portal="portal"
:items="(item.children as T)"
:align-offset="-4"
:label-key="labelKey"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
:external-icon="externalIcon"
v-bind="item.content"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData">
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
</template>
</UContextMenuContent>
</ContextMenu.Sub>
<ContextMenu.CheckboxItem
v-else-if="item.type === 'checkbox'"
:model-value="item.checked"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: uiOverride?.item, color: item?.color })"
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
@update:model-value="item.onUpdateChecked"
@select="item.onSelect"
>
<ReuseItemTemplate :item="item" :index="index" />
</ContextMenu.SubTrigger>
<UContextMenuContent
sub
:class="props.class"
:ui="ui"
:ui-override="uiOverride"
:portal="portal"
:items="(item.children as T)"
:align-offset="-4"
:label-key="labelKey"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
:external-icon="externalIcon"
v-bind="item.content"
</ContextMenu.CheckboxItem>
<ContextMenu.Item
v-else
as-child
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
@select="item.onSelect"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData">
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
</template>
</UContextMenuContent>
</ContextMenu.Sub>
<ContextMenu.CheckboxItem
v-else-if="item.type === 'checkbox'"
:model-value="item.checked"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.class], color: item?.color })"
@update:model-value="item.onUpdateChecked"
@select="item.onSelect"
>
<ReuseItemTemplate :item="item" :index="index" />
</ContextMenu.CheckboxItem>
<ContextMenu.Item
v-else
as-child
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
@select="item.onSelect"
>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<ContextMenuItem, 'type'>)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.class], active, color: item?.color })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />
</ULinkBase>
</ULink>
</ContextMenu.Item>
</template>
</ContextMenu.Group>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<ContextMenuItem, 'type'>)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], active, color: item?.color })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />
</ULinkBase>
</ULink>
</ContextMenu.Item>
</template>
</ContextMenu.Group>
</div>
<slot />

View File

@@ -56,7 +56,7 @@ export interface DrawerSlots {
<script setup lang="ts">
import { computed, toRef } from 'vue'
import { useForwardPropsEmits } from 'reka-ui'
import { VisuallyHidden, useForwardPropsEmits } from 'reka-ui'
import { DrawerRoot, DrawerTrigger, DrawerPortal, DrawerOverlay, DrawerContent, DrawerTitle, DrawerDescription, DrawerHandle } from 'vaul-vue'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
@@ -101,6 +101,20 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.drawer || {}
<DrawerContent :class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })" v-bind="contentProps" v-on="contentEvents">
<DrawerHandle v-if="handle" :class="ui.handle({ class: props.ui?.handle })" />
<VisuallyHidden v-if="!!slots.content && ((title || !!slots.title) || (description || !!slots.description))">
<DrawerTitle v-if="title || !!slots.title">
<slot name="title">
{{ title }}
</slot>
</DrawerTitle>
<DrawerDescription v-if="description || !!slots.description">
<slot name="description">
{{ description }}
</slot>
</DrawerDescription>
</VisuallyHidden>
<slot name="content">
<div :class="ui.container({ class: props.ui?.container })">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description)" :class="ui.header({ class: props.ui?.header })">

View File

@@ -32,6 +32,8 @@ export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cust
children?: ArrayOrNested<DropdownMenuItem>
onSelect?(e: Event): void
onUpdateChecked?(checked: boolean): void
class?: any
ui?: Pick<DropdownMenu['slots'], 'item' | 'label' | 'separator' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLabel' | 'itemLabelExternalIcon' | 'itemTrailing' | 'itemTrailingIcon' | 'itemTrailingKbds' | 'itemTrailingKbdsSize'>
[key: string]: any
}

View File

@@ -83,29 +83,29 @@ const groups = computed<DropdownMenuItem[][]>(() =>
<DefineItemTemplate v-slot="{ item, active, index }">
<slot :name="((item.slot || 'item') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :index="index">
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: [uiOverride?.itemLeadingIcon, item.ui?.itemLeadingIcon], color: item?.color, loading: true })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [uiOverride?.itemLeadingIcon, item.ui?.itemLeadingIcon], color: item?.color, active })" />
<UAvatar v-else-if="item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [uiOverride?.itemLeadingAvatar, item.ui?.itemLeadingAvatar], active })" />
</slot>
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof DropdownMenuContentSlots<T>]" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof DropdownMenuContentSlots<T>]" :class="ui.itemLabel({ class: [uiOverride?.itemLabel, item.ui?.itemLabel], active })">
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
{{ get(item, props.labelKey as string) }}
</slot>
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, color: item?.color, active })" />
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: [uiOverride?.itemLabelExternalIcon, item.ui?.itemLabelExternalIcon], color: item?.color, active })" />
</span>
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
<span :class="ui.itemTrailing({ class: [uiOverride?.itemTrailing, item.ui?.itemTrailing] })">
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
<UIcon v-if="item.children?.length" :name="childrenIcon" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
<UIcon v-if="item.children?.length" :name="childrenIcon" :class="ui.itemTrailingIcon({ class: [uiOverride?.itemTrailingIcon, item.ui?.itemTrailingIcon], color: item?.color, active })" />
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: [uiOverride?.itemTrailingKbds, item.ui?.itemTrailingKbds] })">
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((item.ui?.itemTrailingKbdsSize || props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
</span>
</slot>
<DropdownMenu.ItemIndicator as-child>
<UIcon :name="checkedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color })" />
<UIcon :name="checkedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [uiOverride?.itemTrailingIcon, item.ui?.itemTrailingIcon], color: item?.color })" />
</DropdownMenu.ItemIndicator>
</span>
</slot>
@@ -115,70 +115,72 @@ const groups = computed<DropdownMenuItem[][]>(() =>
<component :is="sub ? DropdownMenu.SubContent : DropdownMenu.Content" :class="props.class" v-bind="contentProps">
<slot name="content-top" />
<DropdownMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<DropdownMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: uiOverride?.label })">
<ReuseItemTemplate :item="item" :index="index" />
</DropdownMenu.Label>
<DropdownMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: uiOverride?.separator })" />
<DropdownMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
<DropdownMenu.SubTrigger
as="button"
type="button"
<div role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
<DropdownMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<DropdownMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: [uiOverride?.label, item.ui?.label, item.class] })">
<ReuseItemTemplate :item="item" :index="index" />
</DropdownMenu.Label>
<DropdownMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: [uiOverride?.separator, item.ui?.separator, item.class] })" />
<DropdownMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
<DropdownMenu.SubTrigger
as="button"
type="button"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
>
<ReuseItemTemplate :item="item" :index="index" />
</DropdownMenu.SubTrigger>
<UDropdownMenuContent
sub
:class="props.class"
:ui="ui"
:ui-override="uiOverride"
:portal="portal"
:items="(item.children as T)"
align="start"
:align-offset="-4"
:side-offset="3"
:label-key="labelKey"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
:external-icon="externalIcon"
v-bind="item.content"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData">
<slot :name="(name as keyof DropdownMenuContentSlots<T>)" v-bind="slotData" />
</template>
</UDropdownMenuContent>
</DropdownMenu.Sub>
<DropdownMenu.CheckboxItem
v-else-if="item.type === 'checkbox'"
:model-value="item.checked"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: uiOverride?.item, color: item?.color })"
:class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color })"
@update:model-value="item.onUpdateChecked"
@select="item.onSelect"
>
<ReuseItemTemplate :item="item" :index="index" />
</DropdownMenu.SubTrigger>
<UDropdownMenuContent
sub
:class="props.class"
:ui="ui"
:ui-override="uiOverride"
:portal="portal"
:items="(item.children as T)"
align="start"
:align-offset="-4"
:side-offset="3"
:label-key="labelKey"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
:external-icon="externalIcon"
v-bind="item.content"
</DropdownMenu.CheckboxItem>
<DropdownMenu.Item
v-else
as-child
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
@select="item.onSelect"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData">
<slot :name="(name as keyof DropdownMenuContentSlots<T>)" v-bind="slotData" />
</template>
</UDropdownMenuContent>
</DropdownMenu.Sub>
<DropdownMenu.CheckboxItem
v-else-if="item.type === 'checkbox'"
:model-value="item.checked"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.class], color: item?.color })"
@update:model-value="item.onUpdateChecked"
@select="item.onSelect"
>
<ReuseItemTemplate :item="item" :index="index" />
</DropdownMenu.CheckboxItem>
<DropdownMenu.Item
v-else
as-child
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
@select="item.onSelect"
>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<DropdownMenuItem, 'type'>)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.class], color: item?.color, active })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />
</ULinkBase>
</ULink>
</DropdownMenu.Item>
</template>
</DropdownMenu.Group>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<DropdownMenuItem, 'type'>)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.ui?.item, item.class], color: item?.color, active })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />
</ULinkBase>
</ULink>
</DropdownMenu.Item>
</template>
</DropdownMenu.Group>
</div>
<slot />

View File

@@ -2,12 +2,12 @@
import type { DeepReadonly } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/form'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput } from '../types/form'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput, FormData } from '../types/form'
import type { ComponentConfig } from '../types/utils'
type FormConfig = ComponentConfig<typeof theme, AppConfig, 'form'>
export interface FormProps<S extends FormSchema> {
export interface FormProps<S extends FormSchema, T extends boolean = true> {
id?: string | number
/** Schema to validate the form state. Supports Standard Schema objects, Yup, Joi, and Superstructs. */
schema?: S
@@ -35,7 +35,7 @@ export interface FormProps<S extends FormSchema> {
* If true, schema transformations will be applied to the state on submit.
* @defaultValue `true`
*/
transform?: boolean
transform?: T
/**
* If true, this form will attach to its parent Form (if any) and validate at the same time.
@@ -50,11 +50,11 @@ export interface FormProps<S extends FormSchema> {
*/
loadingAuto?: boolean
class?: any
onSubmit?: ((event: FormSubmitEvent<InferOutput<S>>) => void | Promise<void>) | (() => void | Promise<void>)
onSubmit?: ((event: FormSubmitEvent<FormData<S, T>>) => void | Promise<void>) | (() => void | Promise<void>)
}
export interface FormEmits<S extends FormSchema> {
(e: 'submit', payload: FormSubmitEvent<InferOutput<S>>): void
export interface FormEmits<S extends FormSchema, T extends boolean = true> {
(e: 'submit', payload: FormSubmitEvent<FormData<S, T>>): void
(e: 'error', payload: FormErrorEvent): void
}
@@ -63,7 +63,7 @@ export interface FormSlots {
}
</script>
<script lang="ts" setup generic="S extends FormSchema">
<script lang="ts" setup generic="S extends FormSchema, T extends boolean = true">
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
import { useEventBus } from '@vueuse/core'
import { useAppConfig } from '#imports'
@@ -75,17 +75,17 @@ import { FormValidationException } from '../types/form'
type I = InferInput<S>
type O = InferOutput<S>
const props = withDefaults(defineProps<FormProps<S>>(), {
const props = withDefaults(defineProps<FormProps<S, T>>(), {
validateOn() {
return ['input', 'blur', 'change'] as FormInputEvents[]
},
validateOnInputDelay: 300,
attach: true,
transform: true,
transform: () => true as T,
loadingAuto: true
})
const emits = defineEmits<FormEmits<S>>()
const emits = defineEmits<FormEmits<S, T>>()
defineSlots<FormSlots>()
const appConfig = useAppConfig() as FormConfig['AppConfig']
@@ -183,10 +183,10 @@ async function getErrors(): Promise<FormErrorWithId[]> {
return resolveErrorIds(errs)
}
type ValidateOpts<Silent extends boolean> = { name?: keyof I | (keyof I)[], silent?: Silent, nested?: boolean, transform?: boolean }
async function _validate(opts: ValidateOpts<false>): Promise<O>
async function _validate(opts: ValidateOpts<true>): Promise<O | false>
async function _validate(opts: ValidateOpts<boolean> = { silent: false, nested: true, transform: false }): Promise<O | false> {
type ValidateOpts<Silent extends boolean, Transform extends boolean> = { name?: keyof I | (keyof I)[], silent?: Silent, nested?: boolean, transform?: Transform }
async function _validate<T extends boolean>(opts: ValidateOpts<false, T>): Promise<FormData<S, T>>
async function _validate<T extends boolean>(opts: ValidateOpts<true, T>): Promise<FormData<S, T> | false>
async function _validate<T extends boolean>(opts: ValidateOpts<boolean, boolean> = { silent: false, nested: true, transform: false }): Promise<FormData<S, T> | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof O)[]
const nestedValidatePromises = !names && opts.nested
@@ -227,7 +227,7 @@ async function _validate(opts: ValidateOpts<boolean> = { silent: false, nested:
Object.assign(props.state, transformedState.value)
}
return props.state as O
return props.state as FormData<S, T>
}
const loading = ref(false)
@@ -236,7 +236,7 @@ provide(formLoadingInjectionKey, readonly(loading))
async function onSubmitWrapper(payload: Event) {
loading.value = props.loadingAuto && true
const event = payload as FormSubmitEvent<O>
const event = payload as FormSubmitEvent<FormData<S, T>>
try {
event.data = await _validate({ nested: true, transform: props.transform })
@@ -265,7 +265,7 @@ provide(formOptionsInjectionKey, computed(() => ({
validateOnInputDelay: props.validateOnInputDelay
})))
defineExpose<Form<I>>({
defineExpose<Form<S>>({
validate: _validate,
errors,

View File

@@ -115,16 +115,16 @@ provide(formFieldInjectionKey, computed(() => ({
<div :class="[(label || !!slots.label || description || !!slots.description) && ui.container({ class: props.ui?.container })]">
<slot :error="error" />
<p v-if="(typeof error === 'string' && error) || !!slots.error" :id="`${ariaId}-error`" :class="ui.error({ class: props.ui?.error })">
<div v-if="(typeof error === 'string' && error) || !!slots.error" :id="`${ariaId}-error`" :class="ui.error({ class: props.ui?.error })">
<slot name="error" :error="error">
{{ error }}
</slot>
</p>
<p v-else-if="help || !!slots.help" :class="ui.help({ class: props.ui?.help })">
</div>
<div v-else-if="help || !!slots.help" :class="ui.help({ class: props.ui?.help })">
<slot name="help" :help="help">
{{ help }}
</slot>
</p>
</div>
</div>
</Primitive>
</template>

View File

@@ -4,7 +4,7 @@ import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/input'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps } from '../types'
import type { ComponentConfig } from '../types/utils'
import type { AcceptableValue, ComponentConfig } from '../types/utils'
type Input = ComponentConfig<typeof theme, AppConfig, 'input'>
@@ -38,12 +38,19 @@ export interface InputProps extends UseComponentIconsProps {
disabled?: boolean
/** Highlight the ring color like a focus state. */
highlight?: boolean
modelModifiers?: {
string?: boolean
number?: boolean
trim?: boolean
lazy?: boolean
nullify?: boolean
}
class?: any
ui?: Input['slots']
}
export interface InputEmits {
(e: 'update:modelValue', payload: string | number): void
export interface InputEmits<T extends AcceptableValue = AcceptableValue> {
(e: 'update:modelValue', payload: T): void
(e: 'blur', event: FocusEvent): void
(e: 'change', event: Event): void
}
@@ -55,7 +62,7 @@ export interface InputSlots {
}
</script>
<script setup lang="ts">
<script setup lang="ts" generic="T extends AcceptableValue">
import { ref, computed, onMounted } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
@@ -74,12 +81,14 @@ const props = withDefaults(defineProps<InputProps>(), {
autocomplete: 'off',
autofocusDelay: 0
})
const emits = defineEmits<InputEmits>()
const emits = defineEmits<InputEmits<T>>()
const slots = defineSlots<InputSlots>()
const [modelValue, modelModifiers] = defineModel<string | number | null>()
// eslint-disable-next-line vue/no-dupe-keys
const [modelValue, modelModifiers] = defineModel<T>()
const appConfig = useAppConfig() as Input['AppConfig']
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps>(props, { deferInputValidation: true })
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
@@ -114,7 +123,7 @@ function updateInput(value: string | null) {
value ||= null
}
modelValue.value = value
modelValue.value = value as T
emitFormInput()
}

View File

@@ -24,6 +24,8 @@ interface _InputMenuItem {
type?: 'label' | 'separator' | 'item'
disabled?: boolean
onSelect?(e?: Event): void
class?: any
ui?: Pick<InputMenu['slots'], 'tagsItem' | 'tagsItemText' | 'tagsItemDelete' | 'tagsItemDeleteIcon' | 'label' | 'separator' | 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChip' | 'itemLeadingChipSize' | 'itemLabel' | 'itemTrailing' | 'itemTrailingIcon'>
[key: string]: any
}
export type InputMenuItem = _InputMenuItem | AcceptableValue | boolean
@@ -170,7 +172,7 @@ export interface InputMenuSlots<
<script setup lang="ts" generic="T extends ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false">
import { computed, ref, toRef, onMounted, toRaw } from 'vue'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits, useFilter } from 'reka-ui'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits, useFilter } from 'reka-ui'
import { defu } from 'defu'
import { isEqual } from 'ohash/utils'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
@@ -426,16 +428,16 @@ defineExpose({
@focus="onFocus"
@remove-tag="onRemoveTag"
>
<TagsInputItem v-for="(item, index) in tags" :key="index" :value="item" :class="ui.tagsItem({ class: props.ui?.tagsItem })">
<TagsInputItemText :class="ui.tagsItemText({ class: props.ui?.tagsItemText })">
<TagsInputItem v-for="(item, index) in tags" :key="index" :value="item" :class="ui.tagsItem({ class: [props.ui?.tagsItem, isInputItem(item) && item.ui?.tagsItem] })">
<TagsInputItemText :class="ui.tagsItemText({ class: [props.ui?.tagsItemText, isInputItem(item) && item.ui?.tagsItemText] })">
<slot name="tags-item-text" :item="(item as NestedItem<T>)" :index="index">
{{ displayValue(item as T) }}
</slot>
</TagsInputItemText>
<TagsInputItemDelete :class="ui.tagsItemDelete({ class: props.ui?.tagsItemDelete })" :disabled="disabled">
<TagsInputItemDelete :class="ui.tagsItemDelete({ class: [props.ui?.tagsItemDelete, isInputItem(item) && item.ui?.tagsItemDelete] })" :disabled="disabled">
<slot name="tags-item-delete" :item="(item as NestedItem<T>)" :index="index">
<UIcon :name="deleteIcon || appConfig.ui.icons.close" :class="ui.tagsItemDeleteIcon({ class: props.ui?.tagsItemDeleteIcon })" />
<UIcon :name="deleteIcon || appConfig.ui.icons.close" :class="ui.tagsItemDeleteIcon({ class: [props.ui?.tagsItemDeleteIcon, isInputItem(item) && item.ui?.tagsItemDeleteIcon] })" />
</slot>
</TagsInputItemDelete>
</TagsInputItem>
@@ -488,49 +490,49 @@ defineExpose({
</slot>
</ComboboxEmpty>
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
<div role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'top'" />
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<ComboboxLabel v-if="isInputItem(item) && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
<ComboboxLabel v-if="isInputItem(item) && item.type === 'label'" :class="ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })">
{{ get(item, props.labelKey as string) }}
</ComboboxLabel>
<ComboboxSeparator v-else-if="isInputItem(item) && item.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
<ComboboxSeparator v-else-if="isInputItem(item) && item.type === 'separator'" :class="ui.separator({ class: [props.ui?.separator, item.ui?.separator, item.class] })" />
<ComboboxItem
v-else
:class="ui.item({ class: props.ui?.item })"
:class="ui.item({ class: [props.ui?.item, isInputItem(item) && item.ui?.item, isInputItem(item) && item.class] })"
:disabled="isInputItem(item) && item.disabled"
:value="props.valueKey && isInputItem(item) ? get(item, props.valueKey as string) : item"
@select="onSelect($event, item)"
>
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
<UIcon v-if="isInputItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
<UAvatar v-else-if="isInputItem(item) && item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
<UIcon v-if="isInputItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon] })" />
<UAvatar v-else-if="isInputItem(item) && item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [props.ui?.itemLeadingAvatar, item.ui?.itemLeadingAvatar] })" />
<UChip
v-else-if="isInputItem(item) && item.chip"
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
:size="((item.ui?.itemLeadingChipSize || props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
inset
standalone
v-bind="item.chip"
:class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip })"
:class="ui.itemLeadingChip({ class: [props.ui?.itemLeadingChip, item.ui?.itemLeadingChip] })"
/>
</slot>
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
<span :class="ui.itemLabel({ class: [props.ui?.itemLabel, isInputItem(item) && item.ui?.itemLabel] })">
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
{{ isInputItem(item) ? get(item, props.labelKey as string) : item }}
</slot>
</span>
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, isInputItem(item) && item.ui?.itemTrailing] })">
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
<ComboboxItemIndicator as-child>
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, isInputItem(item) && item.ui?.itemTrailingIcon] })" />
</ComboboxItemIndicator>
</span>
</slot>
@@ -539,7 +541,7 @@ defineExpose({
</ComboboxGroup>
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'bottom'" />
</ComboboxViewport>
</div>
<slot name="content-bottom" />

View File

@@ -7,7 +7,7 @@ import type { ComponentConfig } from '../types/utils'
type InputNumber = ComponentConfig<typeof theme, AppConfig, 'inputNumber'>
export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue' | 'defaultValue' | 'min' | 'max' | 'step' | 'stepSnapping' | 'disabled' | 'required' | 'id' | 'name' | 'formatOptions' | 'disableWheelChange'> {
export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue' | 'defaultValue' | 'min' | 'max' | 'step' | 'stepSnapping' | 'disabled' | 'required' | 'id' | 'name' | 'formatOptions' | 'disableWheelChange' | 'invertWheelChange'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -36,6 +36,8 @@ export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue
* @IconifyIcon
*/
incrementIcon?: string
/** Disable the increment button. */
incrementDisabled?: boolean
/**
* Configure the decrement button. The `color` and `size` are inherited.
* @defaultValue { variant: 'link' }
@@ -47,6 +49,8 @@ export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue
* @IconifyIcon
*/
decrementIcon?: string
/** Disable the decrement button. */
decrementDisabled?: boolean
autofocus?: boolean
autofocusDelay?: number
/**
@@ -75,6 +79,7 @@ import { onMounted, ref, computed } from 'vue'
import { NumberFieldRoot, NumberFieldInput, NumberFieldDecrement, NumberFieldIncrement, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'
import { tv } from '../utils/tv'
@@ -83,26 +88,31 @@ import UButton from './Button.vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<InputNumberProps>(), {
orientation: 'horizontal'
orientation: 'horizontal',
disabledIncrement: false,
disabledDecrement: false
})
const emits = defineEmits<InputNumberEmits>()
defineSlots<InputNumberSlots>()
const { t, code: codeLocale } = useLocale()
const appConfig = useAppConfig() as InputNumber['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange', 'invertWheelChange'), emits)
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
const { orientation, size: buttonGroupSize } = useButtonGroup<InputNumberProps>(props)
const { t, code: codeLocale } = useLocale()
const locale = computed(() => props.locale || codeLocale.value)
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputNumber || {}) })({
color: color.value,
variant: props.variant,
size: size.value,
size: inputSize.value,
highlight: highlight.value,
orientation: props.orientation
orientation: props.orientation,
buttonGroup: orientation.value
}))
const incrementIcon = computed(() => props.incrementIcon || (props.orientation === 'horizontal' ? appConfig.ui.icons.plus : appConfig.ui.icons.chevronUp))
@@ -162,7 +172,7 @@ defineExpose({
/>
<div :class="ui.increment({ class: props.ui?.increment })">
<NumberFieldIncrement as-child :disabled="disabled">
<NumberFieldIncrement as-child :disabled="disabled || incrementDisabled">
<slot name="increment">
<UButton
:icon="incrementIcon"
@@ -177,7 +187,7 @@ defineExpose({
</div>
<div :class="ui.decrement({ class: props.ui?.decrement })">
<NumberFieldDecrement as-child :disabled="disabled">
<NumberFieldDecrement as-child :disabled="disabled || decrementDisabled">
<slot name="decrement">
<UButton
:icon="decrementIcon"

View File

@@ -89,11 +89,12 @@ export interface LinkSlots {
<script setup lang="ts">
import { computed } from 'vue'
import { defu } from 'defu'
import { isEqual, diff } from 'ohash/utils'
import { isEqual } from 'ohash/utils'
import { useForwardProps } from 'reka-ui'
import { reactiveOmit } from '@vueuse/core'
import { useRoute, useAppConfig } from '#imports'
import { tv } from '../utils/tv'
import { isPartiallyEqual } from '../utils/link'
import ULinkBase from './LinkBase.vue'
defineOptions({ inheritAttrs: false })
@@ -111,7 +112,7 @@ defineSlots<LinkSlots>()
const route = useRoute()
const appConfig = useAppConfig() as Link['AppConfig']
const nuxtLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'raw', 'class'))
const nuxtLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'to', 'href', 'raw', 'custom', 'class'))
const ui = computed(() => tv({
extend: tv(theme),
@@ -125,19 +126,7 @@ const ui = computed(() => tv({
}, appConfig.ui?.link || {})
}))
function isPartiallyEqual(item1: any, item2: any) {
const diffedKeys = diff(item1, item2).reduce((filtered, q) => {
if (q.type === 'added') {
filtered.add(q.key)
}
return filtered
}, new Set<string>())
const item1Filtered = Object.fromEntries(Object.entries(item1).filter(([key]) => !diffedKeys.has(key)))
const item2Filtered = Object.fromEntries(Object.entries(item2).filter(([key]) => !diffedKeys.has(key)))
return isEqual(item1Filtered, item2Filtered)
}
const to = computed(() => props.to ?? props.href)
function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
if (props.active !== undefined) {
@@ -177,7 +166,7 @@ function resolveLinkClass({ route, isActive, isExactActive }: any) {
</script>
<template>
<NuxtLink v-slot="{ href, navigate, route: linkRoute, rel, target, isExternal, isActive, isExactActive }" v-bind="nuxtLinkProps" custom>
<NuxtLink v-slot="{ href, navigate, route: linkRoute, rel, target, isExternal, isActive, isExactActive }" v-bind="nuxtLinkProps" :to="to" custom>
<template v-if="custom">
<slot
v-bind="{

View File

@@ -55,18 +55,19 @@ export interface ModalProps extends DialogRootProps {
export interface ModalEmits extends DialogRootEmits {
'after:leave': []
'after:enter': []
'close:prevent': []
}
export interface ModalSlots {
default(props: { open: boolean }): any
content(props?: {}): any
header(props?: {}): any
content(props: { close: () => void }): any
header(props: { close: () => void }): any
title(props?: {}): any
description(props?: {}): any
close(props: { ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any
body(props?: {}): any
footer(props?: {}): any
close(props: { close: () => void, ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any
body(props: { close: () => void }): any
footer(props: { close: () => void }): any
}
</script>
@@ -123,8 +124,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
}))
</script>
<!-- eslint-disable vue/no-template-shadow -->
<template>
<DialogRoot v-slot="{ open }" v-bind="rootProps">
<DialogRoot v-slot="{ open, close }" v-bind="rootProps">
<DialogTrigger v-if="!!slots.default" as-child :class="props.class">
<slot :open="open" />
</DialogTrigger>
@@ -132,7 +134,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
<DialogPortal v-bind="portalProps">
<DialogOverlay v-if="overlay" :class="ui.overlay({ class: props.ui?.overlay })" />
<DialogContent :class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })" v-bind="contentProps" @after-leave="emits('after:leave')" v-on="contentEvents">
<DialogContent :class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })" v-bind="contentProps" @after-enter="emits('after:enter')" @after-leave="emits('after:leave')" v-on="contentEvents">
<VisuallyHidden v-if="!!slots.content && ((title || !!slots.title) || (description || !!slots.description))">
<DialogTitle v-if="title || !!slots.title">
<slot name="title">
@@ -147,9 +149,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</DialogDescription>
</VisuallyHidden>
<slot name="content">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header">
<slot name="content" :close="close">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (props.close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header" :close="close">
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
<DialogTitle v-if="title || !!slots.title" :class="ui.title({ class: props.ui?.title })">
<slot name="title">
@@ -164,16 +166,16 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</DialogDescription>
</div>
<DialogClose v-if="close || !!slots.close" as-child>
<slot name="close" :ui="ui">
<DialogClose v-if="props.close || !!slots.close" as-child>
<slot name="close" :close="close" :ui="ui">
<UButton
v-if="close"
v-if="props.close"
:icon="closeIcon || appConfig.ui.icons.close"
size="md"
color="neutral"
variant="ghost"
:aria-label="t('modal.close')"
v-bind="(typeof close === 'object' ? close as Partial<ButtonProps> : {})"
v-bind="(typeof props.close === 'object' ? props.close as Partial<ButtonProps> : {})"
:class="ui.close({ class: props.ui?.close })"
/>
</slot>
@@ -182,11 +184,11 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</div>
<div v-if="!!slots.body" :class="ui.body({ class: props.ui?.body })">
<slot name="body" />
<slot name="body" :close="close" />
</div>
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
<slot name="footer" />
<slot name="footer" :close="close" />
</div>
</slot>
</DialogContent>

View File

@@ -1,20 +1,20 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, NavigationMenuContentEmits, CollapsibleRootProps } from 'reka-ui'
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, NavigationMenuContentEmits, AccordionRootProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/navigation-menu'
import type { AvatarProps, BadgeProps, LinkProps, TooltipProps } from '../types'
import type { AvatarProps, BadgeProps, LinkProps, PopoverProps, TooltipProps } from '../types'
import type { ArrayOrNested, DynamicSlots, MergeTypes, NestedItem, EmitsToProps, ComponentConfig } from '../types/utils'
type NavigationMenu = ComponentConfig<typeof theme, AppConfig, 'navigationMenu'>
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type'> {
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type' | 'ui'> {
/** Description is only used when `orientation` is `horizontal`. */
description?: string
[key: string]: any
}
export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'> {
label?: string
/**
* @IconifyIcon
@@ -27,35 +27,42 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
*/
badge?: string | number | BadgeProps
/**
* Display a tooltip on the item.
* Only works when `type` is `link`.
* `{ content: { side: 'right' } }`{lang="ts-type"}
* Display a tooltip on the item when the menu is collapsed with the label of the item.
* This has priority over the global `tooltip` prop.
*/
tooltip?: TooltipProps
tooltip?: boolean | TooltipProps
/**
* Display a popover on the item when the menu is collapsed with the children list.
* This has priority over the global `popover` prop.
*/
popover?: boolean | PopoverProps
/**
* @IconifyIcon
*/
trailingIcon?: string
/**
* The type of the item.
* The `label` type only works on `vertical` orientation.
* The `label` type is only displayed in `vertical` orientation.
* The `trigger` type is used to force the item to be collapsible when its a link in `vertical` orientation.
* @defaultValue 'link'
*/
type?: 'label' | 'link'
type?: 'label' | 'trigger' | 'link'
slot?: string
value?: string
/**
* Make the item collapsible.
* Only works when `orientation` is `vertical`.
* @defaultValue true
* The value of the item. Avoid using `index` as the value to prevent conflicts in horizontal orientation with Reka UI.
* @defaultValue `item-${index}`
*/
collapsible?: boolean
value?: string
children?: NavigationMenuChildItem[]
defaultOpen?: boolean
open?: boolean
onSelect?(e: Event): void
class?: any
ui?: Pick<NavigationMenu['slots'], 'item' | 'linkLeadingAvatarSize' | 'linkLeadingAvatar' | 'linkLeadingIcon' | 'linkLabel' | 'linkLabelExternalIcon' | 'linkTrailing' | 'linkTrailingBadgeSize' | 'linkTrailingBadge' | 'linkTrailingIcon' | 'label' | 'link' | 'content' | 'childList' | 'childLabel' | 'childItem' | 'childLink' | 'childLinkIcon' | 'childLinkWrapper' | 'childLinkLabel' | 'childLinkLabelExternalIcon' | 'childLinkDescription'>
[key: string]: any
}
export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'> {
export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'>, Pick<AccordionRootProps, 'disabled' | 'type' | 'collapsible'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -94,6 +101,18 @@ export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem>
* @defaultValue false
*/
collapsed?: boolean
/**
* Display a tooltip on the items when the menu is collapsed with the label of the item.
* `{ delayDuration: 0, content: { side: 'right' } }`{lang="ts-type"}
* @defaultValue false
*/
tooltip?: boolean | TooltipProps
/**
* Display a popover on the items when the menu is collapsed with the children list.
* `{ mode: 'hover', content: { side: 'right', align: 'start', alignOffset: 2 } }`{lang="ts-type"}
* @defaultValue false
*/
popover?: boolean | PopoverProps
/** Display a line next to the active item. */
highlight?: boolean
/**
@@ -143,8 +162,9 @@ export type NavigationMenuSlots<
<script setup lang="ts" generic="T extends ArrayOrNested<NavigationMenuItem>">
import { computed, toRef } from 'vue'
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'reka-ui'
import { createReusableTemplate } from '@vueuse/core'
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, AccordionRoot, AccordionItem, AccordionTrigger, AccordionContent, useForwardPropsEmits } from 'reka-ui'
import { defu } from 'defu'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { get, isArrayOfArray } from '../utils'
import { tv } from '../utils/tv'
@@ -154,7 +174,7 @@ import ULink from './Link.vue'
import UAvatar from './Avatar.vue'
import UIcon from './Icon.vue'
import UBadge from './Badge.vue'
import UCollapsible from './Collapsible.vue'
import UPopover from './Popover.vue'
import UTooltip from './Tooltip.vue'
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
@@ -162,6 +182,8 @@ const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
contentOrientation: 'horizontal',
externalIcon: true,
delayDuration: 0,
type: 'multiple',
collapsible: true,
unmountOnHide: true,
labelKey: 'label'
})
@@ -182,7 +204,10 @@ const rootProps = useForwardPropsEmits(computed(() => ({
disablePointerLeaveClose: props.disablePointerLeaveClose,
unmountOnHide: props.unmountOnHide
})), emits)
const accordionProps = useForwardPropsEmits(reactivePick(props, 'collapsible', 'disabled', 'type', 'unmountOnHide'), emits)
const contentProps = toRef(() => props.content)
const tooltipProps = toRef(() => defu(typeof props.tooltip === 'boolean' ? {} : props.tooltip, { delayDuration: 0, content: { side: 'right' } }) as TooltipProps)
const popoverProps = toRef(() => defu(typeof props.popover === 'boolean' ? {} : props.popover, { mode: 'hover', content: { side: 'right', align: 'start', alignOffset: 2 } }) as PopoverProps)
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, active?: boolean }>()
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, level?: number }>({
@@ -195,7 +220,7 @@ const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: N
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.navigationMenu || {}) })({
orientation: props.orientation,
contentOrientation: props.contentOrientation,
contentOrientation: props.orientation === 'vertical' ? undefined : props.contentOrientation,
collapsed: props.collapsed,
color: props.color,
variant: props.variant,
@@ -210,92 +235,136 @@ const lists = computed<NavigationMenuItem[][]>(() =>
: [props.items]
: []
)
function getAccordionDefaultValue(list: NavigationMenuItem[]) {
function findItemsWithDefaultOpen(items: NavigationMenuItem[], level = 0): string[] {
return items.reduce((acc: string[], item, index) => {
if (item.defaultOpen || item.open) {
acc.push(item.value || (level > 0 ? `item-${level}-${index}` : `item-${index}`))
}
if (item.children?.length) {
acc.push(...findItemsWithDefaultOpen(item.children, level + 1))
}
return acc
}, [])
}
const indexes = findItemsWithDefaultOpen(list)
return props.type === 'single' ? indexes[0] : indexes
}
</script>
<template>
<DefineLinkTemplate v-slot="{ item, active, index }">
<slot :name="((item.slot || 'item') as keyof NavigationMenuSlots<T>)" :item="item" :index="index">
<slot :name="((item.slot ? `${item.slot}-leading` : 'item-leading') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
<UAvatar v-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active, disabled: !!item.disabled })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
<UAvatar v-if="item.avatar" :size="((item.ui?.linkLeadingAvatarSize || props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: [props.ui?.linkLeadingAvatar, item.ui?.linkLeadingAvatar], active, disabled: !!item.disabled })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon], active, disabled: !!item.disabled })" />
</slot>
<span
v-if="(!collapsed || orientation !== 'vertical') && (get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>])"
:class="ui.linkLabel({ class: props.ui?.linkLabel })"
:class="ui.linkLabel({ class: [props.ui?.linkLabel, item.ui?.linkLabel] })"
>
<slot :name="((item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
{{ get(item, props.labelKey as string) }}
</slot>
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: [props.ui?.linkLabelExternalIcon, item.ui?.linkLabelExternalIcon], active })" />
</span>
<span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[(item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>])" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
<component :is="orientation === 'vertical' && item.children?.length && !collapsed ? AccordionTrigger : 'span'" v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[(item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>])" as="span" :class="ui.linkTrailing({ class: [props.ui?.linkTrailing, item.ui?.linkTrailing] })" @click.stop.prevent>
<slot :name="((item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
<UBadge
v-if="item.badge"
color="neutral"
variant="outline"
:size="((props.ui?.linkTrailingBadgeSize || ui.linkTrailingBadgeSize()) as BadgeProps['size'])"
:size="((item.ui?.linkTrailingBadgeSize || props.ui?.linkTrailingBadgeSize || ui.linkTrailingBadgeSize()) as BadgeProps['size'])"
v-bind="(typeof item.badge === 'string' || typeof item.badge === 'number') ? { label: item.badge } : item.badge"
:class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
:class="ui.linkTrailingBadge({ class: [props.ui?.linkTrailingBadge, item.ui?.linkTrailingBadge] })"
/>
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length && item.collapsible !== false)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
<UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon], active })" />
<UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon], active })" />
</slot>
</span>
</component>
</slot>
</DefineLinkTemplate>
<DefineItemTemplate v-slot="{ item, index, level = 0 }">
<component
:is="(orientation === 'vertical' && item.children?.length) ? UCollapsible : NavigationMenuItem"
:is="(orientation === 'vertical' && !collapsed) ? AccordionItem : NavigationMenuItem"
as="li"
:value="item.value || String(index)"
:default-open="item.defaultOpen"
:disabled="(orientation === 'vertical' && item.children?.length) ? item.collapsible === false : undefined"
:unmount-on-hide="(orientation === 'vertical' && item.children?.length) ? unmountOnHide : undefined"
:open="item.open"
:value="item.value || (level > 0 ? `item-${level}-${index}` : `item-${index}`)"
>
<div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
<div v-if="orientation === 'vertical' && item.type === 'label' && !collapsed" :class="ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })">
<ReuseLinkTemplate :item="item" :index="index" />
</div>
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && item.collapsible !== false) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && !collapsed && item.type === 'trigger') ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
<component
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : NavigationMenuLink"
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : ((orientation === 'vertical' && item.children?.length && !collapsed && !(slotProps as any).href) ? AccordionTrigger : NavigationMenuLink)"
as-child
:active="active || item.active"
:disabled="item.disabled"
@select="item.onSelect"
>
<UTooltip v-if="!!item.tooltip" :content="{ side: 'right' }" v-bind="item.tooltip">
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
<UPopover v-if="orientation === 'vertical' && collapsed && item.children?.length && (!!props.popover || !!item.popover)" v-bind="{ ...popoverProps, ...(typeof item.popover === 'boolean' ? {} : item.popover || {}) }" :ui="{ content: ui.content({ class: [props.ui?.content, item.ui?.content] }) }">
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: level > 0 })">
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
</ULinkBase>
<template #content>
<slot :name="((item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>)" :item="item" :active="active || item.active" :index="index">
<ul :class="ui.childList({ class: [props.ui?.childList, item.ui?.childList] })">
<li :class="ui.childLabel({ class: [props.ui?.childLabel, item.ui?.childLabel] })">
{{ get(item, props.labelKey as string) }}
</li>
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: [props.ui?.childItem, item.ui?.childItem] })">
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
<NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
<ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, item.ui?.childLink, childItem.class], active: childActive })">
<UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: [props.ui?.childLinkIcon, item.ui?.childLinkIcon], active: childActive })" />
<span :class="ui.childLinkLabel({ class: [props.ui?.childLinkLabel, item.ui?.childLinkLabel], active: childActive })">
{{ get(childItem, props.labelKey as string) }}
<UIcon v-if="childItem.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: [props.ui?.childLinkLabelExternalIcon, item.ui?.childLinkLabelExternalIcon], active: childActive })" />
</span>
</ULinkBase>
</NavigationMenuLink>
</ULink>
</li>
</ul>
</slot>
</template>
</UPopover>
<UTooltip v-else-if="orientation === 'vertical' && collapsed && (!!props.tooltip || !!item.tooltip)" :text="get(item, props.labelKey as string)" v-bind="{ ...tooltipProps, ...(typeof item.tooltip === 'boolean' ? {} : item.tooltip || {}) }">
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: level > 0 })">
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
</ULinkBase>
</UTooltip>
<ULinkBase v-else v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
<ULinkBase v-else v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
</ULinkBase>
</component>
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
<slot :name="((item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
<ul :class="ui.childList({ class: props.ui?.childList })">
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])" v-bind="contentProps" :class="ui.content({ class: [props.ui?.content, item.ui?.content] })">
<slot :name="((item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>)" :item="item" :active="active || item.active" :index="index">
<ul :class="ui.childList({ class: [props.ui?.childList, item.ui?.childList] })">
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: [props.ui?.childItem, item.ui?.childItem] })">
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
<NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
<ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, childItem.class], active: childActive })">
<UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: props.ui?.childLinkIcon, active: childActive })" />
<ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, item.ui?.childLink, childItem.class], active: childActive })">
<UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: [props.ui?.childLinkIcon, item.ui?.childLinkIcon], active: childActive })" />
<div :class="ui.childLinkWrapper({ class: props.ui?.childLinkWrapper })">
<p :class="ui.childLinkLabel({ class: props.ui?.childLinkLabel, active: childActive })">
<div :class="ui.childLinkWrapper({ class: [props.ui?.childLinkWrapper, item.ui?.childLinkWrapper] })">
<p :class="ui.childLinkLabel({ class: [props.ui?.childLinkLabel, item.ui?.childLinkLabel], active: childActive })">
{{ get(childItem, props.labelKey as string) }}
<UIcon v-if="childItem.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: props.ui?.childLinkLabelExternalIcon, active: childActive })" />
<UIcon v-if="childItem.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: [props.ui?.childLinkLabelExternalIcon, item.ui?.childLinkLabelExternalIcon], active: childActive })" />
</p>
<p v-if="childItem.description" :class="ui.childLinkDescription({ class: props.ui?.childLinkDescription, active: childActive })">
<p v-if="childItem.description" :class="ui.childLinkDescription({ class: [props.ui?.childLinkDescription, item.ui?.childLinkDescription], active: childActive })">
{{ childItem.description }}
</p>
</div>
@@ -308,7 +377,7 @@ const lists = computed<NavigationMenuItem[][]>(() =>
</NavigationMenuContent>
</ULink>
<template v-if="orientation === 'vertical' && item.children?.length " #content>
<AccordionContent v-if="orientation === 'vertical' && item.children?.length && !collapsed" :class="ui.content({ class: [props.ui?.content, item.ui?.content] })">
<ul :class="ui.childList({ class: props.ui?.childList })">
<ReuseItemTemplate
v-for="(childItem, childIndex) in item.children"
@@ -316,10 +385,10 @@ const lists = computed<NavigationMenuItem[][]>(() =>
:item="childItem"
:index="childIndex"
:level="level + 1"
:class="ui.childItem({ class: props.ui?.childItem })"
:class="ui.childItem({ class: [props.ui?.childItem, childItem.ui?.childItem] })"
/>
</ul>
</template>
</AccordionContent>
</component>
</DefineItemTemplate>
@@ -327,9 +396,17 @@ const lists = computed<NavigationMenuItem[][]>(() =>
<slot name="list-leading" />
<template v-for="(list, listIndex) in lists" :key="`list-${listIndex}`">
<NavigationMenuList :class="ui.list({ class: props.ui?.list })">
<ReuseItemTemplate v-for="(item, index) in list" :key="`list-${listIndex}-${index}`" :item="item" :index="index" :class="ui.item({ class: props.ui?.item })" />
</NavigationMenuList>
<component
v-bind="orientation === 'vertical' && !collapsed ? {
...accordionProps,
defaultValue: getAccordionDefaultValue(list)
} : {}"
:is="orientation === 'vertical' && !collapsed ? AccordionRoot : NavigationMenuList"
as="ul"
:class="ui.list({ class: props.ui?.list })"
>
<ReuseItemTemplate v-for="(item, index) in list" :key="`list-${listIndex}-${index}`" :item="item" :index="index" :class="ui.item({ class: [props.ui?.item, item.ui?.item] })" />
</component>
<div v-if="orientation === 'vertical' && listIndex < lists.length - 1" :class="ui.separator({ class: props.ui?.separator })" />
</template>

View File

@@ -2,13 +2,13 @@
import { computed } from 'vue'
import { useOverlay, type Overlay } from '../composables/useOverlay'
const { overlays, unMount, close } = useOverlay()
const { overlays, unmount, close } = useOverlay()
const mountedOverlays = computed(() => overlays.filter((overlay: Overlay) => overlay.isMounted))
const onAfterLeave = (id: symbol) => {
close(id)
unMount(id)
unmount(id)
}
const onClose = (id: symbol, value: any) => {

View File

@@ -7,7 +7,9 @@ import type { ComponentConfig } from '../types/utils'
type PinInput = ComponentConfig<typeof theme, AppConfig, 'pinInput'>
export interface PinInputProps extends Pick<PinInputRootProps, 'defaultValue' | 'disabled' | 'id' | 'mask' | 'modelValue' | 'name' | 'otp' | 'placeholder' | 'required' | 'type'> {
type PinInputType = 'text' | 'number'
export interface PinInputProps<T extends PinInputType = 'text'> extends Pick<PinInputRootProps<T>, 'defaultValue' | 'disabled' | 'id' | 'mask' | 'modelValue' | 'name' | 'otp' | 'placeholder' | 'required' | 'type'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -37,14 +39,14 @@ export interface PinInputProps extends Pick<PinInputRootProps, 'defaultValue' |
ui?: PinInput['slots']
}
export type PinInputEmits = PinInputRootEmits & {
export type PinInputEmits<T extends PinInputType = 'text'> = PinInputRootEmits<T> & {
change: [payload: Event]
blur: [payload: Event]
}
</script>
<script setup lang="ts">
<script setup lang="ts" generic="T extends PinInputType = 'text'">
import type { ComponentPublicInstance } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { PinInputInput, PinInputRoot, useForwardPropsEmits } from 'reka-ui'
@@ -54,16 +56,16 @@ import { useFormField } from '../composables/useFormField'
import { looseToNumber } from '../utils'
import { tv } from '../utils/tv'
const props = withDefaults(defineProps<PinInputProps>(), {
type: 'text',
const props = withDefaults(defineProps<PinInputProps<T>>(), {
type: 'text' as never,
length: 5,
autofocusDelay: 0
})
const emits = defineEmits<PinInputEmits>()
const emits = defineEmits<PinInputEmits<T>>()
const appConfig = useAppConfig() as PinInput['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'placeholder', 'required', 'type'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'required', 'type'), emits)
const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<PinInputProps>(props)
@@ -77,7 +79,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.pinInput ||
const inputsRef = ref<ComponentPublicInstance[]>([])
const completed = ref(false)
function onComplete(value: string[]) {
function onComplete(value: string[] | number[]) {
// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
emits('change', event)
@@ -113,6 +115,7 @@ defineExpose({
v-bind="{ ...rootProps, ...ariaAttrs }"
:id="id"
:name="name"
:placeholder="placeholder"
:class="ui.root({ class: [props.ui?.root, props.class] })"
@update:model-value="emitFormInput()"
@complete="onComplete"

View File

@@ -43,6 +43,7 @@ export interface PopoverEmits extends PopoverRootEmits {
export interface PopoverSlots {
default(props: { open: boolean }): any
content(props?: {}): any
anchor(props?: {}): any
}
</script>
@@ -103,6 +104,10 @@ const Component = computed(() => props.mode === 'hover' ? HoverCard : Popover)
<slot :open="open" />
</Component.Trigger>
<Component.Anchor v-if="'Anchor' in Component && !!slots.anchor" as-child>
<slot name="anchor" />
</Component.Anchor>
<Component.Portal v-bind="portalProps">
<Component.Content v-bind="contentProps" :class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })" v-on="contentEvents">
<slot name="content" />

View File

@@ -7,7 +7,7 @@ import type { ComponentConfig } from '../types/utils'
type Progress = ComponentConfig<typeof theme, AppConfig, 'progress'>
export interface ProgressProps extends Pick<ProgressRootProps, 'getValueLabel' | 'modelValue'> {
export interface ProgressProps extends Pick<ProgressRootProps, 'getValueLabel' | 'getValueText' | 'modelValue'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -70,7 +70,7 @@ const slots = defineSlots<ProgressSlots>()
const { dir } = useLocale()
const appConfig = useAppConfig() as Progress['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'getValueLabel', 'modelValue'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'getValueLabel', 'getValueText', 'modelValue'), emits)
const isIndeterminate = computed(() => rootProps.value.modelValue === null)
const hasSteps = computed(() => Array.isArray(props.max))

View File

@@ -12,6 +12,8 @@ export type RadioGroupItem = {
description?: string
disabled?: boolean
value?: RadioGroupValue
class?: any
ui?: Pick<RadioGroup['slots'], 'item' | 'container' | 'base' | 'indicator' | 'wrapper' | 'label' | 'description'>
[key: string]: any
} | RadioGroupValue
@@ -176,25 +178,25 @@ function onUpdate(value: any) {
</slot>
</legend>
<component :is="variant === 'list' ? 'div' : Label" v-for="item in normalizedItems" :key="item.value" :class="ui.item({ class: props.ui?.item })">
<div :class="ui.container({ class: props.ui?.container })">
<component :is="(!variant || variant === 'list') ? 'div' : Label" v-for="item in normalizedItems" :key="item.value" :class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class] })">
<div :class="ui.container({ class: [props.ui?.container, item.ui?.container] })">
<RadioGroupItem
:id="item.id"
:value="item.value"
:disabled="item.disabled"
:class="ui.base({ class: props.ui?.base, disabled: item.disabled })"
:class="ui.base({ class: [props.ui?.base, item.ui?.base], disabled: item.disabled })"
>
<RadioGroupIndicator :class="ui.indicator({ class: props.ui?.indicator })" />
<RadioGroupIndicator :class="ui.indicator({ class: [props.ui?.indicator, item.ui?.indicator] })" />
</RadioGroupItem>
</div>
<div v-if="(item.label || !!slots.label) || (item.description || !!slots.description)" :class="ui.wrapper({ class: props.ui?.wrapper })">
<component :is="variant === 'list' ? Label : 'p'" v-if="item.label || !!slots.label" :for="item.id" :class="ui.label({ class: props.ui?.label })">
<div v-if="(item.label || !!slots.label) || (item.description || !!slots.description)" :class="ui.wrapper({ class: [props.ui?.wrapper, item.ui?.wrapper] })">
<component :is="(!variant || variant === 'list') ? Label : 'p'" v-if="item.label || !!slots.label" :for="item.id" :class="ui.label({ class: [props.ui?.label, item.ui?.label] })">
<slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">
{{ item.label }}
</slot>
</component>
<p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
<p v-if="item.description || !!slots.description" :class="ui.description({ class: [props.ui?.description, item.ui?.description] })">
<slot name="description" :item="item" :model-value="(modelValue as RadioGroupValue)">
{{ item.description }}
</slot>

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