Compare commits

...

162 Commits

Author SHA1 Message Date
HugoRCD
259a43930c up 2025-07-07 11:51:34 +02:00
HugoRCD
b7ab65b0c9 Merge remote-tracking branch 'origin/v3' into feat/custom-slots 2025-07-07 11:38:07 +02:00
renovate[bot]
9ab184cc24 chore(deps): lock file maintenance (v3) (#4472)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 11:11:01 +02:00
renovate[bot]
ad0e4ddbf4 chore(deps): update vue-tsc to ^3.0.1 (v3) (#4470)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 10:43:27 +02:00
renovate[bot]
6a93556aed chore(deps): update all non-major dependencies (v3) (#4469)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 10:29:55 +02:00
HugoRCD
18a1c17d98 feat(CommandPalette): add custom slots 2025-07-04 10:06:55 +02:00
TonyWeb
9debce737c fix(Button/Link): merge active-class / inactive-class with app config (#4446)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-07-03 16:18:27 +02:00
Sylvain Marroufin
772631cde9 fix(defineShortcuts): allow extra keys to be combined with shift (#4456) 2025-07-03 15:51:50 +02:00
Teages
d7aefa53b2 fix(useOverlay): support infering close argument from complex emits (#4414)
Co-authored-by: Eugen Istoc <eugenistoc@gmail.com>
2025-07-03 10:45:19 +02:00
renovate[bot]
8922c7388e chore(deps): update all non-major dependencies (v3) (#4448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-07-02 17:31:53 +02:00
Igor G
c355cacd43 feat(Table): add footer support to display column summary (#4194)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-07-02 16:57:21 +02:00
renovate[bot]
a0e71d9e29 chore(deps): update vue-tsc to v3 (v3) (major) (#4445)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-07-02 14:25:37 +02:00
renovate[bot]
127e06ae83 chore(deps): update all non-major dependencies (v3) (#4443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-07-02 11:39:23 +02:00
Benjamin Canac
09c1ed8bf4 chore(renovate): group vue-tsc & vue-component-type-helpers 2025-07-02 11:20:57 +02:00
renovate[bot]
a05102fab3 chore(deps): update dependency reka-ui to v2.3.2 (v3) (#4439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-02 11:07:08 +02:00
renovate[bot]
09caf44d0d chore(deps): update nuxt framework to ^3.17.6 (v3) (#4437)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-02 10:13:13 +02:00
Estéban
15482aae76 docs(icons): add warning about dash collections in vue (#4307)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-07-01 18:00:12 +02:00
Benjamin Canac
f903ec396f feat(Table): add row hover event
Resolves #2435
2025-07-01 16:32:07 +02:00
Benjamin Canac
b00e07f13d feat(Popover): add reference prop 2025-07-01 16:13:31 +02:00
Benjamin Canac
5c573b37b6 docs(prettier): upgrade version 2025-07-01 13:16:38 +02:00
Benjamin Canac
f62c5ec20c feat(Table): add support for context menu
Resolves #4259
2025-07-01 13:15:00 +02:00
Benjamin Canac
b96a1ccbab feat(InputTags): add max-length prop
Resolves #4405
2025-07-01 10:35:24 +02:00
VALERIY SINEVICH
4ce654076c fix(Table): handle reactive columns (#4412) 2025-06-30 15:20:35 +02:00
Romain Hamel
fb9e7bb856 feat(Input/Textarea): add default-value prop (#4404)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-30 15:04:52 +02:00
Benjamin Canac
69a7b957d5 feat(Tooltip): add reference prop
Resolves #4430
2025-06-30 15:01:59 +02:00
Mike Newbon
3b67d54833 fix(Carousel): resolve plugins with page transitions (#4380)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-30 14:55:07 +02:00
Benjamin Canac
df8f20232f fix(Button): add active styles to behave like hover on mobile
Resolves #991
2025-06-30 12:35:36 +02:00
J-Michalek
347694b4b5 fix(Table): add scope attribute to headers (#4417) 2025-06-30 12:33:11 +02:00
renovate[bot]
021880328b chore(deps): lock file maintenance (v3) (#4429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 11:39:01 +02:00
renovate[bot]
9c1f423555 chore(deps): lock file maintenance (v3) (#4428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 10:51:22 +02:00
renovate[bot]
6cb737e038 chore(deps): update all non-major dependencies (v3) (#4391)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 10:20:52 +02:00
renovate[bot]
231b82fe4c chore(deps): update tailwindcss to ^4.1.11 (v3) (#4425)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 10:05:49 +02:00
Benjamin Canac
57a5037b13 fix(vue): handle override when importing from @nuxt/ui 2025-06-26 16:40:35 +02:00
Benjamin Canac
752e2b69bd fix(theme): colors autocomplete in app config 2025-06-26 12:45:57 +02:00
Daniel Roe
6237663a01 chore: update dev:prepare command too (#4399) 2025-06-26 12:20:05 +02:00
Daniel Roe
44cfa00e4d chore: use workspace to run vite commands (#4398) 2025-06-26 10:16:40 +02:00
Benjamin Canac
8cbbab9a6b chore(deps): update @nuxt/ui-pro 2025-06-25 17:48:48 +02:00
Benjamin Canac
2d51e20939 docs(content): update badges 2025-06-25 17:48:37 +02:00
Benjamin Canac
268e29b041 chore(release): v3.2.0 2025-06-25 17:00:05 +02:00
Benjamin Canac
b0364b96b7 fix(SelectMenu): dynamic input size 2025-06-25 16:04:04 +02:00
Benjamin Canac
ba3c6e8788 fix(InputMenu/SelectMenu): dynamic empty size
Resolves #4377
2025-06-25 16:01:04 +02:00
Benjamin Canac
01da3cbf31 docs(components): add illustrations 2025-06-25 11:46:58 +02:00
Estéban
595fc64515 feat(Table): add body-top / body-bottom slots (#4354)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-25 11:17:02 +02:00
Estéban
81569713e9 feat(Modal/Slideover): add actions slot (#4358)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-24 18:03:53 +02:00
Jack Bobakanoosh
1a8feb751e fix(Form): expose reactive fields (#4386) 2025-06-24 17:56:12 +02:00
Maxime Pauvert
1d281e915a docs(app): use findPageBreadcrumb from @nuxt/content (#4359) 2025-06-24 17:45:52 +02:00
Artea
c3adc381c9 fix(Card/Drawer/Modal): prevent scrollbars overflow (#4368)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-24 17:16:13 +02:00
Lars Eberhardt
edca3bcb74 fix(Table): use tr as separator (#4083)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-24 17:09:04 +02:00
Benjamin Canac
8f32ee3d24 chore(deps): update @nuxt/ui-pro 2025-06-24 16:42:56 +02:00
Hugo Richard
9172bb7dc2 docs(app): add copy markdown button (#4369) 2025-06-24 12:11:23 +02:00
renovate[bot]
32dae2e002 chore(deps): update dependency better-sqlite3 to v12 (v3) (#4385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 14:39:02 +02:00
Estéban
be41aed1f3 fix(components): remove default md size on buttons (#4357) 2025-06-23 12:47:30 +02:00
renovate[bot]
bf678412ca chore(deps): lock file maintenance (v3) (#4389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 12:39:12 +02:00
renovate[bot]
a999600e9f chore(deps): update all non-major dependencies (v3) (#4362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 12:05:11 +02:00
Benjamin Canac
04f12adc5b docs(examples): use useClipboard instead of navigator.clipboard 2025-06-18 14:16:54 +02:00
Sigve Hansen
abfd0ede03 fix(Toaster): smoother visibility transition for stacked toasts (#4367) 2025-06-17 16:56:33 +02:00
Vachmara
2fa8db64dd fix(NavigationMenu): nested accordion context at every level (#4363)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-17 10:49:07 +02:00
Benjamin Canac
52f1963833 chore(cli): improve templates 2025-06-17 10:10:08 +02:00
renovate[bot]
9a83c9c7f4 chore(deps): update devdependency happy-dom to v18 (v3) (#4353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 23:03:17 +02:00
renovate[bot]
f2510cb342 chore(deps): lock file maintenance (v3) (#4352)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-16 12:52:42 +02:00
Estéban
4dd56c8111 fix(Pagination): match default button size (#4350) 2025-06-16 11:46:59 +02:00
renovate[bot]
6e3ec6a077 chore(deps): update all non-major dependencies (v3) (#4349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 11:24:09 +02:00
Hugo Richard
59c26ec123 feat(CommandPalette): handle children in items (#4226)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-13 14:49:43 +02:00
Julien Augugliaro
67ef866a40 docs(input): fix typo in mask example (#4334) 2025-06-12 23:48:01 +02:00
J-Michalek
5170cfd7eb feat(Timeline): add reverse prop (#4316)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-12 17:06:51 +02:00
Benjamin Canac
9bcf1ad92f docs(input-tags): add illustration 2025-06-12 16:40:08 +02:00
Benjamin Canac
7a2bd4e617 feat(Select/SelectMenu/Tabs): expose trigger refs
Resolves #4292
2025-06-12 15:38:43 +02:00
Benjamin Canac
8781a07909 fix(InputTags): extend emits interface 2025-06-12 15:35:20 +02:00
Benjamin Canac
2492526d7c docs(showcase): improve file parse 2025-06-12 12:40:57 +02:00
zhong666
54bb2282c5 feat(InputTags): new component (#4261)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-12 12:10:48 +02:00
renovate[bot]
2a2495a652 chore(deps): update tailwindcss to ^4.1.10 (v3) (#4332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 10:53:49 +02:00
renovate[bot]
f17b15ed1e chore(deps): update tailwindcss to ^4.1.9 (v3) (#4326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 17:19:05 +02:00
Eugen Istoc
66355ba301 fix(useOverlay): set props to original props when defaultOpen is set (#4308)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-11 17:05:55 +02:00
Benjamin Canac
4873b3a043 chore(deps): update @nuxt/ui-pro 2025-06-11 17:02:44 +02:00
Benjamin Canac
0d4baf7851 docs(app): improve header dropdown items 2025-06-11 16:55:25 +02:00
syvixor
04333cd8cb docs(app): update v2 versions in header (#4325)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-11 15:27:19 +02:00
renovate[bot]
cb3522ed18 chore(deps): update dependency colortranslator to v5 (v3) (#4314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 12:56:00 +02:00
Benjamin Canac
1a4de49c16 feat(Select/SelectMenu): handle dynamic autofocus
Resolves #4324
2025-06-11 12:53:57 +02:00
Benjamin Canac
3eb7812f2d chore(github): update stale workflow 2025-06-11 12:17:19 +02:00
Benjamin Canac
080aed7225 chore(github): handle workflow_dispatch 2025-06-11 12:11:55 +02:00
Benjamin Canac
d77fd6102a chore(github): update stale workflow 2025-06-11 12:04:30 +02:00
Benjamin Canac
0f558fc0d0 feat(extendLocale): new composable
Resolves #3729
2025-06-11 10:58:20 +02:00
J-Michalek
1841e13b32 docs(timeline): fix duplicate separator field on item.ui (#4313)
Co-authored-by: Jakub <jakub.michalek@freelo.io>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-10 18:21:14 +02:00
Hugo Richard
4e7c1c9c30 fix(defineShortcuts): allow meta_- shortcut (#4321) 2025-06-10 17:54:15 +02:00
renovate[bot]
4b3dd48778 chore(deps): update nuxt-hub/action action to v2 (v3) (#4315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-10 17:44:59 +02:00
renovate[bot]
a9e8ea9231 chore(deps): update all non-major dependencies (v3) (#4277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 17:17:33 +02:00
Benjamin Canac
4dd9344ff9 chore(deps): use workspace:* syntax 2025-06-10 16:53:07 +02:00
renovate[bot]
180c150e0f chore(deps): update dependency reka-ui to v2.3.1 (v3) (#4310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 16:16:01 +02:00
Estéban
145cae798c docs(installation): add tip to improve types in vue (#4318)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-10 15:58:09 +02:00
Benjamin Canac
9aea54267a chore(deps): add @internationalized/date in playground 2025-06-10 15:49:17 +02:00
Benjamin Canac
9400552491 chore(github): improve sha retrieval 2025-06-10 14:52:35 +02:00
Benjamin Canac
89753fc337 chore(github): update workflows permissions 2025-06-10 13:02:32 +02:00
Benjamin Canac
f8a6bd3bf6 chore(github): add stale workflow 2025-06-06 12:29:23 +02:00
Benjamin Canac
228d4c9835 docs(input): add mask example 2025-06-06 11:37:26 +02:00
Benjamin Canac
150b334b1d fix(Modal/Slideover): don't emit close:prevent on closeAutoFocus 2025-06-05 17:22:42 +02:00
Eugen Istoc
bf56e15a2e fix(useOverlay): use original props when not provided to open (#4269) 2025-06-05 15:48:47 +02:00
Benjamin Canac
09151df170 chore(renovate): add vaul-vue to group 2025-06-05 12:47:08 +02:00
Benjamin Canac
22b917a0f7 chore(deps): pin reka-ui & vaul-vue dependencies
Related to https://github.com/nuxt/ui/issues/4257
2025-06-05 12:47:08 +02:00
adjabaev
43cbb94ee2 feat(locale): add Luxembourgish language (#4264) 2025-06-05 12:34:38 +02:00
Benjamin Canac
3bf5acb683 fix(Toast): calc height on next tick
Resolves #4265
2025-06-05 12:13:35 +02:00
Eugen Istoc
d37315cc83 docs(use-overlay): add caveats section regarding provide/inject limit (#4287)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-04 16:01:02 +02:00
renovate[bot]
326bb9a31e chore(deps): update nuxt framework to ^3.17.5 (v3) (#4284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-04 10:34:41 +02:00
Benjamin Canac
4157260a02 docs(timeline): improve responsive 2025-06-03 18:09:36 +02:00
Benjamin Canac
03b20fdb26 chore: prefer nuxt over nuxi 2025-06-02 12:51:40 +02:00
renovate[bot]
004c93bfa2 chore(deps): lock file maintenance (v3) (#4278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 12:46:59 +02:00
renovate[bot]
18eb5e6b97 chore(deps): update tailwindcss to ^4.1.8 (v3) (#4275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 10:55:39 +02:00
renovate[bot]
42f7f94521 chore(deps): update all non-major dependencies (v3) (#4273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 10:22:38 +02:00
dan hale
ea0c459306 feat(Form): expose loading state to default slot (#4247)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-01 16:48:16 +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
364 changed files with 22428 additions and 15820 deletions

1
.github/CODEOWNERS vendored Normal file
View File

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

View File

@@ -10,7 +10,7 @@ body:
id: env
attributes:
label: Environment
description: You can use `npx nuxi info` to fill this section
description: You can use `npx nuxt info` to fill this section
placeholder: |
- Operating System: `Darwin`
- Node Version: `v18.16.0`

View File

@@ -10,7 +10,7 @@ body:
id: env
attributes:
label: Environment
description: You can use `npx nuxi info` to fill this section
description: You can use `npx nuxt info` to fill this section
placeholder: |
- Operating System: `Darwin`
- Node Version: `v18.16.0`

View File

@@ -6,10 +6,6 @@ jobs:
deploy:
runs-on: ${{ matrix.os }}
environment:
name: ${{ github.ref == 'refs/heads/v3' && 'production' || 'preview' }}
url: ${{ steps.deploy.outputs.deployment-url }}
permissions:
contents: read
id-token: write
@@ -40,14 +36,10 @@ jobs:
- name: Prepare build
run: pnpm run dev:prepare
- name: Build application
run: pnpm run docs:build
- name: Deploy to NuxtHub
uses: nuxt-hub/action@v2
env:
NODE_OPTIONS: '--max-old-space-size=8192'
- name: Deploy to NuxtHub
uses: nuxt-hub/action@v1
id: deploy
with:
project-key: ui-7eg3
directory: docs/dist
directory: docs

View File

@@ -93,7 +93,7 @@ jobs:
- name: Store commit SHA
run: |
echo "COMMIT_SHA=$(echo ${{ github.workflow_sha }} | cut -c1-7)" >> $GITHUB_ENV
echo "COMMIT_SHA=$(echo ${{ github.event.pull_request.head.sha || github.sha }} | cut -c1-7)" >> $GITHUB_ENV
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -111,7 +111,7 @@ jobs:
run: pnpm install --ignore-workspace
- name: Prepare
run: pnpm nuxi prepare
run: pnpm nuxt prepare
- name: Typecheck
run: pnpm run typecheck
@@ -138,7 +138,7 @@ jobs:
- name: Store commit SHA
run: |
echo "COMMIT_SHA=$(echo ${{ github.workflow_sha }} | cut -c1-7)" >> $GITHUB_ENV
echo "COMMIT_SHA=$(echo ${{ github.event.pull_request.head.sha || github.sha }} | cut -c1-7)" >> $GITHUB_ENV
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -183,7 +183,7 @@ jobs:
- name: Store commit SHA
run: |
echo "COMMIT_SHA=$(echo ${{ github.workflow_sha }} | cut -c1-7)" >> $GITHUB_ENV
echo "COMMIT_SHA=$(echo ${{ github.event.pull_request.head.sha || github.sha }} | cut -c1-7)" >> $GITHUB_ENV
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -235,7 +235,7 @@ jobs:
- name: Store commit SHA
run: |
echo "COMMIT_SHA=$(echo ${{ github.workflow_sha }} | cut -c1-7)" >> $GITHUB_ENV
echo "COMMIT_SHA=$(echo ${{ github.event.pull_request.head.sha || github.sha }} | cut -c1-7)" >> $GITHUB_ENV
- name: Install pnpm
uses: pnpm/action-setup@v4

View File

@@ -9,10 +9,6 @@ jobs:
deploy:
runs-on: ${{ matrix.os }}
environment:
name: ${{ github.ref == 'refs/heads/v3' && 'production' || 'preview' }}
url: ${{ steps.deploy.outputs.deployment-url }}
permissions:
contents: read
id-token: write
@@ -40,14 +36,10 @@ jobs:
- name: Prepare build
run: pnpm run dev:prepare
- name: Build application
run: pnpm run dev:build
env:
NITRO_PRESET: cloudflare-pages
- name: Deploy to NuxtHub
uses: nuxt-hub/action@v1
id: deploy
uses: nuxt-hub/action@v2
env:
NODE_OPTIONS: '--max-old-space-size=8192'
with:
project-key: ui3-playground-pb9b
directory: playground/dist
directory: playground

27
.github/workflows/reproduction.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: reproduction
on:
workflow_dispatch:
schedule:
- cron: '30 1 * * *'
jobs:
reproduction:
runs-on: ubuntu-latest
permissions:
actions: write
issues: write
steps:
- uses: actions/stale@v9
with:
days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
stale-issue-label: 'needs reproduction' # Label that flags an issue as stale.
only-labels: 'needs reproduction' # Only process these issues
days-before-issue-close: 7
ignore-updates: true
remove-stale-when-updated: false
close-issue-message: This issue was closed because it was open for 7 days without a reproduction.
close-issue-label: closed-by-bot
operations-per-run: 300 #default 30

View File

@@ -1,6 +1,7 @@
name: stale
on:
workflow_dispatch:
schedule:
- cron: '30 1 * * *'
@@ -9,17 +10,28 @@ jobs:
runs-on: ubuntu-latest
permissions:
actions: write
issues: write
steps:
- uses: actions/stale@v9
- uses: actions/stale@4c023f01d613e60293d8004f251a18bfb9bbd71d
with:
days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
stale-issue-label: 'needs reproduction' # Label that flags an issue as stale.
only-labels: 'needs reproduction' # Only process these issues
days-before-issue-close: 7
ignore-updates: true
remove-stale-when-updated: false
close-issue-message: This issue was closed because it was open for 7 days without a reproduction.
close-issue-label: closed-by-bot
operations-per-run: 300 #default 30
days-before-pr-stale: -1
days-before-stale: 60
days-before-close: 7
stale-issue-label: 'stale'
close-issue-label: 'closed-by-bot'
close-issue-message: |
Hi! 👋
This issue has been automatically **closed** due to prolonged inactivity.
We're a small team and can't address every report, but we appreciate your feedback and contributions.
If this issue is still relevant with the latest version of Nuxt UI, please feel free to reopen or create a new issue with updated details.
Thank you for your understanding and support!
— Nuxt UI Team
exempt-issue-labels: 'feature,announcement'
operations-per-run: 300

View File

@@ -1,5 +1,86 @@
# Changelog
## [3.2.0](https://github.com/nuxt/ui/compare/v3.1.3...v3.2.0) (2025-06-25)
### ⚠ BREAKING CHANGES
* **useOverlay:** correct spelling of `unmount` function (#4051)
### Features
* **Avatar:** add `chip` prop ([#4224](https://github.com/nuxt/ui/issues/4224)) ([03ac395](https://github.com/nuxt/ui/commit/03ac395164c02c964361c68743268b1bc90aae59))
* **Carousel:** allow customization of active dot color ([#4229](https://github.com/nuxt/ui/issues/4229)) ([2ee1c5a](https://github.com/nuxt/ui/commit/2ee1c5ac2e20ab9ce2f4037a8e8c64e561b0428b))
* **CommandPalette:** handle `children` in items ([#4226](https://github.com/nuxt/ui/issues/4226)) ([59c26ec](https://github.com/nuxt/ui/commit/59c26ec1230375a24fbaf8a630a696ae854700c7))
* **extendLocale:** new composable ([0f558fc](https://github.com/nuxt/ui/commit/0f558fc0d014d51549222accfc50286d1770d1aa)), closes [#3729](https://github.com/nuxt/ui/issues/3729)
* **Form:** expose loading state to default slot ([#4247](https://github.com/nuxt/ui/issues/4247)) ([ea0c459](https://github.com/nuxt/ui/commit/ea0c459306be585bacaaf5b433114d072550c824))
* **InputTags:** new component ([#4261](https://github.com/nuxt/ui/issues/4261)) ([54bb228](https://github.com/nuxt/ui/commit/54bb2282c58d3bf5a7dde4cdee687c68efd934a0))
* **locale:** add Luxembourgish language ([#4264](https://github.com/nuxt/ui/issues/4264)) ([43cbb94](https://github.com/nuxt/ui/commit/43cbb94ee25106b414fc8fe979fa65ebaa9ccc76))
* **Modal/Slideover:** add `actions` slot ([#4358](https://github.com/nuxt/ui/issues/4358)) ([8156971](https://github.com/nuxt/ui/commit/81569713e9da9d5531ecdf4614660b84c686fa81))
* **Modal/Slideover:** add `close` method in slots ([#4219](https://github.com/nuxt/ui/issues/4219)) ([5835eb5](https://github.com/nuxt/ui/commit/5835eb5f0f835b5f03646dec78f85b2f556a109b))
* **Select/SelectMenu/Tabs:** expose trigger refs ([7a2bd4e](https://github.com/nuxt/ui/commit/7a2bd4e6179373902ba6f285903ea896fd1d378f)), closes [#4292](https://github.com/nuxt/ui/issues/4292)
* **Select/SelectMenu:** handle dynamic `autofocus` ([1a4de49](https://github.com/nuxt/ui/commit/1a4de49c1665c9ef65279315be0393d6272447b9)), closes [#4324](https://github.com/nuxt/ui/issues/4324)
* **Table:** add `body-top` / `body-bottom` slots ([#4354](https://github.com/nuxt/ui/issues/4354)) ([595fc64](https://github.com/nuxt/ui/commit/595fc64515613fe82c3a56fc5518f2e3fcce6e19))
* **Timeline:** add `reverse` prop ([#4316](https://github.com/nuxt/ui/issues/4316)) ([5170cfd](https://github.com/nuxt/ui/commit/5170cfd7eb44a25c64673cf12979f9ca1049695f))
* **Timeline:** new component ([#4215](https://github.com/nuxt/ui/issues/4215)) ([8017767](https://github.com/nuxt/ui/commit/80177679f2aa0d7f0e39e639a02d527a06e6172c))
### Bug Fixes
* **Card/Drawer/Modal:** prevent scrollbars overflow ([#4368](https://github.com/nuxt/ui/issues/4368)) ([c3adc38](https://github.com/nuxt/ui/commit/c3adc381c90dad7152e27fc303ee678efc7c4c94))
* **components:** remove default `md` size on buttons ([#4357](https://github.com/nuxt/ui/issues/4357)) ([be41aed](https://github.com/nuxt/ui/commit/be41aed1f3d3476801e1840dbb8766926bc93c05))
* **defineShortcuts:** allow `meta_-` shortcut ([#4321](https://github.com/nuxt/ui/issues/4321)) ([4e7c1c9](https://github.com/nuxt/ui/commit/4e7c1c9c305b45dd76d4c238e70a6aeedae78c8b))
* **Form:** conditionally type form data via `transform` prop ([#4188](https://github.com/nuxt/ui/issues/4188)) ([37abcc6](https://github.com/nuxt/ui/commit/37abcc6a5b0a678be626673af5067956657a50d6))
* **Form:** expose reactive fields ([#4386](https://github.com/nuxt/ui/issues/4386)) ([1a8feb7](https://github.com/nuxt/ui/commit/1a8feb751e6827c414ef82fe9fb259ba7dcc7e08))
* **InputMenu/SelectMenu:** dynamic `empty` size ([ba3c6e8](https://github.com/nuxt/ui/commit/ba3c6e8788ed75d86d4406749797da52d7816b84)), closes [#4377](https://github.com/nuxt/ui/issues/4377)
* **InputTags:** extend emits interface ([8781a07](https://github.com/nuxt/ui/commit/8781a079096def0d3bae5b8d896db0df6ce37e23))
* **Modal/Slideover:** don't emit `close:prevent` on `closeAutoFocus` ([150b334](https://github.com/nuxt/ui/commit/150b334b1d242c6dc132193e23359c03e6f35666))
* **NavigationMenu:** nested accordion context at every level ([#4363](https://github.com/nuxt/ui/issues/4363)) ([2fa8db6](https://github.com/nuxt/ui/commit/2fa8db64ddf4c92a19e73774143518d87d001b72))
* **NavigationMenu:** set content `max-height` in `horizontal` orientation ([62bc7b2](https://github.com/nuxt/ui/commit/62bc7b25a2d205d8dffb47a109196f91ff3e823a)), closes [#4208](https://github.com/nuxt/ui/issues/4208)
* **Pagination:** match default button `size` ([#4350](https://github.com/nuxt/ui/issues/4350)) ([4dd56c8](https://github.com/nuxt/ui/commit/4dd56c8111e5a224105b82d541b7742b46abb34a))
* **Select/SelectMenu:** display falsy values ([7df7ee3](https://github.com/nuxt/ui/commit/7df7ee336a925d7ee07f866551dad9350785c9fc))
* **Select/SelectMenu:** prevent empty string display when multiple ([483e473](https://github.com/nuxt/ui/commit/483e473e3f5681cc97c3766ea47283dc95f76345))
* **SelectMenu:** dynamic input size ([b0364b9](https://github.com/nuxt/ui/commit/b0364b96b73b9e543781a35962c03b5a983352c4))
* **Table:** use `tr` as separator ([#4083](https://github.com/nuxt/ui/issues/4083)) ([edca3bc](https://github.com/nuxt/ui/commit/edca3bcb743c7eb63e6abbaa801d3858342a8777))
* **Toast:** calc height on next tick ([3bf5acb](https://github.com/nuxt/ui/commit/3bf5acb683f0ad09735b2417d265d6fcfd901b11)), closes [#4265](https://github.com/nuxt/ui/issues/4265)
* **Toaster:** smoother visibility transition for stacked toasts ([#4367](https://github.com/nuxt/ui/issues/4367)) ([abfd0ed](https://github.com/nuxt/ui/commit/abfd0ede036fa2953f9abc841d77ac71bbd3bba9))
* **useOverlay:** correct spelling of `unmount` function ([#4051](https://github.com/nuxt/ui/issues/4051)) ([546df57](https://github.com/nuxt/ui/commit/546df572fca60325315bed17c9be3367052fb7a9))
* **useOverlay:** set props to original props when `defaultOpen` is set ([#4308](https://github.com/nuxt/ui/issues/4308)) ([66355ba](https://github.com/nuxt/ui/commit/66355ba301d569b9f44527bafc5f8f09bcda63c0))
* **useOverlay:** use original props when not provided to `open` ([#4269](https://github.com/nuxt/ui/issues/4269)) ([bf56e15](https://github.com/nuxt/ui/commit/bf56e15a2eed7d51199d5641649a822e91ca41ba))
## [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

View File

@@ -31,8 +31,9 @@ const component = ({ name, primitive, pro, prose, content }) => {
? `
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
${pro ? `import type { ComponentConfig } from '@nuxt/ui'` : ''}
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
import type { ComponentConfig } from '../types/utils'
${!pro ? `import type { ComponentConfig } from '../types/utils'` : ''}
type ${upperName} = ComponentConfig<typeof theme, AppConfig, ${upperName}${pro ? `, '${key}'` : ''}>
@@ -62,7 +63,7 @@ defineSlots<${upperName}Slots>()
const appConfig = useAppConfig() as ${upperName}['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.${camelName} || {}) })())
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.${pro ? 'uiPro' : 'ui'}?.${camelName} || {}) })())
</script>
<template>
@@ -75,8 +76,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.${camelName}
<script lang="ts">
import type { ${upperName}RootProps, ${upperName}RootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
${pro ? `import type { ComponentConfig } from '@nuxt/ui'` : ''}
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
import type { ComponentConfig } from '../types/utils'
${!pro ? `import type { ComponentConfig } from '../types/utils'` : ''}
type ${upperName} = ComponentConfig<typeof theme, AppConfig, ${upperName}${pro ? `, '${key}'` : ''}>
@@ -105,7 +107,7 @@ const appConfig = useAppConfig() as ${upperName}['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props), emits)
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.${camelName} || {}) })())
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.${pro ? 'uiPro' : 'ui'}?.${camelName} || {}) })())
</script>
<template>
@@ -145,7 +147,8 @@ const test = ({ name, prose, content }) => {
? undefined
: `
import { describe, it, expect } from 'vitest'
import ${upperName}, { type ${upperName}Props, type ${upperName}Slots } from '../../${content ? '../' : ''}src/runtime/components/${content ? 'content/' : ''}${upperName}.vue'
import ${upperName} from '../../${content ? '../' : ''}src/runtime/components/${content ? 'content/' : ''}${upperName}.vue'
import type { ${upperName}Props, ${upperName}Slots } from '../../${content ? '../' : ''}src/runtime/components/${content ? 'content/' : ''}${upperName}.vue'
import ComponentRender from '../${content ? '../' : ''}component-render'
describe('${upperName}', () => {
@@ -186,6 +189,7 @@ links:${primitive
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/${pro ? 'ui-pro' : 'ui'}/tree/v3/src/runtime/components/${upperName}.vue
navigation.badge: Soon
---
## Usage

View File

@@ -1,6 +1,7 @@
<template>
<UBanner
id="ui3-launch"
title="Nuxt UI v3 is officially released!"
icon="i-lucide-rocket"
:actions="[
{
@@ -10,9 +11,5 @@
}
]"
close
>
<template #title>
<span class="font-semibold">Nuxt UI v3</span> is officially released.
</template>
</UBanner>
/>
</template>

View File

@@ -22,9 +22,7 @@ onMounted(() => {
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const githubLink = computed(() => {
return `https://github.com/nuxt/${value.value}`
})
const githubLink = computed(() => `https://github.com/nuxt/${value.value}`)
const desktopLinks = computed(() => props.links.map(({ icon, ...link }) => link))
const mobileLinks = computed(() => [
@@ -36,6 +34,16 @@ const mobileLinks = computed(() => [
target: '_blank'
}
])
const items = computed(() => {
const ui2 = { label: 'v2.22.0', to: 'https://ui2.nuxt.com' }
const uiPro1 = { label: 'v1.8.0', to: 'https://ui2.nuxt.com/pro' }
return [
{ label: `v${config.version}`, active: true, color: 'primary' as const, checked: true, type: 'checkbox' as const },
route.path === '/' ? ui2 : route.path.startsWith('/pro') ? uiPro1 : module.value === 'ui-pro' ? uiPro1 : ui2
]
})
</script>
<template>
@@ -53,7 +61,7 @@ const mobileLinks = computed(() => [
<UDropdownMenu
v-slot="{ open }"
:modal="false"
:items="[{ label: `v${config.version}`, active: true, color: 'primary', checked: true, type: 'checkbox' }, { label: module === 'ui-pro' ? 'v1.7.1' : 'v2.21.1', to: module === 'ui-pro' ? 'https://ui2.nuxt.com/pro' : 'https://ui2.nuxt.com' }]"
:items="items"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-0' }"
size="xs"
>

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
const route = useRoute()
const toast = useToast()
const { copy, copied } = useClipboard()
const site = useSiteConfig()
const mdPath = computed(() => `${site.url}/raw${route.path}.md`)
const items = [
{
label: 'Copy Markdown link',
icon: 'i-lucide-link',
onSelect() {
copy(mdPath.value)
toast.add({
title: 'Copied to clipboard',
icon: 'i-lucide-check-circle'
})
}
},
{
label: 'View as Markdown',
icon: 'i-simple-icons:markdown',
target: '_blank',
to: `/raw${route.path}.md`
},
{
label: 'Open in ChatGPT',
icon: 'i-simple-icons:openai',
target: '_blank',
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
},
{
label: 'Open in Claude',
icon: 'i-simple-icons:anthropic',
target: '_blank',
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
}
]
async function copyPage() {
copy(await $fetch<string>(`/raw${route.path}.md`))
}
</script>
<template>
<UButtonGroup>
<UButton
label="Copy page"
:icon="copied ? 'i-lucide-copy-check' : 'i-lucide-copy'"
color="neutral"
variant="outline"
:ui="{
leadingIcon: [copied ? 'text-primary' : 'text-neutral', 'size-3.5']
}"
@click="copyPage"
/>
<UDropdownMenu
:items="items"
:content="{
align: 'end',
side: 'bottom',
sideOffset: 8
}"
:ui="{
content: 'w-48'
}"
>
<UButton
icon="i-lucide-chevron-down"
size="sm"
color="neutral"
variant="outline"
/>
</UDropdownMenu>
</UButtonGroup>
</template>

View File

@@ -25,6 +25,9 @@ function getEmojiFlag(locale: string): string {
kk: 'kz', // Kazakh -> Kazakhstan
km: 'kh', // Khmer -> Cambodia
ko: 'kr', // Korean -> South Korea
ky: 'kg', // Kyrgyz -> Kyrgyzstan
lb: 'lu', // Luxembourgish -> Luxembourg
ms: 'my', // Malay -> Malaysia
nb: 'no', // Norwegian Bokmål -> Norway
sl: 'si', // Slovenian -> Slovenia
sv: 'se', // Swedish -> Sweden

View File

@@ -83,7 +83,7 @@ const groups = [
</template>
<template #billing-label="{ item }">
{{ item.label }}
<span class="font-medium text-primary">{{ item.label }}</span>
<UBadge variant="subtle" size="sm">
50% off

View File

@@ -0,0 +1,119 @@
<script setup lang="ts">
const toast = useToast()
const groups = [{
id: 'actions',
label: 'Actions',
items: [{
label: 'Create new',
icon: 'i-lucide-plus',
children: [{
label: 'New file',
icon: 'i-lucide-file-plus',
suffix: 'Create a new file in the current directory',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'New file created!' })
},
kbds: ['meta', 'N']
}, {
label: 'New folder',
icon: 'i-lucide-folder-plus',
suffix: 'Create a new folder in the current directory',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'New folder created!' })
},
kbds: ['meta', 'F']
}, {
label: 'New project',
icon: 'i-lucide-folder-git',
suffix: 'Create a new project from a template',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'New project created!' })
},
kbds: ['meta', 'P']
}]
}, {
label: 'Share',
icon: 'i-lucide-share',
children: [{
label: 'Copy link',
icon: 'i-lucide-link',
suffix: 'Copy a link to the current item',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Link copied to clipboard!' })
},
kbds: ['meta', 'L']
}, {
label: 'Share via email',
icon: 'i-lucide-mail',
suffix: 'Share the current item via email',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Share via email dialog opened!' })
}
}, {
label: 'Share on social',
icon: 'i-lucide-share-2',
suffix: 'Share the current item on social media',
children: [{
label: 'Twitter',
icon: 'i-simple-icons-twitter',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Shared on Twitter!' })
}
}, {
label: 'LinkedIn',
icon: 'i-simple-icons-linkedin',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Shared on LinkedIn!' })
}
}, {
label: 'Facebook',
icon: 'i-simple-icons-facebook',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Shared on Facebook!' })
}
}]
}]
}, {
label: 'Settings',
icon: 'i-lucide-settings',
children: [{
label: 'General',
icon: 'i-lucide-sliders',
suffix: 'Configure general settings',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'General settings opened!' })
}
}, {
label: 'Appearance',
icon: 'i-lucide-palette',
suffix: 'Customize the appearance',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Appearance settings opened!' })
}
}, {
label: 'Security',
icon: 'i-lucide-shield',
suffix: 'Manage security settings',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Security settings opened!' })
}
}]
}]
}]
</script>
<template>
<UCommandPalette :groups="groups" class="flex-1" />
</template>

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

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { object, string, nonempty, refine, type Infer } from 'superstruct'
import { object, string, nonempty, refine } from 'superstruct'
import type { Infer } from 'superstruct'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = object({

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { object, string, type InferType } from 'yup'
import { object, string } from 'yup'
import type { InferType } from 'yup'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = object({

View File

@@ -0,0 +1,9 @@
<script setup lang="ts">
const tags = ref(['Vue'])
</script>
<template>
<UFormField label="Tags" required>
<UInputTags v-model="tags" placeholder="Enter tags..." />
</UFormField>
</template>

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

@@ -1,15 +1,9 @@
<script setup lang="ts">
const value = ref('npx nuxi module add ui')
const copied = ref(false)
import { useClipboard } from '@vueuse/core'
function copy() {
navigator.clipboard.writeText(value.value)
copied.value = true
const value = ref('npx nuxt module add ui')
setTimeout(() => {
copied.value = false
}, 2000)
}
const { copy, copied } = useClipboard()
</script>
<template>
@@ -25,7 +19,7 @@ function copy() {
size="sm"
:icon="copied ? 'i-lucide-copy-check' : 'i-lucide-copy'"
aria-label="Copy to clipboard"
@click="copy"
@click="copy(value)"
/>
</UTooltip>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { vMaska } from 'maska/vue'
</script>
<template>
<div class="flex flex-col gap-2">
<UInput v-maska="'#### #### #### ####'" placeholder="4242 4242 4242 4242" icon="i-lucide-credit-card" />
<div class="flex items-center gap-2">
<UInput v-maska="'##/##'" placeholder="MM/YY" icon="i-lucide-calendar" />
<UInput v-maska="'###'" placeholder="CVC" />
</div>
</div>
</template>

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

@@ -0,0 +1,43 @@
<script setup lang="ts">
const open = ref(false)
const anchor = ref({ x: 0, y: 0 })
const reference = computed(() => ({
getBoundingClientRect: () =>
({
width: 0,
height: 0,
left: anchor.value.x,
right: anchor.value.x,
top: anchor.value.y,
bottom: anchor.value.y,
...anchor.value
} as DOMRect)
}))
</script>
<template>
<UPopover
:open="open"
:reference="reference"
:content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
>
<div
class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
@pointerenter="open = true"
@pointerleave="open = false"
@pointermove="(ev) => {
anchor.x = ev.clientX
anchor.y = ev.clientY
}"
>
Hover me
</div>
<template #content>
<div class="p-4">
{{ anchor.x.toFixed(0) }} - {{ anchor.y.toFixed(0) }}
</div>
</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,106 @@
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn, TableRow } from '@nuxt/ui'
const UBadge = resolveComponent('UBadge')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
}, {
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
}, {
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
}, {
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
}, {
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}])
const columns: TableColumn<Payment>[] = [{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
}, {
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
}, {
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = ({
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
})[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
}
}, {
accessorKey: 'email',
header: 'Email'
}, {
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
footer: ({ column }) => {
const total = column.getFacetedRowModel().rows.reduce((acc: number, row: TableRow<Payment>) => acc + Number.parseFloat(row.getValue('amount')), 0)
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(total)
return h('div', { class: 'text-right font-medium' }, `Total: ${formatted}`)
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}]
</script>
<template>
<UTable :data="data" :columns="columns" class="flex-1" />
</template>

View File

@@ -2,6 +2,7 @@
import { h, resolveComponent } from 'vue'
import { upperFirst } from 'scule'
import type { TableColumn } from '@nuxt/ui'
import { useClipboard } from '@vueuse/core'
const UButton = resolveComponent('UButton')
const UCheckbox = resolveComponent('UCheckbox')
@@ -9,6 +10,7 @@ const UBadge = resolveComponent('UBadge')
const UDropdownMenu = resolveComponent('UDropdownMenu')
const toast = useToast()
const { copy } = useClipboard()
type Payment = {
id: string
@@ -220,7 +222,7 @@ const columns: TableColumn<Payment>[] = [{
}, {
label: 'Copy payment ID',
onSelect() {
navigator.clipboard.writeText(row.original.id)
copy(row.original.id)
toast.add({
title: 'Payment ID copied to clipboard!',

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import { getGroupedRowModel, type GroupingOptions } from '@tanstack/vue-table'
import { getGroupedRowModel } from '@tanstack/vue-table'
import type { GroupingOptions } from '@tanstack/vue-table'
const UBadge = resolveComponent('UBadge')

View File

@@ -2,12 +2,14 @@
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Row } from '@tanstack/vue-table'
import { useClipboard } from '@vueuse/core'
const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge')
const UDropdownMenu = resolveComponent('UDropdownMenu')
const toast = useToast()
const { copy } = useClipboard()
type Payment = {
id: string
@@ -119,7 +121,7 @@ function getRowItems(row: Row<Payment>) {
}, {
label: 'Copy payment ID',
onSelect() {
navigator.clipboard.writeText(row.original.id)
copy(row.original.id)
toast.add({
title: 'Payment ID copied to clipboard!',

View File

@@ -0,0 +1,159 @@
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { ContextMenuItem, TableColumn, TableRow } from '@nuxt/ui'
import { useClipboard } from '@vueuse/core'
const UBadge = resolveComponent('UBadge')
const UCheckbox = resolveComponent('UCheckbox')
const toast = useToast()
const { copy } = useClipboard()
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
}, {
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
}, {
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
}, {
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
}, {
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}])
const columns: TableColumn<Payment>[] = [{
id: 'select',
header: ({ table }) => h(UCheckbox, {
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
'aria-label': 'Select all'
}),
cell: ({ row }) => h(UCheckbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'aria-label': 'Select row'
})
}, {
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
}, {
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
}, {
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = ({
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
})[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
}
}, {
accessorKey: 'email',
header: 'Email'
}, {
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}]
const items = ref<ContextMenuItem[]>([])
function getRowItems(row: TableRow<Payment>) {
return [{
type: 'label' as const,
label: 'Actions'
}, {
label: 'Copy payment ID',
onSelect() {
copy(row.original.id)
toast.add({
title: 'Payment ID copied to clipboard!',
color: 'success',
icon: 'i-lucide-circle-check'
})
}
}, {
label: row.getIsExpanded() ? 'Collapse' : 'Expand',
onSelect() {
row.toggleExpanded()
}
}, {
type: 'separator' as const
}, {
label: 'View customer'
}, {
label: 'View payment details'
}]
}
function onContextmenu(_e: Event, row: TableRow<Payment>) {
items.value = getRowItems(row)
}
</script>
<template>
<UContextMenu :items="items">
<UTable
:data="data"
:columns="columns"
class="flex-1"
@contextmenu="onContextmenu"
>
<template #expanded="{ row }">
<pre>{{ row.original }}</pre>
</template>
</UTable>
</UContextMenu>
</template>

View File

@@ -0,0 +1,157 @@
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn, TableRow } from '@nuxt/ui'
const UBadge = resolveComponent('UBadge')
const UCheckbox = resolveComponent('UCheckbox')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
}, {
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
}, {
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
}, {
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
}, {
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}])
const columns: TableColumn<Payment>[] = [{
id: 'select',
header: ({ table }) => h(UCheckbox, {
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
'aria-label': 'Select all'
}),
cell: ({ row }) => h(UCheckbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'aria-label': 'Select row'
})
}, {
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
}, {
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
}, {
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = ({
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
})[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
}
}, {
accessorKey: 'email',
header: 'Email'
}, {
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}]
const anchor = ref({ x: 0, y: 0 })
const reference = computed(() => ({
getBoundingClientRect: () =>
({
width: 0,
height: 0,
left: anchor.value.x,
right: anchor.value.x,
top: anchor.value.y,
bottom: anchor.value.y,
...anchor.value
} as DOMRect)
}))
const open = ref(false)
const openDebounced = refDebounced(open, 10)
const selectedRow = ref<TableRow<Payment> | null>(null)
function onHover(_e: Event, row: TableRow<Payment> | null) {
selectedRow.value = row
open.value = !!row
}
</script>
<template>
<div class="flex w-full flex-1 gap-1">
<UTable
:data="data"
:columns="columns"
class="flex-1"
@pointermove="(ev: PointerEvent) => {
anchor.x = ev.clientX
anchor.y = ev.clientY
}"
@hover="onHover"
/>
<UPopover
:content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
:open="openDebounced"
:reference="reference"
>
<template #content>
<div class="p-4">
{{ selectedRow?.original?.id }}
</div>
</template>
</UPopover>
</div>
</template>

View File

@@ -112,7 +112,7 @@ function onSelect(row: TableRow<Payment>, e?: Event) {
</script>
<template>
<div class=" flex w-full flex-1 gap-1">
<div class="flex w-full flex-1 gap-1">
<div class="flex-1">
<UTable
ref="table"

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'
import { useClipboard } from '@vueuse/core'
interface User {
id: number
@@ -10,6 +11,7 @@ interface User {
}
const toast = useToast()
const { copy } = useClipboard()
const data = ref<User[]>([{
id: 1,
@@ -71,7 +73,8 @@ function getDropdownActions(user: User): DropdownMenuItem[][] {
label: 'Copy user Id',
icon: 'i-lucide-copy',
onSelect: () => {
navigator.clipboard.writeText(user.id.toString())
copy(user.id.toString())
toast.add({
title: 'User ID copied to clipboard!',
color: 'success',

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="translate-x-[calc(50%-1rem)]"
/>
</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

@@ -0,0 +1,41 @@
<script setup lang="ts">
const open = ref(false)
const anchor = ref({ x: 0, y: 0 })
const reference = computed(() => ({
getBoundingClientRect: () =>
({
width: 0,
height: 0,
left: anchor.value.x,
right: anchor.value.x,
top: anchor.value.y,
bottom: anchor.value.y,
...anchor.value
} as DOMRect)
}))
</script>
<template>
<UTooltip
:open="open"
:reference="reference"
:content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
>
<div
class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
@pointerenter="open = true"
@pointerleave="open = false"
@pointermove="(ev) => {
anchor.x = ev.clientX
anchor.y = ev.clientY
}"
>
Hover me
</div>
<template #content>
{{ anchor.x.toFixed(0) }} - {{ anchor.y.toFixed(0) }}
</template>
</UTooltip>
</template>

View File

@@ -2,7 +2,8 @@
import { kebabCase } from 'scule'
import type { ContentNavigationItem } from '@nuxt/content'
import type { PageLink } from '@nuxt/ui-pro'
import { findPageBreadcrumb, mapContentNavigation } from '@nuxt/ui-pro/utils/content'
import { mapContentNavigation } from '@nuxt/ui-pro/utils/content'
import { findPageBreadcrumb } from '@nuxt/content/utils'
const route = useRoute()
const { framework, module } = useSharedData()
@@ -37,7 +38,7 @@ const { data: surround } = await useAsyncData(`${kebabCase(route.path)}-surround
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const breadcrumb = computed(() => mapContentNavigation(findPageBreadcrumb(navigation?.value, page.value)).map(({ icon, ...link }) => link))
const breadcrumb = computed(() => mapContentNavigation(findPageBreadcrumb(navigation?.value, page.value?.path, { indexAsChild: true })).map(({ icon, ...link }) => link))
if (!import.meta.prerender) {
// Redirect to the correct framework version if the page is not the current framework
@@ -141,7 +142,7 @@ const communityLinks = computed(() => [{
<MDC v-if="page.description" :value="page.description" unwrap="p" :cache-key="`${kebabCase(route.path)}-description`" />
</template>
<template v-if="page.links?.length" #links>
<template #links>
<UButton
v-for="link in page.links"
:key="link.label"
@@ -154,6 +155,7 @@ const communityLinks = computed(() => [{
<UAvatar v-bind="link.avatar" size="2xs" :alt="`${link.label} avatar`" />
</template>
</UButton>
<PageHeaderLinks />
</template>
</UPageHeader>

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

@@ -16,12 +16,12 @@ function handleMessage(message) {
async function handleFormatMessage(message) {
if (!globalThis.prettier) {
await Promise.all([
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/standalone.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/babel.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/estree.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/html.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/markdown.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/typescript.js')
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/standalone.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/babel.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/estree.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/html.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/markdown.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/typescript.js')
])
}

View File

@@ -84,7 +84,7 @@ You can play with Nuxt UI components as well as your app components directly fro
Install the module to your Nuxt application with one command:
```bash [Terminal]
npx nuxi module add compodium
npx nuxt module add compodium
```
::

View File

@@ -115,7 +115,7 @@ Start your project using the [nuxt/starter#ui](https://github.com/nuxt/starter/t
Create a new project locally by running the following command:
```bash [Terminal]
npx nuxi init -t ui <my-app>
npm create nuxt@latest -- -t ui
```
::note

View File

@@ -78,6 +78,22 @@ components.d.ts
::
::tip
Internally, Nuxt UI relies on custom alias to resolve the theme types. If you're using TypeScript, you should add an alias to your `tsconfig` to enable auto-completion in your `vite.config.ts`.
```json [tsconfig.node.json]
{
"compilerOptions": {
"paths": {
"#build/ui": [
"./node_modules/@nuxt/ui/.nuxt/ui"
]
}
}
}
```
::
#### Use the Nuxt UI Vue plugin in your `main.ts`
```ts [main.ts]{3,14}
@@ -179,7 +195,7 @@ Start your project using the [nuxtlabs/nuxt-ui-vue-starter](https://github.com/n
Create a new project locally by running the following command:
```bash [Terminal]
npx nuxi init -t github:nuxtlabs/nuxt-ui-vue-starter <my-app>
npm create nuxt@latest -- -t github:nuxtlabs/nuxt-ui-vue-starter
```
::note

View File

@@ -32,6 +32,12 @@ props:
You can use any name from the <https://icones.js.org> collection.
::
::warning
When using collections with a dash (`-`), you need to separate the icon name from the collection name with a colon (`:`) as `@iconify/vue` does not handle this case like `@nuxt/icon`. For example, instead of `i-simple-icons-github` you need to write `i-simple-icons:github` or `simple-icons:github`.
Learn more about the [Iconify naming convention](https://iconify.design/docs/icon-components/vue/#icon).
::
### Component Props
Some components also have an `icon` prop to display an icon, like the [Button](/components/button) for example:

View File

@@ -60,7 +60,7 @@ import { fr } from '@nuxt/ui-pro/locale'
### Custom locale
You also have the option to add your own locale using `defineLocale`:
You can create your own locale using the `defineLocale` composable:
::module-only
@@ -125,6 +125,65 @@ Look at the `code` parameter, there you need to pass the iso code of the languag
::
### Extend locale :badge{label="New" class="align-text-top"}
You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:
::module-only
#ui
:::div
```vue [app.vue]
<script setup lang="ts">
import { en } from '@nuxt/ui/locale'
const locale = extendLocale(en, {
code: 'en-GB',
messages: {
commandPalette: {
placeholder: 'Search a component...'
}
}
})
</script>
<template>
<UApp :locale="locale">
<NuxtPage />
</UApp>
</template>
```
:::
#ui-pro
:::div
```vue [app.vue]
<script setup lang="ts">
import { en } from '@nuxt/ui-pro/locale'
const locale = extendLocale(en, {
code: 'en-GB',
messages: {
commandPalette: {
placeholder: 'Search a component...'
}
}
})
</script>
<template>
<UApp :locale="locale">
<NuxtPage />
</UApp>
</template>
```
:::
::
### Dynamic locale
To dynamically switch between languages, you can use the [Nuxt I18n](https://i18n.nuxtjs.org/) module.

View File

@@ -60,7 +60,7 @@ import { fr } from '@nuxt/ui-pro/locale'
### Custom locale
You also have the option to add your locale using `defineLocale`:
You can create your own locale using the `defineLocale` composable:
::module-only
@@ -127,6 +127,67 @@ Look at the `code` parameter, there you need to pass the iso code of the languag
::
### Extend locale :badge{label="New" class="align-text-top"}
You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:
::module-only
#ui
:::div
```vue [App.vue]
<script setup lang="ts">
import { en } from '@nuxt/ui/locale'
import { extendLocale } from '@nuxt/ui/composables/defineLocale.js'
const locale = extendLocale(en, {
code: 'en-GB',
messages: {
commandPalette: {
placeholder: 'Search a component...'
}
}
})
</script>
<template>
<UApp :locale="locale">
<RouterView />
</UApp>
</template>
```
:::
#ui-pro
:::div
```vue [App.vue]
<script setup lang="ts">
import { en } from '@nuxt/ui-pro/locale'
import { extendLocale } from '@nuxt/ui/composables/defineLocale.js'
const locale = extendLocale(en, {
code: 'en-GB',
messages: {
commandPalette: {
placeholder: 'Search a component...'
}
}
})
</script>
<template>
<UApp :locale="locale">
<RouterView />
</UApp>
</template>
```
:::
::
### Dynamic locale
To dynamically switch between languages, you can use the [Vue I18n](https://vue-i18n.intlify.dev/) plugin.

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

@@ -1,6 +1,6 @@
---
title: useOverlay
description: "A composable to programmatically control overlays."
description: 'A composable to programmatically control overlays.'
---
## Usage
@@ -9,9 +9,11 @@ Use the auto-imported `useOverlay` composable to programmatically control [Modal
```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'
const overlay = useOverlay()
const modal = overlay.create(MyModal)
const modal = overlay.create(LazyModalExample)
async function openModal() {
modal.open()
@@ -29,71 +31,73 @@ In order to return a value from the overlay, the `overlay.open().instance.result
### `create(component: T, options: OverlayOptions): OverlayInstance`
Creates an overlay, and returns a factory instance
Create an overlay, and return a factory instance.
- Parameters:
- `component`: The overlay component
- `options` The overlay options
- `defaultOpen?: boolean` Opens the overlay immediately after being created `default: false`
- `component`: The overlay component.
- `options`:
- `defaultOpen?: boolean` Open the overlay immediately after being created. Defaults to `false`.
- `props?: ComponentProps`: An optional object of props to pass to the rendered component.
- `destroyOnClose?: boolean` Removes the overlay from memory when closed `default: false`
- `destroyOnClose?: boolean` Removes the overlay from memory when closed. Defaults to `false`.
### `open(id: symbol, props?: ComponentProps<T>): OpenedOverlay<T>`
Opens the overlay using its `id`
Open an overlay by its `id`.
- Parameters:
- `id`: The identifier of the overlay
- `id`: The identifier of the overlay.
- `props`: An optional object of props to pass to the rendered component.
### `close(id: symbol, value?: any): void`
Close an overlay using its `id`
Close an overlay by its `id`.
- Parameters:
- `id`: The identifier of the overlay
- `value`: A value to resolve the overlay promise with
- `id`: The identifier of the overlay.
- `value`: A value to resolve the overlay promise with.
### `patch(id: symbol, props: ComponentProps<T>): void`
Update an overlay using its `id`
Update an overlay by its `id`.
- Parameters:
- `id`: The identifier of the overlay
- `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`
Remove an overlay from the DOM by its `id`.
- Parameters:
- `id`: The identifier of the overlay
- `id`: The identifier of the overlay.
### `isOpen(id: symbol): boolean`
Checks if an overlay its open using its `id`
Check if an overlay is open using its `id`.
- Parameters:
- `id`: The identifier of the overlay
- `id`: The identifier of the overlay.
### `overlays: Overlay[]`
In-memory list of overlays that were created
In-memory list of all overlays that were created.
## Overlay Instance API
## Instance API
### `open(props?: ComponentProps<T>): Promise<OpenedOverlay<T>>`
Opens the overlay
Open the overlay.
- Parameters:
- `props`: An optional object of props to pass to the rendered component.
```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'
const overlay = useOverlay()
const modal = overlay.create(MyModalContent)
const modal = overlay.create(LazyModalExample)
function openModal() {
modal.open({
@@ -105,23 +109,25 @@ function openModal() {
### `close(value?: any): void`
Close the overlay
Close the overlay.
- Parameters:
- `value`: A value to resolve the overlay promise with
- `value`: A value to resolve the overlay promise with.
### `patch(props: ComponentProps<T>)`
Updates the props of the overlay.
Update the props of the overlay.
- Parameters:
- `props`: An object of props to update on the rendered component.
```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'
const overlay = useOverlay()
const modal = overlay.create(MyModal, {
const modal = overlay.create(LazyModalExample, {
title: 'Welcome'
})
@@ -141,6 +147,8 @@ Here's a complete example of how to use the `useOverlay` composable:
```vue
<script setup lang="ts">
import { ModalA, ModalB, SlideoverA } from '#components'
const overlay = useOverlay()
// Create with default props
@@ -150,7 +158,7 @@ const modalB = overlay.create(ModalB)
const slideoverA = overlay.create(SlideoverA)
const openModalA = () => {
// Open Modal A, but override the title prop
// Open modalA, but override the title prop
modalA.open({ title: 'Hello' })
}
@@ -160,16 +168,37 @@ const openModalB = async () => {
const input = await modalBInstance.result
// Pass the result from modalB to the slideover, and open it.
// Pass the result from modalB to the slideover, and open it
slideoverA.open({ input })
}
</script>
<template>
<div>
<button @click="openModal">Open Modal</button>
</div>
<button @click="openModalA">Open Modal</button>
</template>
```
In this example, we're using the `useOverlay` composable to control multiple modals and slideovers.
## Caveats
### Provide / Inject
When opening overlays programmatically (e.g. modals, slideovers, etc), the overlay component can only access injected values from the component containing `UApp` (typically `app.vue` or layout components). This is because overlays are mounted outside of the page context by the `UApp` component.
As such, using `provide()` in pages or parent components isn't supported directly. To pass provided values to overlays, the recommended approach is to use props instead:
```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'
const providedValue = inject('valueProvidedInPage')
const modal = overlay.create(LazyModalExample, {
props: {
providedValue,
otherData: someValue
}
})
</script>
```

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

@@ -9,7 +9,6 @@ links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/CheckboxGroup.vue
navigation.badge: New
---

View File

@@ -156,7 +156,7 @@ props:
---
::
### Variant :badge{label="New" class="align-text-top"}
### Variant
Use the `variant` prop to change the variant of the Checkbox.
@@ -190,7 +190,7 @@ props:
---
::
### Indicator :badge{label="New" class="align-text-top"}
### Indicator
Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.

View File

@@ -52,9 +52,11 @@ Each group contains an `items` array of objects that define the commands. Each i
- `loading?: boolean`{lang="ts-type"}
- `disabled?: boolean`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `placeholder?: string`{lang="ts-type"}
- `children?: CommandPaletteItem[]`{lang="ts-type"}
- `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"}
- `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.
@@ -110,6 +112,10 @@ props:
---
::
::tip{to="#with-children-in-items"}
Each item can take a `children` array of objects with the following properties to create submenus:
::
### Multiple
Use the `multiple` prop to allow multiple selections.
@@ -246,6 +252,128 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.se
:::
::
### Selected Icon
Use the `selected-icon` prop to customize the selected item [Icon](/components/icon). Defaults to `i-lucide-check`.
::component-code
---
collapse: true
hide:
- autofocus
ignore:
- groups
- modelValue
- multiple
- class
external:
- groups
- modelValue
class: '!p-0'
props:
multiple: true
autofocus: false
modelValue:
- label: 'Benjamin Canac'
suffix: 'benjamincanac'
avatar:
src: 'https://github.com/benjamincanac.png'
selectedIcon: 'i-lucide-circle-check'
groups:
- id: 'users'
label: 'Users'
items:
- label: 'Benjamin Canac'
suffix: 'benjamincanac'
avatar:
src: 'https://github.com/benjamincanac.png'
- label: 'Sylvain Marroufin'
suffix: 'smarroufin'
avatar:
src: 'https://github.com/smarroufin.png'
- label: 'Sébastien Chopin'
suffix: 'atinux'
avatar:
src: 'https://github.com/atinux.png'
- label: 'Romain Hamel'
suffix: 'romhml'
avatar:
src: 'https://github.com/romhml.png'
- label: 'Haytham A. Salama'
suffix: 'Haythamasalama'
avatar:
src: 'https://github.com/Haythamasalama.png'
- label: 'Daniel Roe'
suffix: 'danielroe'
avatar:
src: 'https://github.com/danielroe.png'
- label: 'Neil Richter'
suffix: 'noook'
avatar:
src: 'https://github.com/noook.png'
class: 'flex-1'
---
::
::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.check` key.
:::
#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.check` key.
:::
::
### Trailing Icon :badge{label="New" class="align-text-top"}
Use the `trailing-icon` prop to customize the trailing [Icon](/components/icon) when an item has children. Defaults to `i-lucide-chevron-right`.
::component-code
---
collapse: true
prettier: true
hide:
- autofocus
ignore:
- groups
- class
external:
- groups
class: '!p-0'
props:
autofocus: false
trailingIcon: 'i-lucide-arrow-right'
groups:
- id: 'actions'
items:
- label: 'Share'
icon: 'i-lucide-share'
children:
- label: 'Email'
icon: 'i-lucide-mail'
- label: 'Copy'
icon: 'i-lucide-copy'
- label: 'Link'
icon: 'i-lucide-link'
class: 'flex-1'
---
::
::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.chevronRight` key.
:::
#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.chevronRight` key.
:::
::
### Loading
Use the `loading` prop to show a loading icon on the CommandPalette.
@@ -279,7 +407,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
---
@@ -295,7 +423,7 @@ class: '!p-0'
props:
autofocus: false
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
groups:
- id: 'apps'
items:
@@ -321,37 +449,6 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.lo
:::
::
### Disabled
Use the `disabled` prop to disable the CommandPalette.
::component-code
---
collapse: true
hide:
- autofocus
ignore:
- groups
- class
external:
- groups
class: '!p-0'
props:
autofocus: false
disabled: true
groups:
- id: 'apps'
items:
- label: 'Calendar'
icon: 'i-lucide-calendar'
- label: 'Music'
icon: 'i-lucide-music'
- label: 'Maps'
icon: 'i-lucide-map'
class: 'flex-1'
---
::
### Close
Use the `close` prop to display a [Button](/components/button) to dismiss the CommandPalette.
@@ -468,6 +565,124 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.cl
:::
::
### Back :badge{label="New" class="align-text-top"}
Use the `back` prop to customize or hide the back button (with `false` value) displayed when navigating into a submenu.
You can pass any property from the [Button](/components/button) component to customize it.
::component-code
---
collapse: true
prettier: true
hide:
- autofocus
ignore:
- back.color
- groups
- class
external:
- groups
class: '!p-0'
props:
autofocus: false
back:
color: primary
groups:
- id: 'actions'
items:
- label: 'Share'
icon: 'i-lucide-share'
children:
- label: 'Email'
icon: 'i-lucide-mail'
- label: 'Copy'
icon: 'i-lucide-copy'
- label: 'Link'
icon: 'i-lucide-link'
class: 'flex-1'
---
::
### Back Icon :badge{label="New" class="align-text-top"}
Use the `back-icon` prop to customize the back button [Icon](/components/icon). Defaults to `i-lucide-arrow-left`.
::component-code
---
collapse: true
hide:
- autofocus
ignore:
- class
- groups
- back
external:
- groups
class: '!p-0'
props:
autofocus: false
back: true
backIcon: 'i-lucide-house'
groups:
- id: 'actions'
items:
- label: 'Share'
icon: 'i-lucide-share'
children:
- label: 'Email'
icon: 'i-lucide-mail'
- label: 'Copy'
icon: 'i-lucide-copy'
- label: 'Link'
icon: 'i-lucide-link'
class: 'flex-1'
---
::
::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.arrowLeft` key.
:::
#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.arrowLeft` key.
:::
::
### Disabled
Use the `disabled` prop to disable the CommandPalette.
::component-code
---
collapse: true
hide:
- autofocus
ignore:
- groups
- class
external:
- groups
class: '!p-0'
props:
autofocus: false
disabled: true
groups:
- id: 'apps'
items:
- label: 'Calendar'
icon: 'i-lucide-calendar'
- label: 'Music'
icon: 'i-lucide-music'
- label: 'Maps'
icon: 'i-lucide-map'
class: 'flex-1'
---
::
## Examples
### Control selected item(s)
@@ -502,6 +717,28 @@ props:
This example uses the `@update:model-value` event to reset the search term when an item is selected.
::
### With children in items :badge{label="New" class="align-text-top"}
You can create hierarchical menus by using the `children` property in items. When an item has children, it will automatically display a chevron icon and enable navigation into a submenu.
::component-example
---
collapse: true
prettier: true
name: 'command-palette-items-children-example'
class: '!p-0'
props:
autofocus: false
---
::
::note
When navigating into a submenu:
- The search term is reset
- A back button appears in the input
- You can go back to the previous group by pressing the :kbd{value="backspace"} key
::
### With fetched items
You can fetch items from an API and use them in the CommandPalette.
@@ -658,6 +895,7 @@ You will have access to the following slots:
::component-example
---
collapse: true
name: 'command-palette-custom-slot-example'
class: '!p-0'
props:

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

@@ -135,7 +135,7 @@ props:
### Multiple
Use the `multiple` prop to allow multiple selections, the selected items will be displayed as badges.
Use the `multiple` prop to allow multiple selections, the selected items will be displayed as tags.
::component-code
---
@@ -166,7 +166,7 @@ Ensure to pass an array to the `default-value` prop or the `v-model` directive.
### Delete Icon
With `multiple`, use the `delete-icon` prop to customize the delete [Icon](/components/icon) in the badges. Defaults to `i-lucide-x`.
With `multiple`, use the `delete-icon` prop to customize the delete [Icon](/components/icon) in the tags. Defaults to `i-lucide-x`.
::component-code
---
@@ -518,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
---
@@ -532,7 +532,7 @@ external:
props:
modelValue: 'Backlog'
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
items:
- Backlog
- Todo
@@ -612,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.
@@ -782,6 +782,14 @@ name: 'input-menu-countries-example'
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
| ---- | ---- |
| `inputRef`{lang="ts-type"} | `Ref<InstanceType<typeof ComboboxTrigger> \| null>`{lang="ts-type"} |
## Theme
:component-theme

View File

@@ -1,6 +1,6 @@
---
title: InputNumber
description: Input numerical values with a customizable range.
description: An input for numerical values with a customizable range.
category: form
links:
- label: NumberField
@@ -287,8 +287,8 @@ name: 'input-number-slots-example'
When accessing the component via a template ref, you can use the following:
| Name | Type |
|----------------------------|-------------------------------------------------|
| `inputRef`{lang="ts-type"} | `Ref<HTMLInputElement \| null>`{lang="ts-type"} |
| -------------------------- | ----------------------------------------------- |
| `inputRef`{lang="ts-type"} | `Ref<InstanceType<typeof NumberFieldInput> \| null>`{lang="ts-type"} |
## Theme

View File

@@ -0,0 +1,296 @@
---
title: InputTags
description: An input element that displays interactive tags.
category: form
links:
- label: InputTags
icon: i-custom-reka-ui
to: https://reka-ui.com/docs/components/tags-input
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/InputTags.vue
navigation.badge: New
---
## Usage
Use the `v-model` directive to control the value of the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
---
::
Use the `default-value` prop to set the initial value when you do not need to control its state.
::component-code
---
prettier: true
ignore:
- defaultValue
props:
defaultValue: ['Vue']
---
::
### Placeholder
Use the `placeholder` prop to set a placeholder text.
::component-code
---
props:
placeholder: 'Enter tags...'
---
::
### Max Length :badge{label="Soon" class="align-text-top"}
Use the `max-length` prop to set the maximum number of characters allowed in a tag.
::component-code
---
props:
maxLength: 4
---
::
### Color
Use the `color` prop to change the ring color when the InputTags is focused.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
color: neutral
highlight: true
---
::
::note
The `highlight` prop is used here to show the focus state. It's used internally when a validation error occurs.
::
### Variants
Use the `variant` prop to change the appearance of the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
variant: subtle
color: neutral
highlight: false
---
::
### Sizes
Use the `size` prop to adjust the size of the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
size: xl
---
::
### Icon
Use the `icon` prop to show an [Icon](/components/icon) inside the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
icon: 'i-lucide-search'
size: md
variant: outline
---
::
::note
Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.
::
### Avatar
Use the `avatar` prop to show an [Avatar](/components/avatar) inside the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
avatar:
src: 'https://github.com/vuejs.png'
size: md
variant: outline
---
::
### Delete Icon
Use the `delete-icon` prop to customize the delete [Icon](/components/icon) in the tags. Defaults to `i-lucide-x`.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
deleteIcon: 'i-lucide-trash'
---
::
::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.close` key.
:::
#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.close` key.
:::
::
### Loading
Use the `loading` prop to show a loading icon on the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
loading: true
trailing: false
---
::
### Loading Icon
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
loading: true
loadingIcon: 'i-lucide-loader'
---
::
::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.loading` key.
:::
#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.loading` key.
:::
::
### Disabled
Use the `disabled` prop to disable the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
disabled: true
---
::
## Examples
### Within a FormField
You can use the InputTags within a [FormField](/components/form-field) component to display a label, help text, required indicator, etc.
::component-example
---
name: 'input-tags-form-field-example'
---
::
## API
### Props
:component-props
### Slots
:component-slots
### Emits
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
| -------------------------- | ----------------------------------------------- |
| `inputRef`{lang="ts-type"} | `Ref<InstanceType<typeof TagsInputInput> \| null>`{lang="ts-type"} |
## Theme
:component-theme

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...'
---
::
@@ -278,6 +278,16 @@ name: 'input-kbd-example'
This example uses the `defineShortcuts` composable to focus the Input when the :kbd{value="/"} key is pressed.
::
### With mask
There's no built-in support for masks, but you can use libraries like [maska](https://github.com/beholdr/maska) to mask the Input.
::component-example
---
name: 'input-mask-example'
---
::
### With floating label
You can use the `#default` slot to add a floating label to the Input.

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

@@ -21,10 +21,9 @@ Use the `items` prop as an array of objects with the following properties:
- `icon?: string`{lang="ts-type"}
- `avatar?: AvatarProps`{lang="ts-type"}
- `badge?: string | number | BadgeProps`{lang="ts-type"}
- `tooltip?: TooltipProps`{lang="ts-type"} :badge{label="Soon"}
- `tooltip?: TooltipProps`{lang="ts-type"}
- `trailingIcon?: string`{lang="ts-type"}
- `type?: 'label' | 'link'`{lang="ts-type"}
- `collapsible?: boolean`{lang="ts-type"} :badge{label="Soon"}
- `type?: 'label' | 'trigger' | 'link'`{lang="ts-type"}
- `defaultOpen?: boolean`{lang="ts-type"}
- `open?: boolean`{lang="ts-type"}
- `value?: string`{lang="ts-type"}
@@ -33,7 +32,7 @@ Use the `items` prop as an array of objects with the following properties:
- `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, childItem?: ClassNameValue, childLink?: ClassNameValue, childLinkIcon?: ClassNameValue, childLinkWrapper?: ClassNameValue, childLinkLabel?: ClassNameValue, childLinkLabelExternalIcon?: ClassNameValue, childLinkDescription?: ClassNameValue }`{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.
@@ -145,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.
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
@@ -244,7 +243,11 @@ Groups will be spaced when orientation is `horizontal` and separated when orient
### Collapsed
Use the `collapsed` prop to collapse the NavigationMenu, this can be useful in a sidebar for example.
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
---
@@ -257,8 +260,17 @@ external:
- items
externalTypes:
- NavigationMenuItem[][]
items:
tooltip:
- true
- false
popover:
- true
- false
props:
collapsed: true
tooltip: false
popover: false
orientation: 'vertical'
items:
- - label: Links
@@ -283,8 +295,6 @@ props:
description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
- label: Composables
icon: i-lucide-database
collapsible: false
open: false
children:
- label: defineShortcuts
icon: i-lucide-file-text
@@ -300,8 +310,6 @@ props:
to: /composables/use-toast
- label: Components
icon: i-lucide-box
collapsible: false
open: false
to: /components
active: true
children:
@@ -340,10 +348,6 @@ props:
---
::
::tip
You can set the `collapsible: false` property on items with children to prevent them from being collapsible. This allows the item to act as a regular link while still displaying its children in a submenu.
::
### Highlight
Use the `highlight` prop to display a highlighted border for the active item.
@@ -885,9 +889,11 @@ You can inspect the DOM to see each item's content being rendered.
## Examples
### With tooltips in items :badge{label="Soon" class="align-text-top"}
### With tooltip in items
You can use the `tooltip` property to display a [Tooltip](/components/tooltip) around an item. This can be useful when the menu is collapsed.
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
---
@@ -900,7 +906,12 @@ external:
- items
externalTypes:
- NavigationMenuItem[][]
items:
tooltip:
- true
- false
props:
tooltip: true
collapsed: true
orientation: 'vertical'
items:
@@ -908,40 +919,24 @@ props:
type: 'label'
- label: Guide
icon: i-lucide-book-open
tooltip:
text: 'Guide'
children:
- label: Introduction
description: Fully styled and customizable components for Nuxt.
icon: i-lucide-house
tooltip:
text: 'Introduction'
- label: Installation
description: Learn how to install and configure Nuxt UI in your application.
icon: i-lucide-cloud-download
tooltip:
text: 'Installation'
- label: 'Icons'
icon: 'i-lucide-smile'
description: 'You have nothing to do, @nuxt/icon will handle it automatically.'
tooltip:
text: 'Icons'
- label: 'Colors'
icon: 'i-lucide-swatch-book'
description: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
tooltip:
text: 'Colors'
- label: 'Theme'
icon: 'i-lucide-cog'
description: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
tooltip:
text: 'Theme'
- label: Composables
icon: i-lucide-database
tooltip:
text: 'Composables'
collapsible: false
open: false
children:
- label: defineShortcuts
icon: i-lucide-file-text
@@ -957,12 +952,8 @@ props:
to: /composables/use-toast
- label: Components
icon: i-lucide-box
tooltip:
text: 'Components'
to: /components
active: true
collapsible: false
open: false
children:
- label: Link
icon: i-lucide-file-text
@@ -994,17 +985,126 @@ props:
to: https://github.com/nuxt/ui
target: _blank
tooltip:
text: 'GitHub'
text: 'Open on GitHub'
kbds:
- 3.8k
- label: Help
icon: i-lucide-circle-help
disabled: true
tooltip:
text: 'Help'
---
::
### With popover in items
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.
@@ -1052,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

@@ -180,6 +180,8 @@ props:
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |

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,31 @@ name: 'popover-command-palette-example'
---
::
### With following cursor :badge{label="Soon" class="align-text-top"}
You can make the Popover follow the cursor when hovering over an element using the [`reference`](https://reka-ui.com/docs/components/tooltip#trigger) prop:
::component-example
---
name: 'popover-cursor-example'
---
::
### With anchor slot
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

@@ -159,7 +159,7 @@ props:
---
::
### Variant :badge{label="New" class="align-text-top"}
### Variant
Use the `variant` prop to change the variant of the RadioGroup.
@@ -240,7 +240,7 @@ props:
---
::
### Indicator :badge{label="New" class="align-text-top"}
### Indicator
Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.

View File

@@ -555,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
---
@@ -570,7 +570,7 @@ external:
props:
modelValue: 'Backlog'
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
items:
- Backlog
- Todo
@@ -655,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.
@@ -815,6 +815,14 @@ name: 'select-menu-countries-example'
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
| ---- | ---- |
| `triggerRef`{lang="ts-type"} | `Ref<InstanceType<typeof ComboboxTrigger> \| null>`{lang="ts-type"} |
## Theme
:component-theme

View File

@@ -507,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
---
@@ -522,7 +522,7 @@ external:
props:
modelValue: 'Backlog'
loading: true
loadingIcon: 'i-lucide-repeat-2'
loadingIcon: 'i-lucide-loader'
items:
- Backlog
- Todo
@@ -607,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.
@@ -709,6 +709,14 @@ collapse: true
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
| ---- | ---- |
| `triggerRef`{lang="ts-type"} | `Ref<InstanceType<typeof SelectTrigger> \| null>`{lang="ts-type"} |
## Theme
:component-theme

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
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

@@ -200,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

@@ -77,6 +77,7 @@ Use the `columns` prop as an array of [ColumnDef](https://tanstack.com/table/lat
- `accessorKey`: [The key of the row object to use when extracting the value for the column.]{class="text-muted"}
- `header`: [The header to display for the column. If a string is passed, it can be used as a default for the column ID. If a function is passed, it will be passed a props object for the header and should return the rendered header value (the exact type depends on the adapter being used).]{class="text-muted"}
- `footer`: [The footer to display for the column. Works exactly like header, but is displayed under the table.]{class="text-muted"}
- `cell`: [The cell to display each row for the column. If a function is passed, it will be passed a props object for the cell and should return the rendered cell value (the exact type depends on the adapter being used).]{class="text-muted"}
- `meta`: [Extra properties for the column.]{class="text-muted"}
- `class`:
@@ -161,7 +162,7 @@ props:
### Sticky
Use the `sticky` prop to make the header sticky.
Use the `sticky` prop to make the header or footer sticky.
::component-code
---
@@ -172,6 +173,10 @@ ignore:
- class
external:
- data
items:
sticky:
- true
- false
props:
sticky: true
data:
@@ -266,8 +271,8 @@ You can group rows based on a given column value and show/hide sub rows via some
#### Important parts:
* Add prop `grouping` to `UTable` component with an array of column ids you want to group by.
* Add prop `grouping-options` to `UTable`. It must include `getGroupedRowModel`, you can import it from `@tanstack/vue-table` or implement your own.
* Add `grouping` prop with an array of column ids you want to group by.
* Add `grouping-options` prop. It must include `getGroupedRowModel`, you can import it from `@tanstack/vue-table` or implement your own.
* Expand rows via `row.toggleExpanded()` method on any cell of the row. Keep in mind, it also toggles `#expanded` slot.
* Use `aggregateFn` on column definition to define how to aggregate the rows.
* `agregatedCell` renderer on column definition only works if there is no `cell` renderer.
@@ -304,19 +309,19 @@ class: '!p-0'
You can use the `row-selection` prop to control the selection state of the rows (can be binded with `v-model`).
::
### With `@select` event
### With row select event
You can add a `@select` listener to make rows clickable. The handler function receives the `TableRow` instance as the first argument and an optional `Event` as the second argument.
You can add a `@select` listener to make rows clickable with or without a checkbox column.
::note
You can use this to navigate to a page, open a modal or even to select the row manually.
The handler function receives the `TableRow` instance as the first argument and an optional `Event` as the second argument.
::
::component-example
---
prettier: true
collapse: true
name: 'table-row-selection-event-example'
name: 'table-row-select-event-example'
highlights:
- 123
- 130
@@ -324,6 +329,70 @@ class: '!p-0'
---
::
::tip
You can use this to navigate to a page, open a modal or even to select the row manually.
::
### With row context menu event :badge{label="Soon" class="align-text-top"}
You can add a `@contextmenu` listener to make rows right clickable and wrap the Table in a [ContextMenu](/components/context-menu) component to display row actions for example.
::note
The handler function receives the `Event` and `TableRow` instance as the first and second arguments respectively.
::
::component-example
---
prettier: true
collapse: true
name: 'table-row-context-menu-event-example'
highlights:
- 130
- 170
class: '!p-0'
---
::
### With row hover event :badge{label="Soon" class="align-text-top"}
You can add a `@hover` listener to make rows hoverable and use a [Popover](/components/popover) or a [Tooltip](/components/tooltip) component to display row details for example.
::note
The handler function receives the `Event` and `TableRow` instance as the first and second arguments respectively.
::
::component-example
---
prettier: true
collapse: true
name: 'table-row-hover-event-example'
highlights:
- 126
- 149
class: '!p-0'
---
::
::note
This example is similar as the Popover [with following cursor example](/components/popover#with-following-cursor) and uses a [`refDebounced`](https://vueuse.org/shared/refDebounced/#refdebounced) to prevent the Popover from opening and closing too quickly when moving the cursor from one row to another.
::
### With column footer :badge{label="Soon" class="align-text-top"}
You can add a `footer` property to the column definition to render a footer for the column.
::component-example
---
prettier: true
collapse: true
name: 'table-column-footer-example'
highlights:
- 94
- 108
class: '!p-0'
---
::
### With column sorting
You can update a column `header` to render a [Button](/components/button) component inside the `header` to toggle the sorting state using the TanStack Table [Sorting APIs](https://tanstack.com/table/latest/docs/api/features/sorting).

View File

@@ -222,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
@@ -238,6 +242,14 @@ Use the `slot` property to customize a specific item.
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
| ---- | ---- |
| `triggersRef`{lang="ts-type"} | `Ref<ComponentPublicInstance[]>`{lang="ts-type"} |
## Theme
:component-theme

View File

@@ -124,7 +124,7 @@ props:
---
::
### Icon :badge{label="New" class="align-text-top"}
### Icon
Use the `icon` prop to show an [Icon](/components/icon) inside the Textarea.
@@ -157,7 +157,7 @@ props:
---
::
### Avatar :badge{label="New" class="align-text-top"}
### Avatar
Use the `avatar` prop to show an [Avatar](/components/avatar) inside the Textarea.
@@ -176,7 +176,7 @@ props:
---
::
### Loading :badge{label="New" class="align-text-top"}
### Loading
Use the `loading` prop to show a loading icon on the Textarea.
@@ -192,9 +192,9 @@ props:
---
::
### Loading Icon :badge{label="New" class="align-text-top"}
### 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
---
@@ -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,269 @@
---
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: New
---
## 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, 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'
class: 'overflow-x-auto'
---
::
### Reverse
Use the reverse prop to reverse the direction of the Timeline.
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
reverse: true
modelValue: 2
orientation: 'vertical'
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'
class: 'overflow-x-auto'
---
::
## 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

@@ -186,6 +186,16 @@ name: 'tooltip-open-example'
In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts), you can toggle the Tooltip by pressing :kbd{value="O"}.
::
### With following cursor :badge{label="Soon" class="align-text-top"}
You can make the Tooltip follow the cursor when hovering over an element using the [`reference`](https://reka-ui.com/docs/components/tooltip#trigger) prop:
::component-example
---
name: 'tooltip-cursor-example'
---
::
## API
### Props

View File

@@ -407,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

@@ -33,7 +33,7 @@ export default defineNuxtModule((_, nuxt) => {
}
const name = template.name.toLowerCase().replace(/\s/g, '-')
const filename = join(process.cwd(), 'docs/public/assets/showcase', `${name}.png`)
const filename = join(nuxt.options.rootDir, 'public/assets/showcase', `${name}.png`)
if (existsSync(filename)) {
continue

View File

@@ -2,41 +2,49 @@
"private": true,
"name": "@nuxt/ui-docs",
"type": "module",
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"generate": "nuxt generate",
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@ai-sdk/vue": "^1.2.12",
"@iconify-json/logos": "^1.2.4",
"@iconify-json/lucide": "^1.2.43",
"@iconify-json/simple-icons": "^1.2.34",
"@iconify-json/vscode-icons": "^1.2.21",
"@nuxt/content": "^3.5.1",
"@iconify-json/lucide": "^1.2.56",
"@iconify-json/simple-icons": "^1.2.42",
"@iconify-json/vscode-icons": "^1.2.23",
"@nuxt/content": "^3.6.3",
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@a30de4d",
"@nuxthub/core": "^0.8.27",
"@nuxt/ui": "workspace:*",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@22fdc5e",
"@nuxthub/core": "^0.9.0",
"@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^21.1.1",
"@octokit/rest": "^22.0.0",
"@rollup/plugin-yaml": "^4.1.2",
"@vueuse/integrations": "^13.2.0",
"@vueuse/nuxt": "^13.2.0",
"ai": "^4.3.15",
"@vueuse/integrations": "^13.5.0",
"@vueuse/nuxt": "^13.5.0",
"ai": "^4.3.16",
"better-sqlite3": "^12.2.0",
"capture-website": "^4.2.0",
"joi": "^17.13.3",
"motion-v": "^1.0.2",
"nuxt": "^3.17.3",
"nuxt-component-meta": "^0.11.0",
"nuxt-llms": "^0.1.2",
"nuxt-og-image": "^5.1.3",
"prettier": "^3.5.3",
"maska": "^3.2.0",
"motion-v": "^1.5.0",
"nuxt": "^3.17.6",
"nuxt-component-meta": "^0.12.1",
"nuxt-llms": "^0.1.3",
"nuxt-og-image": "^5.1.9",
"prettier": "^3.6.2",
"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.4.1",
"workers-ai-provider": "^0.7.1",
"yup": "^1.6.1",
"zod": "^3.24.4"
"zod": "^3.25.75"
},
"devDependencies": {
"wrangler": "^4.15.1"
"wrangler": "^4.23.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,412 +1,8 @@
import json5 from 'json5'
import { camelCase, kebabCase } from 'scule'
import { visit } from '@nuxt/content/runtime'
import type { H3Event } from 'h3'
import type { PageCollectionItemBase } from '@nuxt/content'
import * as theme from '../../.nuxt/ui'
import * as themePro from '../../.nuxt/ui-pro'
import meta from '#nuxt-component-meta'
// @ts-expect-error - no types available
import components from '#component-example/nitro'
type ComponentAttributes = {
':pro'?: string
':prose'?: string
':props'?: string
':external'?: string
':externalTypes'?: string
':ignore'?: string
':hide'?: string
':slots'?: string
}
type ThemeConfig = {
pro: boolean
prose: boolean
componentName: string
}
type CodeConfig = {
pro: boolean
props: Record<string, unknown>
external: string[]
externalTypes: string[]
ignore: string[]
hide: string[]
componentName: string
slots?: Record<string, string>
}
type Document = {
title: string
body: any
}
const parseBoolean = (value?: string): boolean => value === 'true'
function getComponentMeta(componentName: string) {
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1)
const strategies = [
`U${pascalCaseName}`,
`Prose${pascalCaseName}`,
pascalCaseName
]
let componentMeta: any
let finalMetaComponentName: string = pascalCaseName
for (const nameToTry of strategies) {
finalMetaComponentName = nameToTry
const metaAttempt = (meta as Record<string, any>)[nameToTry]?.meta
if (metaAttempt) {
componentMeta = metaAttempt
break
}
}
if (!componentMeta) {
console.warn(`[getComponentMeta] Metadata not found for ${pascalCaseName} using strategies: U, Prose, or no prefix. Last tried: ${finalMetaComponentName}`)
}
return {
pascalCaseName,
metaComponentName: finalMetaComponentName,
componentMeta
}
}
function replaceNodeWithPre(node: any[], language: string, code: string, filename?: string) {
node[0] = 'pre'
node[1] = { language, code }
if (filename) node[1].filename = filename
}
function visitAndReplace(doc: Document, type: string, handler: (node: any[]) => void) {
visit(doc.body, (node) => {
if (Array.isArray(node) && node[0] === type) {
handler(node)
}
return true
}, node => node)
}
function generateTSInterface(
name: string,
items: any[],
itemHandler: (item: any) => string,
description: string
) {
let code = `/**\n * ${description}\n */\ninterface ${name} {\n`
for (const item of items) {
code += itemHandler(item)
}
code += `}`
return code
}
function propItemHandler(propValue: any): string {
if (!propValue?.name) return ''
const propName = propValue.name
const propType = propValue.type
? Array.isArray(propValue.type)
? propValue.type.map((t: any) => t.name || t).join(' | ')
: propValue.type.name || propValue.type
: 'any'
const isRequired = propValue.required || false
const hasDescription = propValue.description && propValue.description.trim().length > 0
const hasDefault = propValue.default !== undefined
let result = ''
if (hasDescription || hasDefault) {
result += ` /**\n`
if (hasDescription) {
const descLines = propValue.description.split(/\r?\n/)
descLines.forEach((line: string) => {
result += ` * ${line}\n`
})
}
if (hasDefault) {
let defaultValue = propValue.default
if (typeof defaultValue === 'string') {
defaultValue = `"${defaultValue.replace(/"/g, '\\"')}"`
} else {
defaultValue = JSON.stringify(defaultValue)
}
result += ` * @default ${defaultValue}\n`
}
result += ` */\n`
}
result += ` ${propName}${isRequired ? '' : '?'}: ${propType};\n`
return result
}
function slotItemHandler(slotValue: any): string {
if (!slotValue?.name) return ''
const slotName = slotValue.name
const hasDescription = slotValue.description && slotValue.description.trim().length > 0
let result = ''
if (hasDescription) {
result += ` /**\n`
const descLines = slotValue.description.split(/\r?\n/)
descLines.forEach((line: string) => {
result += ` * ${line}\n`
})
result += ` */\n`
}
if (slotValue.bindings && Object.keys(slotValue.bindings).length > 0) {
let bindingsType = '{\n'
Object.entries(slotValue.bindings).forEach(([bindingName, bindingValue]: [string, any]) => {
const bindingType = bindingValue.type || 'any'
bindingsType += ` ${bindingName}: ${bindingType};\n`
})
bindingsType += ' }'
result += ` ${slotName}(bindings: ${bindingsType}): any;\n`
} else {
result += ` ${slotName}(): any;\n`
}
return result
}
function emitItemHandler(event: any): string {
if (!event?.name) return ''
let payloadType = 'void'
if (event.type) {
payloadType = Array.isArray(event.type)
? event.type.map((t: any) => t.name || t).join(' | ')
: event.type.name || event.type
}
let result = ''
if (event.description && event.description.trim().length > 0) {
result += ` /**\n`
event.description.split(/\r?\n/).forEach((line: string) => {
result += ` * ${line}\n`
})
result += ` */\n`
}
result += ` ${event.name}: (payload: ${payloadType}) => void;\n`
return result
}
const generateThemeConfig = ({ pro, prose, componentName }: ThemeConfig) => {
const computedTheme = pro ? (prose ? themePro.prose : themePro) : theme
const componentTheme = computedTheme[componentName as keyof typeof computedTheme]
return {
[pro ? 'uiPro' : 'ui']: prose
? { prose: { [componentName]: componentTheme } }
: { [componentName]: componentTheme }
}
}
const generateComponentCode = ({
pro,
props,
external,
externalTypes,
hide,
componentName,
slots
}: CodeConfig) => {
const filteredProps = Object.fromEntries(
Object.entries(props).filter(([key]) => !hide.includes(key))
)
const imports = pro
? ''
: external
.filter((_, index) => externalTypes[index] && externalTypes[index] !== 'undefined')
.map((ext, index) => {
const type = externalTypes[index]?.replace(/[[\]]/g, '')
return `import type { ${type} } from '@nuxt/${pro ? 'ui-pro' : 'ui'}'`
})
.join('\n')
let itemsCode = ''
if (props.items) {
itemsCode = pro
? `const items = ref(${json5.stringify(props.items, null, 2)})`
: `const items = ref<${externalTypes[0]}>(${json5.stringify(props.items, null, 2)})`
delete filteredProps.items
}
let calendarValueCode = ''
if (componentName === 'calendar' && props.modelValue && Array.isArray(props.modelValue)) {
calendarValueCode = `const value = ref(new CalendarDate(${props.modelValue.join(', ')}))`
}
const propsString = Object.entries(filteredProps)
.map(([key, value]) => {
const formattedKey = kebabCase(key)
if (typeof value === 'string') {
return `${formattedKey}="${value}"`
} else if (typeof value === 'number') {
return `:${formattedKey}="${value}"`
} else if (typeof value === 'boolean') {
return value ? formattedKey : `:${formattedKey}="false"`
}
return ''
})
.filter(Boolean)
.join(' ')
const itemsProp = props.items ? ':items="items"' : ''
const vModelProp = componentName === 'calendar' && props.modelValue ? 'v-model="value"' : ''
const allProps = [propsString, itemsProp, vModelProp].filter(Boolean).join(' ')
const formattedProps = allProps ? ` ${allProps}` : ''
let scriptSetup = ''
if (imports || itemsCode || calendarValueCode) {
scriptSetup = '<script setup lang="ts">'
if (imports) scriptSetup += `\n${imports}`
if (imports && (itemsCode || calendarValueCode)) scriptSetup += '\n'
if (calendarValueCode) scriptSetup += `\n${calendarValueCode}`
if (itemsCode) scriptSetup += `\n${itemsCode}`
scriptSetup += '\n</script>\n\n'
}
let componentContent = ''
let slotContent = ''
if (slots && Object.keys(slots).length > 0) {
const defaultSlot = slots.default?.trim()
if (defaultSlot) {
const indentedContent = defaultSlot
.split('\n')
.map(line => line.trim() ? ` ${line}` : line)
.join('\n')
componentContent = `\n${indentedContent}\n `
}
Object.entries(slots).forEach(([slotName, content]) => {
if (slotName !== 'default' && content?.trim()) {
const indentedSlotContent = content.trim()
.split('\n')
.map(line => line.trim() ? ` ${line}` : line)
.join('\n')
slotContent += `\n <template #${slotName}>\n${indentedSlotContent}\n </template>`
}
})
}
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1)
let componentTemplate = ''
if (componentContent || slotContent) {
componentTemplate = `<U${pascalCaseName}${formattedProps}>${componentContent}${slotContent}</U${pascalCaseName}>` // Removed space before closing tag
} else {
componentTemplate = `<U${pascalCaseName}${formattedProps} />`
}
return `${scriptSetup}<template>
${componentTemplate}
</template>`
}
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('content:llms:generate:document', async (_: H3Event, doc: PageCollectionItemBase) => {
const componentName = camelCase(doc.title)
visitAndReplace(doc, 'component-theme', (node) => {
const attributes = node[1] as Record<string, string>
const mdcSpecificName = attributes?.slug
const finalComponentName = mdcSpecificName ? camelCase(mdcSpecificName) : componentName
const pro = parseBoolean(attributes[':pro'])
const prose = parseBoolean(attributes[':prose'])
const appConfig = generateThemeConfig({ pro, prose, componentName: finalComponentName })
replaceNodeWithPre(
node,
'ts',
`export default defineAppConfig(${json5.stringify(appConfig, null, 2)?.replace(/,([ |\t\n]+[}|\])])/g, '$1')})`,
'app.config.ts'
)
})
visitAndReplace(doc, 'component-code', (node) => {
const attributes = node[1] as ComponentAttributes
const pro = parseBoolean(attributes[':pro'])
const props = attributes[':props'] ? json5.parse(attributes[':props']) : {}
const external = attributes[':external'] ? json5.parse(attributes[':external']) : []
const externalTypes = attributes[':externalTypes'] ? json5.parse(attributes[':externalTypes']) : []
const ignore = attributes[':ignore'] ? json5.parse(attributes[':ignore']) : []
const hide = attributes[':hide'] ? json5.parse(attributes[':hide']) : []
const slots = attributes[':slots'] ? json5.parse(attributes[':slots']) : {}
const code = generateComponentCode({
pro,
props,
external,
externalTypes,
ignore,
hide,
componentName,
slots
})
replaceNodeWithPre(node, 'vue', code)
})
visitAndReplace(doc, 'component-props', (node) => {
const attributes = node[1] as Record<string, string>
const mdcSpecificName = attributes?.name
const isProse = parseBoolean(attributes[':prose'])
const finalComponentName = mdcSpecificName ? camelCase(mdcSpecificName) : componentName
const { pascalCaseName, componentMeta } = getComponentMeta(finalComponentName)
if (!componentMeta?.props) return
const interfaceName = isProse ? `Prose${pascalCaseName}Props` : `${pascalCaseName}Props`
const interfaceCode = generateTSInterface(
interfaceName,
Object.values(componentMeta.props),
propItemHandler,
`Props for the ${isProse ? 'Prose' : ''}${pascalCaseName} component`
)
replaceNodeWithPre(node, 'ts', interfaceCode)
})
visitAndReplace(doc, 'component-slots', (node) => {
const { pascalCaseName, componentMeta } = getComponentMeta(componentName)
if (!componentMeta?.slots) return
const interfaceCode = generateTSInterface(
`${pascalCaseName}Slots`,
Object.values(componentMeta.slots),
slotItemHandler,
`Slots for the ${pascalCaseName} component`
)
replaceNodeWithPre(node, 'ts', interfaceCode)
})
visitAndReplace(doc, 'component-emits', (node) => {
const { pascalCaseName, componentMeta } = getComponentMeta(componentName)
const hasEvents = componentMeta?.events && Object.keys(componentMeta.events).length > 0
if (hasEvents) {
const interfaceCode = generateTSInterface(
`${pascalCaseName}Emits`,
Object.values(componentMeta.events),
emitItemHandler,
`Emitted events for the ${pascalCaseName} component`
)
replaceNodeWithPre(node, 'ts', interfaceCode)
} else {
node[0] = 'p'
node[1] = {}
node[2] = 'No events available for this component.'
}
})
visitAndReplace(doc, 'component-example', (node) => {
const camelName = camelCase(node[1]['name'])
const name = camelName.charAt(0).toUpperCase() + camelName.slice(1)
const code = components[name].code
replaceNodeWithPre(node, 'vue', code, `${name}.vue`)
})
transformMDC(doc as any)
})
})

View File

@@ -0,0 +1,30 @@
import { stringify } from 'minimark/stringify'
import { withLeadingSlash } from 'ufo'
export default eventHandler(async (event) => {
const slug = getRouterParams(event)['slug.md']
if (!slug?.endsWith('.md')) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
const path = withLeadingSlash(slug.replace('.md', ''))
// @ts-expect-error TODO: fix this
const page = await queryCollection(event, 'content').path(path).first()
if (!page) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
// Add title and description to the top of the page if missing
if (page.body.value[0]?.[0] !== 'h1') {
page.body.value.unshift(['blockquote', {}, page.description])
page.body.value.unshift(['h1', {}, page.title])
}
const transformedPage = transformMDC({
title: page.title,
body: page.body
})
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
return stringify({ ...transformedPage.body, type: 'minimark' }, { format: 'markdown/html' })
})

View File

@@ -0,0 +1,410 @@
import json5 from 'json5'
import { camelCase, kebabCase } from 'scule'
import { visit } from '@nuxt/content/runtime'
import * as theme from '../../.nuxt/ui'
import * as themePro from '../../.nuxt/ui-pro'
import meta from '#nuxt-component-meta'
// @ts-expect-error - no types available
import components from '#component-example/nitro'
type ComponentAttributes = {
':pro'?: string
':prose'?: string
':props'?: string
':external'?: string
':externalTypes'?: string
':ignore'?: string
':hide'?: string
':slots'?: string
}
type ThemeConfig = {
pro: boolean
prose: boolean
componentName: string
}
type CodeConfig = {
pro: boolean
props: Record<string, unknown>
external: string[]
externalTypes: string[]
ignore: string[]
hide: string[]
componentName: string
slots?: Record<string, string>
}
type Document = {
title: string
body: any
}
const parseBoolean = (value?: string): boolean => value === 'true'
function getComponentMeta(componentName: string) {
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1)
const strategies = [
`U${pascalCaseName}`,
`Prose${pascalCaseName}`,
pascalCaseName
]
let componentMeta: any
let finalMetaComponentName: string = pascalCaseName
for (const nameToTry of strategies) {
finalMetaComponentName = nameToTry
const metaAttempt = (meta as Record<string, any>)[nameToTry]?.meta
if (metaAttempt) {
componentMeta = metaAttempt
break
}
}
if (!componentMeta) {
console.warn(`[getComponentMeta] Metadata not found for ${pascalCaseName} using strategies: U, Prose, or no prefix. Last tried: ${finalMetaComponentName}`)
}
return {
pascalCaseName,
metaComponentName: finalMetaComponentName,
componentMeta
}
}
function replaceNodeWithPre(node: any[], language: string, code: string, filename?: string) {
node[0] = 'pre'
node[1] = { language, code }
if (filename) node[1].filename = filename
}
function visitAndReplace(doc: Document, type: string, handler: (node: any[]) => void) {
visit(doc.body, (node) => {
if (Array.isArray(node) && node[0] === type) {
handler(node)
}
return true
}, node => node)
}
function generateTSInterface(
name: string,
items: any[],
itemHandler: (item: any) => string,
description: string
) {
let code = `/**\n * ${description}\n */\ninterface ${name} {\n`
for (const item of items) {
code += itemHandler(item)
}
code += `}`
return code
}
function propItemHandler(propValue: any): string {
if (!propValue?.name) return ''
const propName = propValue.name
const propType = propValue.type
? Array.isArray(propValue.type)
? propValue.type.map((t: any) => t.name || t).join(' | ')
: propValue.type.name || propValue.type
: 'any'
const isRequired = propValue.required || false
const hasDescription = propValue.description && propValue.description.trim().length > 0
const hasDefault = propValue.default !== undefined
let result = ''
if (hasDescription || hasDefault) {
result += ` /**\n`
if (hasDescription) {
const descLines = propValue.description.split(/\r?\n/)
descLines.forEach((line: string) => {
result += ` * ${line}\n`
})
}
if (hasDefault) {
let defaultValue = propValue.default
if (typeof defaultValue === 'string') {
defaultValue = `"${defaultValue.replace(/"/g, '\\"')}"`
} else {
defaultValue = JSON.stringify(defaultValue)
}
result += ` * @default ${defaultValue}\n`
}
result += ` */\n`
}
result += ` ${propName}${isRequired ? '' : '?'}: ${propType};\n`
return result
}
function slotItemHandler(slotValue: any): string {
if (!slotValue?.name) return ''
const slotName = slotValue.name
const hasDescription = slotValue.description && slotValue.description.trim().length > 0
let result = ''
if (hasDescription) {
result += ` /**\n`
const descLines = slotValue.description.split(/\r?\n/)
descLines.forEach((line: string) => {
result += ` * ${line}\n`
})
result += ` */\n`
}
if (slotValue.bindings && Object.keys(slotValue.bindings).length > 0) {
let bindingsType = '{\n'
Object.entries(slotValue.bindings).forEach(([bindingName, bindingValue]: [string, any]) => {
const bindingType = bindingValue.type || 'any'
bindingsType += ` ${bindingName}: ${bindingType};\n`
})
bindingsType += ' }'
result += ` ${slotName}(bindings: ${bindingsType}): any;\n`
} else {
result += ` ${slotName}(): any;\n`
}
return result
}
function emitItemHandler(event: any): string {
if (!event?.name) return ''
let payloadType = 'void'
if (event.type) {
payloadType = Array.isArray(event.type)
? event.type.map((t: any) => t.name || t).join(' | ')
: event.type.name || event.type
}
let result = ''
if (event.description && event.description.trim().length > 0) {
result += ` /**\n`
event.description.split(/\r?\n/).forEach((line: string) => {
result += ` * ${line}\n`
})
result += ` */\n`
}
result += ` ${event.name}: (payload: ${payloadType}) => void;\n`
return result
}
const generateThemeConfig = ({ pro, prose, componentName }: ThemeConfig) => {
const computedTheme = pro ? (prose ? themePro.prose : themePro) : theme
const componentTheme = computedTheme[componentName as keyof typeof computedTheme]
return {
[pro ? 'uiPro' : 'ui']: prose
? { prose: { [componentName]: componentTheme } }
: { [componentName]: componentTheme }
}
}
const generateComponentCode = ({
pro,
props,
external,
externalTypes,
hide,
componentName,
slots
}: CodeConfig) => {
const filteredProps = Object.fromEntries(
Object.entries(props).filter(([key]) => !hide.includes(key))
)
const imports = pro
? ''
: external
.filter((_, index) => externalTypes[index] && externalTypes[index] !== 'undefined')
.map((ext, index) => {
const type = externalTypes[index]?.replace(/[[\]]/g, '')
return `import type { ${type} } from '@nuxt/${pro ? 'ui-pro' : 'ui'}'`
})
.join('\n')
let itemsCode = ''
if (props.items) {
itemsCode = pro
? `const items = ref(${json5.stringify(props.items, null, 2)})`
: `const items = ref<${externalTypes[0]}>(${json5.stringify(props.items, null, 2)})`
delete filteredProps.items
}
let calendarValueCode = ''
if (componentName === 'calendar' && props.modelValue && Array.isArray(props.modelValue)) {
calendarValueCode = `const value = ref(new CalendarDate(${props.modelValue.join(', ')}))`
}
const propsString = Object.entries(filteredProps)
.map(([key, value]) => {
const formattedKey = kebabCase(key)
if (typeof value === 'string') {
return `${formattedKey}="${value}"`
} else if (typeof value === 'number') {
return `:${formattedKey}="${value}"`
} else if (typeof value === 'boolean') {
return value ? formattedKey : `:${formattedKey}="false"`
}
return ''
})
.filter(Boolean)
.join(' ')
const itemsProp = props.items ? ':items="items"' : ''
const vModelProp = componentName === 'calendar' && props.modelValue ? 'v-model="value"' : ''
const allProps = [propsString, itemsProp, vModelProp].filter(Boolean).join(' ')
const formattedProps = allProps ? ` ${allProps}` : ''
let scriptSetup = ''
if (imports || itemsCode || calendarValueCode) {
scriptSetup = '<script setup lang="ts">'
if (imports) scriptSetup += `\n${imports}`
if (imports && (itemsCode || calendarValueCode)) scriptSetup += '\n'
if (calendarValueCode) scriptSetup += `\n${calendarValueCode}`
if (itemsCode) scriptSetup += `\n${itemsCode}`
scriptSetup += '\n</script>\n\n'
}
let componentContent = ''
let slotContent = ''
if (slots && Object.keys(slots).length > 0) {
const defaultSlot = slots.default?.trim()
if (defaultSlot) {
const indentedContent = defaultSlot
.split('\n')
.map(line => line.trim() ? ` ${line}` : line)
.join('\n')
componentContent = `\n${indentedContent}\n `
}
Object.entries(slots).forEach(([slotName, content]) => {
if (slotName !== 'default' && content?.trim()) {
const indentedSlotContent = content.trim()
.split('\n')
.map(line => line.trim() ? ` ${line}` : line)
.join('\n')
slotContent += `\n <template #${slotName}>\n${indentedSlotContent}\n </template>`
}
})
}
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1)
let componentTemplate = ''
if (componentContent || slotContent) {
componentTemplate = `<U${pascalCaseName}${formattedProps}>${componentContent}${slotContent}</U${pascalCaseName}>` // Removed space before closing tag
} else {
componentTemplate = `<U${pascalCaseName}${formattedProps} />`
}
return `${scriptSetup}<template>
${componentTemplate}
</template>`
}
export function transformMDC(doc: Document): Document {
const componentName = camelCase(doc.title)
visitAndReplace(doc, 'component-theme', (node) => {
const attributes = node[1] as Record<string, string>
const mdcSpecificName = attributes?.slug
const finalComponentName = mdcSpecificName ? camelCase(mdcSpecificName) : componentName
const pro = parseBoolean(attributes[':pro'])
const prose = parseBoolean(attributes[':prose'])
const appConfig = generateThemeConfig({ pro, prose, componentName: finalComponentName })
replaceNodeWithPre(
node,
'ts',
`export default defineAppConfig(${json5.stringify(appConfig, null, 2)?.replace(/,([ |\t\n]+[}|\])])/g, '$1')})`,
'app.config.ts'
)
})
visitAndReplace(doc, 'component-code', (node) => {
const attributes = node[1] as ComponentAttributes
const pro = parseBoolean(attributes[':pro'])
const props = attributes[':props'] ? json5.parse(attributes[':props']) : {}
const external = attributes[':external'] ? json5.parse(attributes[':external']) : []
const externalTypes = attributes[':externalTypes'] ? json5.parse(attributes[':externalTypes']) : []
const ignore = attributes[':ignore'] ? json5.parse(attributes[':ignore']) : []
const hide = attributes[':hide'] ? json5.parse(attributes[':hide']) : []
const slots = attributes[':slots'] ? json5.parse(attributes[':slots']) : {}
const code = generateComponentCode({
pro,
props,
external,
externalTypes,
ignore,
hide,
componentName,
slots
})
replaceNodeWithPre(node, 'vue', code)
})
visitAndReplace(doc, 'component-props', (node) => {
const attributes = node[1] as Record<string, string>
const mdcSpecificName = attributes?.name
const isProse = parseBoolean(attributes[':prose'])
const finalComponentName = mdcSpecificName ? camelCase(mdcSpecificName) : componentName
const { pascalCaseName, componentMeta } = getComponentMeta(finalComponentName)
if (!componentMeta?.props) return
const interfaceName = isProse ? `Prose${pascalCaseName}Props` : `${pascalCaseName}Props`
const interfaceCode = generateTSInterface(
interfaceName,
Object.values(componentMeta.props),
propItemHandler,
`Props for the ${isProse ? 'Prose' : ''}${pascalCaseName} component`
)
replaceNodeWithPre(node, 'ts', interfaceCode)
})
visitAndReplace(doc, 'component-slots', (node) => {
const { pascalCaseName, componentMeta } = getComponentMeta(componentName)
if (!componentMeta?.slots) return
const interfaceCode = generateTSInterface(
`${pascalCaseName}Slots`,
Object.values(componentMeta.slots),
slotItemHandler,
`Slots for the ${pascalCaseName} component`
)
replaceNodeWithPre(node, 'ts', interfaceCode)
})
visitAndReplace(doc, 'component-emits', (node) => {
const { pascalCaseName, componentMeta } = getComponentMeta(componentName)
const hasEvents = componentMeta?.events && Object.keys(componentMeta.events).length > 0
if (hasEvents) {
const interfaceCode = generateTSInterface(
`${pascalCaseName}Emits`,
Object.values(componentMeta.events),
emitItemHandler,
`Emitted events for the ${pascalCaseName} component`
)
replaceNodeWithPre(node, 'ts', interfaceCode)
} else {
node[0] = 'p'
node[1] = {}
node[2] = 'No events available for this component.'
}
})
visitAndReplace(doc, 'component-example', (node) => {
const camelName = camelCase(node[1]['name'])
const name = camelName.charAt(0).toUpperCase() + camelName.slice(1)
const code = components[name].code
replaceNodeWithPre(node, 'vue', code, `${name}.vue`)
})
return doc
}

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.2",
"packageManager": "pnpm@10.11.0",
"version": "3.2.0",
"packageManager": "pnpm@10.12.4",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/ui.git"
@@ -96,38 +96,37 @@
"scripts": {
"build": "nuxt-module-build build",
"prepack": "pnpm build",
"dev": "nuxi dev playground --uiDev",
"dev:build": "nuxi build playground",
"dev:vue": "vite playground-vue -- --uiDev",
"dev:vue:build": "vite build playground-vue",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && nuxi prepare docs && vite build playground-vue",
"docs": "nuxi dev docs --uiDev",
"docs:build": "nuxi build docs",
"docs:prepare": "nuxt-component-meta docs",
"dev": "nuxt dev playground --uiDev",
"dev:build": "nuxt build playground",
"dev:vue": "pnpm --filter playground-vue dev -- --uiDev",
"dev:vue:build": "pnpm --filter playground-vue build",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground && nuxt prepare docs && pnpm dev:vue:build",
"docs": "nuxt dev docs --uiDev",
"docs:build": "nuxt build docs",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "vue-tsc --noEmit && nuxi typecheck playground && nuxi typecheck docs && cd playground-vue && vue-tsc --noEmit",
"typecheck": "vue-tsc --noEmit && nuxt typecheck playground && nuxt typecheck docs && cd playground-vue && vue-tsc --noEmit",
"test": "vitest",
"test:vue": "vitest -c vitest.vue.config.ts",
"release": "release-it"
},
"dependencies": {
"@iconify/vue": "^4.3.0",
"@internationalized/date": "^3.8.0",
"@internationalized/number": "^3.6.1",
"@iconify/vue": "^5.0.0",
"@internationalized/date": "^3.8.2",
"@internationalized/number": "^3.6.3",
"@nuxt/fonts": "^0.11.4",
"@nuxt/icon": "^1.12.0",
"@nuxt/kit": "^3.17.3",
"@nuxt/schema": "^3.17.3",
"@nuxt/icon": "^1.15.0",
"@nuxt/kit": "^3.17.6",
"@nuxt/schema": "^3.17.6",
"@nuxtjs/color-mode": "^3.5.2",
"@standard-schema/spec": "^1.0.0",
"@tailwindcss/postcss": "^4.1.6",
"@tailwindcss/vite": "^4.1.6",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/vue-table": "^8.21.3",
"@unhead/vue": "^2.0.8",
"@vueuse/core": "^13.2.0",
"@vueuse/integrations": "^13.2.0",
"colortranslator": "^4.1.0",
"@unhead/vue": "^2.0.12",
"@vueuse/core": "^13.5.0",
"@vueuse/integrations": "^13.5.0",
"colortranslator": "^5.0.0",
"consola": "^3.4.2",
"defu": "^6.1.4",
"embla-carousel-auto-height": "^8.6.0",
@@ -144,31 +143,31 @@
"mlly": "^1.7.4",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"reka-ui": "^2.2.1",
"reka-ui": "2.3.2",
"scule": "^1.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.6",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.4",
"unplugin-auto-import": "^19.2.0",
"unplugin-vue-components": "^28.5.0",
"vaul-vue": "^0.4.1",
"vue-component-type-helpers": "^2.2.10"
"tailwindcss": "^4.1.11",
"tinyglobby": "^0.2.14",
"unplugin": "^2.3.5",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vaul-vue": "0.4.1",
"vue-component-type-helpers": "^3.0.1"
},
"devDependencies": {
"@nuxt/eslint-config": "^1.3.1",
"@nuxt/eslint-config": "^1.5.2",
"@nuxt/module-builder": "^1.0.1",
"@nuxt/test-utils": "^3.18.0",
"@nuxt/test-utils": "^3.19.2",
"@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.7",
"nuxt": "^3.17.3",
"release-it": "^19.0.2",
"vitest": "^3.1.3",
"eslint": "^9.30.1",
"happy-dom": "^18.0.1",
"nuxt": "^3.17.6",
"release-it": "^19.0.3",
"vitest": "^3.2.4",
"vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.2.10"
"vue-tsc": "^3.0.1"
},
"peerDependencies": {
"@inertiajs/vue3": "^2.0.7",
@@ -209,7 +208,7 @@
"debug": "4.3.7",
"rollup": "4.34.9",
"unimport": "4.1.1",
"unplugin": "^2.3.4"
"unplugin": "^2.3.5"
},
"pnpm": {
"onlyBuiltDependencies": [

View File

@@ -10,15 +10,15 @@
"typecheck": "vue-tsc -p ./tsconfig.app.json"
},
"dependencies": {
"@nuxt/ui": "latest",
"vue": "^3.5.14",
"@nuxt/ui": "workspace:*",
"vue": "^3.5.17",
"vue-router": "^4.5.1",
"zod": "^3.24.4"
"zod": "^3.25.75"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.4",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vue-tsc": "^2.2.10"
"vue-tsc": "^3.0.1"
}
}

View File

@@ -40,6 +40,7 @@ const components = [
'input',
'input-menu',
'input-number',
'input-tags',
'kbd',
'link',
'modal',
@@ -61,6 +62,7 @@ const components = [
'tabs',
'table',
'textarea',
'timeline',
'toast',
'tooltip',
'tree'

View File

@@ -40,6 +40,7 @@ const components = [
'input',
'input-menu',
'input-number',
'input-tags',
'kbd',
'link',
'modal',
@@ -61,6 +62,7 @@ const components = [
'tabs',
'table',
'textarea',
'timeline',
'toast',
'tooltip',
'tree'

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

@@ -10,8 +10,9 @@ const open = ref(false)
const searchTerm = ref('')
// const searchTermDebounced = refDebounced(searchTerm, 200)
const selected = ref([])
const commandPalette = useTemplateRef('commandPalette')
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
const { data: _users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
// params: { q: searchTermDebounced },
transform: (data: User[]) => {
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
@@ -22,10 +23,6 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
const loading = ref(false)
const groups = computed(() => [{
id: 'users',
label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
items: users.value || []
}, {
id: 'actions',
items: [{
label: 'Add new file',
@@ -74,6 +71,57 @@ const groups = computed(() => [{
toast.add({ title: 'Label added!' })
},
kbds: ['meta', 'L']
}, {
label: 'Set Wallpaper',
suffix: 'Choose from beautiful wallpaper collection.',
icon: 'i-lucide-image',
view: 'wallpaper',
placeholder: 'Search wallpapers...'
}, {
label: 'More actions',
placeholder: 'Search actions...',
children: [{
label: 'Create new file',
suffix: 'Create a new file in the current directory or workspace.',
icon: 'i-lucide-file-plus',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'New file added!' })
}
}, {
label: 'Create new folder',
suffix: 'Create a new folder in the current directory or workspace.',
icon: 'i-lucide-folder-plus',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'New folder added!' })
}
}, {
label: 'Share',
placeholder: 'Search share options...',
icon: 'i-lucide-share',
children: [{
label: 'Share with everyone',
suffix: 'Share with everyone in the current directory or workspace.',
icon: 'i-lucide-share',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Shared with everyone!' })
}
}, {
label: 'Share with team',
suffix: 'Share with the team in the current directory or workspace.',
icon: 'i-lucide-users',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Shared with team!' })
}
}]
}]
}]
}])
@@ -95,6 +143,116 @@ const labels = [{
}]
const label = ref()
const wallpapers = [
{
id: 1,
name: 'red_distortion_1',
gradient: 'from-red-500 via-orange-500 to-pink-500',
category: 'Abstract',
featured: true
},
{
id: 2,
name: 'blue_distortion_1',
gradient: 'from-blue-600 via-purple-600 to-indigo-600',
category: 'Abstract',
featured: true
},
{
id: 3,
name: 'mono_dark_distortion_1',
gradient: 'from-gray-900 via-gray-700 to-gray-800',
category: 'Monochrome',
featured: false
},
{
id: 4,
name: 'chromatic_dark_1',
gradient: 'from-emerald-600 via-teal-600 to-cyan-600',
category: 'Chromatic',
featured: true
},
{
id: 5,
name: 'red_distortion_2',
gradient: 'from-rose-600 via-red-600 to-orange-600',
category: 'Abstract',
featured: false
},
{
id: 6,
name: 'purple_cosmic_1',
gradient: 'from-violet-700 via-purple-700 to-fuchsia-700',
category: 'Cosmic',
featured: true
},
{
id: 7,
name: 'golden_sunset_1',
gradient: 'from-yellow-500 via-orange-500 to-red-500',
category: 'Nature',
featured: false
},
{
id: 8,
name: 'ocean_deep_1',
gradient: 'from-blue-800 via-blue-900 to-indigo-900',
category: 'Nature',
featured: true
},
{
id: 9,
name: 'mono_light_distortion_1',
gradient: 'from-gray-200 via-gray-300 to-gray-400',
category: 'Monochrome',
featured: false
},
{
id: 10,
name: 'green_matrix_1',
gradient: 'from-green-800 via-emerald-700 to-teal-700',
category: 'Chromatic',
featured: false
},
{
id: 11,
name: 'pink_dreams_1',
gradient: 'from-pink-500 via-rose-500 to-purple-500',
category: 'Abstract',
featured: true
},
{
id: 12,
name: 'midnight_blue_1',
gradient: 'from-slate-900 via-blue-900 to-indigo-900',
category: 'Nature',
featured: false
}
]
const filteredWallpapers = computed(() => {
let filtered = wallpapers
// Filter by search term
if (searchTerm.value.trim()) {
const search = searchTerm.value.toLowerCase()
filtered = filtered.filter(w =>
w.name.toLowerCase().includes(search)
|| w.category.toLowerCase().includes(search)
)
}
return filtered
})
function setWallpaper(wallpaper: any) {
toast.add({
title: `Wallpaper set to ${wallpaper.name}!`,
description: 'Your desktop wallpaper has been updated.',
icon: 'i-lucide-image'
})
}
// function onSelect(item: typeof groups.value[number]['items'][number]) {
function onSelect(item: any) {
console.log('Selected', item)
@@ -102,6 +260,12 @@ function onSelect(item: any) {
defineShortcuts({
meta_k: () => open.value = !open.value,
meta_shift_a: {
usingInput: true,
handler: () => {
commandPalette.value?.openView('askAI')
}
},
...extractShortcuts(groups.value)
})
</script>
@@ -109,6 +273,7 @@ defineShortcuts({
<template>
<DefineTemplate>
<UCommandPalette
ref="commandPalette"
v-model="selected"
v-model:search-term="searchTerm"
:loading="status === 'pending'"
@@ -121,7 +286,51 @@ defineShortcuts({
multiple
class="sm:max-h-80"
@update:model-value="onSelect"
/>
>
<template #wallpaper>
<div class="flex-1 overflow-y-auto p-6">
<div class="grid grid-cols-4 gap-4">
<div
v-for="wallpaper in filteredWallpapers"
:key="wallpaper.id"
class="group relative cursor-pointer"
@click="setWallpaper(wallpaper)"
>
<div
class="aspect-video rounded-lg bg-gradient-to-br shadow-lg ring-1 ring-black/5"
:class="wallpaper.gradient"
/>
<div class="mt-2 px-1">
<div class="flex items-center gap-2">
<h3 class="text-sm font-medium text-highlighted truncate">
{{ wallpaper.name }}
</h3>
<UChip
v-if="wallpaper.featured"
label="★"
size="xs"
color="primary"
class="shrink-0"
/>
</div>
<p class="text-xs text-dimmed">
{{ wallpaper.category }}
</p>
</div>
</div>
</div>
</div>
</template>
<template #askAI>
<div class="flex flex-col items-center justify-center gap-4 p-6">
<UIcon name="i-lucide-sparkles" class="size-8 text-primary" />
<span class="text-lg font-semibold text-highlighted">
Ask me anything...
</span>
</div>
</template>
</UCommandPalette>
</DefineTemplate>
<div class="flex-1 flex flex-col gap-12 w-full max-w-lg">

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
@@ -145,5 +145,18 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
class="w-48"
/>
</div>
<div class="flex items-center gap-4">
<UInputMenu
v-for="variant in variants"
:key="variant"
:items="items"
:model-value="[fruits[0]!]"
multiple
icon="i-lucide-search"
placeholder="Search..."
:variant="variant"
class="w-48"
/>
</div>
</div>
</template>

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