Compare commits

...

68 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
244 changed files with 8391 additions and 4946 deletions

View File

@@ -1,5 +1,51 @@
# Changelog # 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) ## [3.1.3](https://github.com/nuxt/ui/compare/v3.1.2...v3.1.3) (2025-05-26)
### ⚠ BREAKING CHANGES ### ⚠ BREAKING CHANGES

View File

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

View File

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

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

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

@@ -1,5 +1,6 @@
<script setup lang="ts"> <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' import type { FormSubmitEvent } from '@nuxt/ui'
const schema = object({ const schema = object({

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <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' import type { FormSubmitEvent } from '@nuxt/ui'
const schema = object({ const schema = object({

View File

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

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

View File

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

View File

@@ -2,12 +2,14 @@
import { h, resolveComponent } from 'vue' import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import type { Row } from '@tanstack/vue-table' import type { Row } from '@tanstack/vue-table'
import { useClipboard } from '@vueuse/core'
const UButton = resolveComponent('UButton') const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge') const UBadge = resolveComponent('UBadge')
const UDropdownMenu = resolveComponent('UDropdownMenu') const UDropdownMenu = resolveComponent('UDropdownMenu')
const toast = useToast() const toast = useToast()
const { copy } = useClipboard()
type Payment = { type Payment = {
id: string id: string
@@ -119,7 +121,7 @@ function getRowItems(row: Row<Payment>) {
}, { }, {
label: 'Copy payment ID', label: 'Copy payment ID',
onSelect() { onSelect() {
navigator.clipboard.writeText(row.original.id) copy(row.original.id)
toast.add({ toast.add({
title: 'Payment ID copied to clipboard!', 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> </script>
<template> <template>
<div class=" flex w-full flex-1 gap-1"> <div class="flex w-full flex-1 gap-1">
<div class="flex-1"> <div class="flex-1">
<UTable <UTable
ref="table" ref="table"

View File

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

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

View File

@@ -16,12 +16,12 @@ function handleMessage(message) {
async function handleFormatMessage(message) { async function handleFormatMessage(message) {
if (!globalThis.prettier) { if (!globalThis.prettier) {
await Promise.all([ await Promise.all([
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/standalone.js'), import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/standalone.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/babel.js'), import('https://cdn.jsdelivr.net/npm/prettier@3.6.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.6.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.6.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.6.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/plugins/typescript.js')
]) ])
} }

View File

@@ -32,6 +32,12 @@ props:
You can use any name from the <https://icones.js.org> collection. 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 ### Component Props
Some components also have an `icon` prop to display an icon, like the [Button](/components/button) for example: Some components also have an `icon` prop to display an icon, like the [Button](/components/button) for example:

View File

@@ -125,7 +125,7 @@ Look at the `code` parameter, there you need to pass the iso code of the languag
:: ::
### Extend locale :badge{label="Soon" class="align-text-top"} ### 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: You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:

View File

@@ -127,7 +127,7 @@ Look at the `code` parameter, there you need to pass the iso code of the languag
:: ::
### Extend locale :badge{label="Soon" class="align-text-top"} ### 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: You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:

View File

@@ -9,7 +9,6 @@ links:
- label: GitHub - label: GitHub
icon: i-simple-icons-github icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/CheckboxGroup.vue 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. 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`. 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"} - `loading?: boolean`{lang="ts-type"}
- `disabled?: boolean`{lang="ts-type"} - `disabled?: boolean`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot) - [`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"} - `onSelect?(e?: Event): void`{lang="ts-type"}
- `class?: any`{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. 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 ### Multiple
Use the `multiple` prop to allow multiple selections. 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 ### Loading
Use the `loading` prop to show a loading icon on the CommandPalette. Use the `loading` prop to show a loading icon on the CommandPalette.
@@ -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 ### Close
Use the `close` prop to display a [Button](/components/button) to dismiss the CommandPalette. 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 ## Examples
### Control selected item(s) ### 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. 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 ### With fetched items
You can fetch items from an API and use them in the CommandPalette. 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 ::component-example
--- ---
collapse: true
name: 'command-palette-custom-slot-example' name: 'command-palette-custom-slot-example'
class: '!p-0' class: '!p-0'
props: props:

View File

@@ -782,6 +782,14 @@ name: 'input-menu-countries-example'
:component-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 ComboboxTrigger> \| null>`{lang="ts-type"} |
## Theme ## Theme
:component-theme :component-theme

View File

@@ -1,6 +1,6 @@
--- ---
title: InputNumber title: InputNumber
description: Input numerical values with a customizable range. description: An input for numerical values with a customizable range.
category: form category: form
links: links:
- label: NumberField - label: NumberField
@@ -287,8 +287,8 @@ name: 'input-number-slots-example'
When accessing the component via a template ref, you can use the following: When accessing the component via a template ref, you can use the following:
| Name | Type | | 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 ## Theme

View File

@@ -1,6 +1,7 @@
--- ---
title: InputTags title: InputTags
description: An input element that displays interactive tags. description: An input element that displays interactive tags.
category: form
links: links:
- label: InputTags - label: InputTags
icon: i-custom-reka-ui icon: i-custom-reka-ui
@@ -8,7 +9,7 @@ links:
- label: GitHub - label: GitHub
icon: i-simple-icons-github icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/InputTags.vue to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/InputTags.vue
navigation.badge: Soon navigation.badge: New
--- ---
## Usage ## Usage
@@ -50,6 +51,17 @@ props:
--- ---
:: ::
### 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 ### Color
Use the `color` prop to change the ring color when the InputTags is focused. Use the `color` prop to change the ring color when the InputTags is focused.
@@ -276,8 +288,8 @@ name: 'input-tags-form-field-example'
When accessing the component via a template ref, you can use the following: When accessing the component via a template ref, you can use the following:
| Name | Type | | Name | Type |
|----------------------------|-------------------------------------------------| | -------------------------- | ----------------------------------------------- |
| `inputRef`{lang="ts-type"} | `Ref<HTMLInputElement \| null>`{lang="ts-type"} | | `inputRef`{lang="ts-type"} | `Ref<InstanceType<typeof TagsInputInput> \| null>`{lang="ts-type"} |
## Theme ## Theme

View File

@@ -280,7 +280,7 @@ This example uses the `defineShortcuts` composable to focus the Input when the :
### With mask ### With mask
There's no built-in support for masks, but you can use librairies like [maska](https://github.com/beholdr/maska) to mask the Input. 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 ::component-example
--- ---

View File

@@ -889,7 +889,7 @@ You can inspect the DOM to see each item's content being rendered.
## Examples ## Examples
### With tooltip in items :badge{label="New" class="align-text-top"} ### With tooltip in items
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. 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.
@@ -994,7 +994,7 @@ props:
--- ---
:: ::
### With popover in items :badge{label="New" class="align-text-top"} ### 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. 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.

View File

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

View File

@@ -202,7 +202,17 @@ name: 'popover-command-palette-example'
--- ---
:: ::
### With anchor slot :badge{label="New" class="align-text-top"} ### 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. You can use the `#anchor` slot to position the Popover against a custom element.

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. 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`. Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.

View File

@@ -815,6 +815,14 @@ name: 'select-menu-countries-example'
:component-emits :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 ## Theme
:component-theme :component-theme

View File

@@ -709,6 +709,14 @@ collapse: true
:component-emits :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 ## Theme
:component-theme :component-theme

View File

@@ -136,7 +136,7 @@ props:
--- ---
:: ::
### Tooltip :badge{label="New" 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. 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

@@ -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"} - `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"} - `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"} - `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"} - `meta`: [Extra properties for the column.]{class="text-muted"}
- `class`: - `class`:
@@ -161,7 +162,7 @@ props:
### Sticky ### Sticky
Use the `sticky` prop to make the header sticky. Use the `sticky` prop to make the header or footer sticky.
::component-code ::component-code
--- ---
@@ -172,6 +173,10 @@ ignore:
- class - class
external: external:
- data - data
items:
sticky:
- true
- false
props: props:
sticky: true sticky: true
data: data:
@@ -266,8 +271,8 @@ You can group rows based on a given column value and show/hide sub rows via some
#### Important parts: #### Important parts:
* Add prop `grouping` to `UTable` component with an array of column ids you want to group by. * Add `grouping` prop 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-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. * 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. * 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. * `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`). 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 ::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 ::component-example
--- ---
prettier: true prettier: true
collapse: true collapse: true
name: 'table-row-selection-event-example' name: 'table-row-select-event-example'
highlights: highlights:
- 123 - 123
- 130 - 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 ### 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). 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

@@ -242,6 +242,14 @@ You will have access to the following slots:
:component-emits :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 ## Theme
:component-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. 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. 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. Use the `loading` prop to show a loading icon on the Textarea.
@@ -192,7 +192,7 @@ 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-loader-circle`. Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.

View File

@@ -6,7 +6,7 @@ links:
- label: GitHub - label: GitHub
icon: i-simple-icons-github icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Timeline.vue to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Timeline.vue
navigation.badge: Soon navigation.badge: New
--- ---
## Usage ## Usage
@@ -173,6 +173,46 @@ 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 ## Examples
### Control active item ### Control active item

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"}. 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 ## API
### Props ### Props

View File

@@ -11,39 +11,40 @@
"dependencies": { "dependencies": {
"@ai-sdk/vue": "^1.2.12", "@ai-sdk/vue": "^1.2.12",
"@iconify-json/logos": "^1.2.4", "@iconify-json/logos": "^1.2.4",
"@iconify-json/lucide": "^1.2.47", "@iconify-json/lucide": "^1.2.56",
"@iconify-json/simple-icons": "^1.2.38", "@iconify-json/simple-icons": "^1.2.42",
"@iconify-json/vscode-icons": "^1.2.22", "@iconify-json/vscode-icons": "^1.2.23",
"@nuxt/content": "^3.5.1", "@nuxt/content": "^3.6.3",
"@nuxt/image": "^1.10.0", "@nuxt/image": "^1.10.0",
"@nuxt/ui": "workspace:*", "@nuxt/ui": "workspace:*",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@beebbd4", "@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@22fdc5e",
"@nuxthub/core": "^0.9.0", "@nuxthub/core": "^0.9.0",
"@nuxtjs/plausible": "^1.2.0", "@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^22.0.0", "@octokit/rest": "^22.0.0",
"@rollup/plugin-yaml": "^4.1.2", "@rollup/plugin-yaml": "^4.1.2",
"@vueuse/integrations": "^13.3.0", "@vueuse/integrations": "^13.5.0",
"@vueuse/nuxt": "^13.3.0", "@vueuse/nuxt": "^13.5.0",
"ai": "^4.3.16", "ai": "^4.3.16",
"better-sqlite3": "^12.2.0",
"capture-website": "^4.2.0", "capture-website": "^4.2.0",
"joi": "^17.13.3", "joi": "^17.13.3",
"maska": "^3.1.1", "maska": "^3.2.0",
"motion-v": "^1.2.1", "motion-v": "^1.5.0",
"nuxt": "^3.17.5", "nuxt": "^3.17.6",
"nuxt-component-meta": "^0.11.0", "nuxt-component-meta": "^0.12.1",
"nuxt-llms": "^0.1.3", "nuxt-llms": "^0.1.3",
"nuxt-og-image": "^5.1.6", "nuxt-og-image": "^5.1.9",
"prettier": "^3.5.3", "prettier": "^3.6.2",
"shiki-transformer-color-highlight": "^1.0.0", "shiki-transformer-color-highlight": "^1.0.0",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.6",
"superstruct": "^2.0.2", "superstruct": "^2.0.2",
"ufo": "^1.6.1", "ufo": "^1.6.1",
"valibot": "^1.1.0", "valibot": "^1.1.0",
"workers-ai-provider": "^0.6.0", "workers-ai-provider": "^0.7.1",
"yup": "^1.6.1", "yup": "^1.6.1",
"zod": "^3.25.57" "zod": "^3.25.75"
}, },
"devDependencies": { "devDependencies": {
"wrangler": "^4.19.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: 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

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 { H3Event } from 'h3'
import type { PageCollectionItemBase } from '@nuxt/content' 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) => { export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('content:llms:generate:document', async (_: H3Event, doc: PageCollectionItemBase) => { nitroApp.hooks.hook('content:llms:generate:document', async (_: H3Event, doc: PageCollectionItemBase) => {
const componentName = camelCase(doc.title) transformMDC(doc as any)
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`)
})
}) })
}) })

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", "name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.", "description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "3.1.3", "version": "3.2.0",
"packageManager": "pnpm@10.12.1", "packageManager": "pnpm@10.12.4",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/nuxt/ui.git" "url": "git+https://github.com/nuxt/ui.git"
@@ -98,9 +98,9 @@
"prepack": "pnpm build", "prepack": "pnpm build",
"dev": "nuxt dev playground --uiDev", "dev": "nuxt dev playground --uiDev",
"dev:build": "nuxt build playground", "dev:build": "nuxt build playground",
"dev:vue": "vite playground-vue -- --uiDev", "dev:vue": "pnpm --filter playground-vue dev -- --uiDev",
"dev:vue:build": "vite build playground-vue", "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 && vite build playground-vue", "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": "nuxt dev docs --uiDev",
"docs:build": "nuxt build docs", "docs:build": "nuxt build docs",
"lint": "eslint .", "lint": "eslint .",
@@ -115,17 +115,17 @@
"@internationalized/date": "^3.8.2", "@internationalized/date": "^3.8.2",
"@internationalized/number": "^3.6.3", "@internationalized/number": "^3.6.3",
"@nuxt/fonts": "^0.11.4", "@nuxt/fonts": "^0.11.4",
"@nuxt/icon": "^1.13.0", "@nuxt/icon": "^1.15.0",
"@nuxt/kit": "^3.17.5", "@nuxt/kit": "^3.17.6",
"@nuxt/schema": "^3.17.5", "@nuxt/schema": "^3.17.6",
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"@standard-schema/spec": "^1.0.0", "@standard-schema/spec": "^1.0.0",
"@tailwindcss/postcss": "^4.1.10", "@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/vite": "^4.1.10", "@tailwindcss/vite": "^4.1.11",
"@tanstack/vue-table": "^8.21.3", "@tanstack/vue-table": "^8.21.3",
"@unhead/vue": "^2.0.10", "@unhead/vue": "^2.0.12",
"@vueuse/core": "^13.3.0", "@vueuse/core": "^13.5.0",
"@vueuse/integrations": "^13.3.0", "@vueuse/integrations": "^13.5.0",
"colortranslator": "^5.0.0", "colortranslator": "^5.0.0",
"consola": "^3.4.2", "consola": "^3.4.2",
"defu": "^6.1.4", "defu": "^6.1.4",
@@ -143,31 +143,31 @@
"mlly": "^1.7.4", "mlly": "^1.7.4",
"ohash": "^2.0.11", "ohash": "^2.0.11",
"pathe": "^2.0.3", "pathe": "^2.0.3",
"reka-ui": "2.3.1", "reka-ui": "2.3.2",
"scule": "^1.3.0", "scule": "^1.3.0",
"tailwind-variants": "^1.0.0", "tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.10", "tailwindcss": "^4.1.11",
"tinyglobby": "^0.2.14", "tinyglobby": "^0.2.14",
"unplugin": "^2.3.5", "unplugin": "^2.3.5",
"unplugin-auto-import": "^19.3.0", "unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.7.0", "unplugin-vue-components": "^28.8.0",
"vaul-vue": "0.4.1", "vaul-vue": "0.4.1",
"vue-component-type-helpers": "^2.2.10" "vue-component-type-helpers": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint-config": "^1.4.1", "@nuxt/eslint-config": "^1.5.2",
"@nuxt/module-builder": "^1.0.1", "@nuxt/module-builder": "^1.0.1",
"@nuxt/test-utils": "^3.19.1", "@nuxt/test-utils": "^3.19.2",
"@release-it/conventional-changelog": "^10.0.1", "@release-it/conventional-changelog": "^10.0.1",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"embla-carousel": "^8.6.0", "embla-carousel": "^8.6.0",
"eslint": "^9.28.0", "eslint": "^9.30.1",
"happy-dom": "^17.6.3", "happy-dom": "^18.0.1",
"nuxt": "^3.17.5", "nuxt": "^3.17.6",
"release-it": "^19.0.3", "release-it": "^19.0.3",
"vitest": "^3.2.3", "vitest": "^3.2.4",
"vitest-environment-nuxt": "^1.0.1", "vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.2.10" "vue-tsc": "^3.0.1"
}, },
"peerDependencies": { "peerDependencies": {
"@inertiajs/vue3": "^2.0.7", "@inertiajs/vue3": "^2.0.7",

View File

@@ -11,14 +11,14 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/ui": "workspace:*", "@nuxt/ui": "workspace:*",
"vue": "^3.5.16", "vue": "^3.5.17",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
"zod": "^3.25.57" "zod": "^3.25.75"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.4", "@vitejs/plugin-vue": "^5.2.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.3.5", "vite": "^6.3.5",
"vue-tsc": "^2.2.10" "vue-tsc": "^3.0.1"
} }
} }

View File

@@ -10,8 +10,9 @@ const open = ref(false)
const searchTerm = ref('') const searchTerm = ref('')
// const searchTermDebounced = refDebounced(searchTerm, 200) // const searchTermDebounced = refDebounced(searchTerm, 200)
const selected = ref([]) 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 }, // params: { q: searchTermDebounced },
transform: (data: User[]) => { 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}` } })) || [] 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 loading = ref(false)
const groups = computed(() => [{ const groups = computed(() => [{
id: 'users',
label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
items: users.value || []
}, {
id: 'actions', id: 'actions',
items: [{ items: [{
label: 'Add new file', label: 'Add new file',
@@ -74,6 +71,57 @@ const groups = computed(() => [{
toast.add({ title: 'Label added!' }) toast.add({ title: 'Label added!' })
}, },
kbds: ['meta', 'L'] 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 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: typeof groups.value[number]['items'][number]) {
function onSelect(item: any) { function onSelect(item: any) {
console.log('Selected', item) console.log('Selected', item)
@@ -102,6 +260,12 @@ function onSelect(item: any) {
defineShortcuts({ defineShortcuts({
meta_k: () => open.value = !open.value, meta_k: () => open.value = !open.value,
meta_shift_a: {
usingInput: true,
handler: () => {
commandPalette.value?.openView('askAI')
}
},
...extractShortcuts(groups.value) ...extractShortcuts(groups.value)
}) })
</script> </script>
@@ -109,6 +273,7 @@ defineShortcuts({
<template> <template>
<DefineTemplate> <DefineTemplate>
<UCommandPalette <UCommandPalette
ref="commandPalette"
v-model="selected" v-model="selected"
v-model:search-term="searchTerm" v-model:search-term="searchTerm"
:loading="status === 'pending'" :loading="status === 'pending'"
@@ -121,7 +286,51 @@ defineShortcuts({
multiple multiple
class="sm:max-h-80" class="sm:max-h-80"
@update:model-value="onSelect" @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> </DefineTemplate>
<div class="flex-1 flex flex-col gap-12 w-full max-w-lg"> <div class="flex-1 flex flex-col gap-12 w-full max-w-lg">

View File

@@ -3,6 +3,7 @@ import { h, resolveComponent } from 'vue'
import { upperFirst } from 'scule' import { upperFirst } from 'scule'
import type { TableColumn, TableRow } from '@nuxt/ui' import type { TableColumn, TableRow } from '@nuxt/ui'
import { getPaginationRowModel } from '@tanstack/vue-table' import { getPaginationRowModel } from '@tanstack/vue-table'
import { useClipboard, refDebounced } from '@vueuse/core'
const UButton = resolveComponent('UButton') const UButton = resolveComponent('UButton')
const UCheckbox = resolveComponent('UCheckbox') const UCheckbox = resolveComponent('UCheckbox')
@@ -10,6 +11,7 @@ const UBadge = resolveComponent('UBadge')
const UDropdownMenu = resolveComponent('UDropdownMenu') const UDropdownMenu = resolveComponent('UDropdownMenu')
const toast = useToast() const toast = useToast()
const { copy } = useClipboard()
type Payment = { type Payment = {
id: string id: string
@@ -145,6 +147,35 @@ const data = ref<Payment[]>([{
const currentID = ref(4601) const currentID = ref(4601)
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'
}]
}
const columns: TableColumn<Payment>[] = [{ const columns: TableColumn<Payment>[] = [{
id: 'select', id: 'select',
header: ({ table }) => h(UCheckbox, { header: ({ table }) => h(UCheckbox, {
@@ -211,6 +242,16 @@ const columns: TableColumn<Payment>[] = [{
}, { }, {
accessorKey: 'amount', accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, '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 }) => { cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount')) const amount = Number.parseFloat(row.getValue('amount'))
@@ -225,38 +266,11 @@ const columns: TableColumn<Payment>[] = [{
id: 'actions', id: 'actions',
enableHiding: false, enableHiding: false,
cell: ({ row }) => { cell: ({ row }) => {
const items = [{
type: 'label',
label: 'Actions'
}, {
label: 'Copy payment ID',
onSelect() {
navigator.clipboard.writeText(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'
}, {
label: 'View customer'
}, {
label: 'View payment details'
}]
return h('div', { class: 'text-right' }, h(UDropdownMenu, { return h('div', { class: 'text-right' }, h(UDropdownMenu, {
'content': { 'content': {
align: 'end' align: 'end'
}, },
items, 'items': getRowItems(row),
'aria-label': 'Actions dropdown' 'aria-label': 'Actions dropdown'
}, () => h(UButton, { }, () => h(UButton, {
'icon': 'i-lucide-ellipsis-vertical', 'icon': 'i-lucide-ellipsis-vertical',
@@ -294,8 +308,41 @@ function randomize() {
data.value = data.value.sort(() => Math.random() - 0.5) data.value = data.value.sort(() => Math.random() - 0.5)
} }
const rowSelection = ref<Record<string, boolean>>({})
function onSelect(row: TableRow<Payment>) { function onSelect(row: TableRow<Payment>) {
console.log(row) row.toggleSelected(!row.getIsSelected())
}
const contextmenuRow = ref<TableRow<Payment> | null>(null)
const contextmenuItems = computed(() => contextmenuRow.value ? getRowItems(contextmenuRow.value) : [])
function onContextmenu(e: Event, row: TableRow<Payment>) {
contextmenuRow.value = row
}
const popoverOpen = ref(false)
const popoverOpenDebounced = refDebounced(popoverOpen, 1)
const popoverAnchor = ref({ x: 0, y: 0 })
const popoverRow = ref<TableRow<Payment> | null>(null)
const reference = computed(() => ({
getBoundingClientRect: () =>
({
width: 0,
height: 0,
left: popoverAnchor.value.x,
right: popoverAnchor.value.x,
top: popoverAnchor.value.y,
bottom: popoverAnchor.value.y,
...popoverAnchor.value
} as DOMRect)
}))
function onHover(_e: Event, row: TableRow<Payment> | null) {
popoverRow.value = row
popoverOpen.value = !!row
} }
onMounted(() => { onMounted(() => {
@@ -342,27 +389,44 @@ onMounted(() => {
</UDropdownMenu> </UDropdownMenu>
</div> </div>
<UTable <UContextMenu :items="contextmenuItems">
ref="table" <UTable
:data="data" ref="table"
:columns="columns" :data="data"
:column-pinning="columnPinning" :columns="columns"
:loading="loading" :column-pinning="columnPinning"
:pagination="pagination" :row-selection="rowSelection"
:pagination-options="{ :loading="loading"
getPaginationRowModel: getPaginationRowModel() :pagination="pagination"
}" :pagination-options="{
:ui="{ getPaginationRowModel: getPaginationRowModel()
tr: 'divide-x divide-default' }"
}" :ui="{
sticky tr: 'divide-x divide-default'
class="border border-accented rounded-sm" }"
@select="onSelect" sticky
> class="border border-accented rounded-sm"
<template #expanded="{ row }"> @select="onSelect"
<pre>{{ row.original }}</pre> @contextmenu="onContextmenu"
@pointermove="(ev: PointerEvent) => {
popoverAnchor.x = ev.clientX
popoverAnchor.y = ev.clientY
}"
@hover="onHover"
>
<template #expanded="{ row }">
<pre>{{ row.original }}</pre>
</template>
</UTable>
</UContextMenu>
<UPopover :content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }" :open="popoverOpenDebounced" :reference="reference">
<template #content>
<div class="p-4">
{{ popoverRow?.original?.id }}
</div>
</template> </template>
</UTable> </UPopover>
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<div class="text-sm text-muted"> <div class="text-sm text-muted">

View File

@@ -9,6 +9,7 @@ const orientations = Object.keys(theme.variants.orientation)
const orientation = ref('vertical' as const) const orientation = ref('vertical' as const)
const color = ref('primary' as const) const color = ref('primary' as const)
const size = ref('md' as const) const size = ref('md' as const)
const reverse = ref(false)
const items = [{ const items = [{
date: 'Mar 15, 2025', date: 'Mar 15, 2025',
@@ -46,6 +47,7 @@ const value = ref('kickoff')
<USelect v-model="orientation" :items="orientations" placeholder="Orientation" /> <USelect v-model="orientation" :items="orientations" placeholder="Orientation" />
<USelect v-model="size" :items="sizes" placeholder="Size" /> <USelect v-model="size" :items="sizes" placeholder="Size" />
<USelect v-model="value" :items="items.map(item => item.value)" placeholder="Value" /> <USelect v-model="value" :items="items.map(item => item.value)" placeholder="Value" />
<USelect v-model="reverse" :items="[true, false]" placeholder="Reverse" />
</div> </div>
<UTimeline <UTimeline
@@ -54,6 +56,7 @@ const value = ref('kickoff')
:orientation="orientation" :orientation="orientation"
:size="size" :size="size"
:items="items" :items="items"
:reverse="reverse"
class="data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-96" class="data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-96"
/> />
</div> </div>

View File

@@ -9,17 +9,17 @@
"typecheck": "nuxt typecheck" "typecheck": "nuxt typecheck"
}, },
"dependencies": { "dependencies": {
"@iconify-json/lucide": "^1.2.47", "@iconify-json/lucide": "^1.2.56",
"@iconify-json/simple-icons": "^1.2.38", "@iconify-json/simple-icons": "^1.2.42",
"@internationalized/date": "^3.8.2", "@internationalized/date": "^3.8.2",
"@nuxt/ui": "workspace:*", "@nuxt/ui": "workspace:*",
"@nuxthub/core": "^0.9.0", "@nuxthub/core": "^0.9.0",
"nuxt": "^3.17.5", "nuxt": "^3.17.6",
"zod": "^3.25.57" "zod": "^3.25.75"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vue-tsc": "^2.2.10" "vue-tsc": "^3.0.1"
}, },
"resolutions": { "resolutions": {
"unimport": "4.1.1" "unimport": "4.1.1"

4832
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,12 @@
"reka-ui", "reka-ui",
"vaul-vue" "vaul-vue"
] ]
}, {
"groupName": "vue-tsc",
"matchPackageNames": [
"vue-tsc",
"vue-component-type-helpers"
]
}, { }, {
"matchDepTypes": ["peerDependencies"], "matchDepTypes": ["peerDependencies"],
"enabled": false "enabled": false

View File

@@ -11,7 +11,7 @@ import { defu } from 'defu'
/** /**
* This plugin adds all the Nuxt UI components as auto-imports. * This plugin adds all the Nuxt UI components as auto-imports.
*/ */
export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix: NonNullable<NuxtUIOptions['prefix']> }, meta: UnpluginContextMeta) { export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix: NonNullable<NuxtUIOptions['prefix']>, extraRuntimeDir?: string }, meta: UnpluginContextMeta) {
const components = globSync('**/*.vue', { cwd: join(runtimeDir, 'components') }) const components = globSync('**/*.vue', { cwd: join(runtimeDir, 'components') })
const componentNames = new Set(components.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`)) const componentNames = new Set(components.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))
@@ -50,13 +50,15 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
name: 'nuxt:ui:components', name: 'nuxt:ui:components',
enforce: 'pre', enforce: 'pre',
resolveId(id, importer) { resolveId(id, importer) {
// only apply to runtime nuxt ui components if (!importer) {
if (!importer || !normalize(importer).includes(runtimeDir)) { return
}
if (!normalize(importer).includes(runtimeDir) && (!options.extraRuntimeDir || !normalize(importer).includes(options.extraRuntimeDir))) {
return return
} }
// only apply to relative imports // only apply to relative imports or nuxt ui runtime components
if (!RELATIVE_IMPORT_RE.test(id)) { if (!RELATIVE_IMPORT_RE.test(id) && !id.startsWith('@nuxt/ui/components/')) {
return return
} }

View File

@@ -3,7 +3,8 @@ import { normalize } from 'pathe'
import { resolvePathSync } from 'mlly' import { resolvePathSync } from 'mlly'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { runtimeDir, type NuxtUIOptions } from '../unplugin' import { runtimeDir } from '../unplugin'
import type { NuxtUIOptions } from '../unplugin'
/** /**
* This plugin normalises Nuxt environment (#imports) and `import.meta.client` within the Nuxt UI components. * This plugin normalises Nuxt environment (#imports) and `import.meta.client` within the Nuxt UI components.

View File

@@ -4,7 +4,8 @@ import { genSafeVariableName } from 'knitwork'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { resolvePathSync } from 'mlly' import { resolvePathSync } from 'mlly'
import { runtimeDir, type NuxtUIOptions } from '../unplugin' import { runtimeDir } from '../unplugin'
import type { NuxtUIOptions } from '../unplugin'
import type { UnpluginOptions } from 'unplugin' import type { UnpluginOptions } from 'unplugin'

View File

@@ -133,7 +133,6 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.alert || {})
<UButton <UButton
v-if="close" v-if="close"
:icon="closeIcon || appConfig.ui.icons.close" :icon="closeIcon || appConfig.ui.icons.close"
size="md"
color="neutral" color="neutral"
variant="link" variant="link"
:aria-label="t('alert.close')" :aria-label="t('alert.close')"

View File

@@ -42,14 +42,15 @@ export interface ButtonSlots {
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { type Ref, computed, ref, inject } from 'vue' import { computed, ref, inject } from 'vue'
import type { Ref } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { useForwardProps } from 'reka-ui' import { useForwardProps } from 'reka-ui'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
import { useComponentIcons } from '../composables/useComponentIcons' import { useComponentIcons } from '../composables/useComponentIcons'
import { useButtonGroup } from '../composables/useButtonGroup' import { useButtonGroup } from '../composables/useButtonGroup'
import { formLoadingInjectionKey } from '../composables/useFormField' import { formLoadingInjectionKey } from '../composables/useFormField'
import { omit } from '../utils' import { omit, mergeClasses } from '../utils'
import { tv } from '../utils/tv' import { tv } from '../utils/tv'
import { pickLinkProps } from '../utils/link' import { pickLinkProps } from '../utils/link'
import UIcon from './Icon.vue' import UIcon from './Icon.vue'
@@ -57,11 +58,7 @@ import UAvatar from './Avatar.vue'
import ULink from './Link.vue' import ULink from './Link.vue'
import ULinkBase from './LinkBase.vue' import ULinkBase from './LinkBase.vue'
const props = withDefaults(defineProps<ButtonProps>(), { const props = defineProps<ButtonProps>()
active: undefined,
activeClass: '',
inactiveClass: ''
})
const slots = defineSlots<ButtonSlots>() const slots = defineSlots<ButtonSlots>()
const appConfig = useAppConfig() as Button['AppConfig'] const appConfig = useAppConfig() as Button['AppConfig']
@@ -96,10 +93,10 @@ const ui = computed(() => tv({
variants: { variants: {
active: { active: {
true: { true: {
base: props.activeClass base: mergeClasses(appConfig.ui?.button?.variants?.active?.true?.base, props.activeClass)
}, },
false: { false: {
base: props.inactiveClass base: mergeClasses(appConfig.ui?.button?.variants?.active?.false?.base, props.inactiveClass)
} }
} }
} }

View File

@@ -118,7 +118,7 @@ export interface CarouselEmits {
import { computed, ref, watch, onMounted } from 'vue' import { computed, ref, watch, onMounted } from 'vue'
import useEmblaCarousel from 'embla-carousel-vue' import useEmblaCarousel from 'embla-carousel-vue'
import { Primitive, useForwardProps } from 'reka-ui' import { Primitive, useForwardProps } from 'reka-ui'
import { reactivePick, computedAsync } from '@vueuse/core' import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale' import { useLocale } from '../composables/useLocale'
import { tv } from '../utils/tv' import { tv } from '../utils/tv'
@@ -175,41 +175,45 @@ const options = computed<EmblaOptionsType>(() => ({
direction: dir.value === 'rtl' ? 'rtl' : 'ltr' direction: dir.value === 'rtl' ? 'rtl' : 'ltr'
})) }))
const plugins = computedAsync<EmblaPluginType[]>(async () => { const plugins = ref<EmblaPluginType[]>([])
const plugins = []
async function loadPlugins() {
const emblaPlugins: EmblaPluginType[] = []
if (props.autoplay) { if (props.autoplay) {
const AutoplayPlugin = await import('embla-carousel-autoplay').then(r => r.default) const AutoplayPlugin = await import('embla-carousel-autoplay').then(r => r.default)
plugins.push(AutoplayPlugin(typeof props.autoplay === 'boolean' ? {} : props.autoplay)) emblaPlugins.push(AutoplayPlugin(typeof props.autoplay === 'boolean' ? {} : props.autoplay))
} }
if (props.autoScroll) { if (props.autoScroll) {
const AutoScrollPlugin = await import('embla-carousel-auto-scroll').then(r => r.default) const AutoScrollPlugin = await import('embla-carousel-auto-scroll').then(r => r.default)
plugins.push(AutoScrollPlugin(typeof props.autoScroll === 'boolean' ? {} : props.autoScroll)) emblaPlugins.push(AutoScrollPlugin(typeof props.autoScroll === 'boolean' ? {} : props.autoScroll))
} }
if (props.autoHeight) { if (props.autoHeight) {
const AutoHeightPlugin = await import('embla-carousel-auto-height').then(r => r.default) const AutoHeightPlugin = await import('embla-carousel-auto-height').then(r => r.default)
plugins.push(AutoHeightPlugin(typeof props.autoHeight === 'boolean' ? {} : props.autoHeight)) emblaPlugins.push(AutoHeightPlugin(typeof props.autoHeight === 'boolean' ? {} : props.autoHeight))
} }
if (props.classNames) { if (props.classNames) {
const ClassNamesPlugin = await import('embla-carousel-class-names').then(r => r.default) const ClassNamesPlugin = await import('embla-carousel-class-names').then(r => r.default)
plugins.push(ClassNamesPlugin(typeof props.classNames === 'boolean' ? {} : props.classNames)) emblaPlugins.push(ClassNamesPlugin(typeof props.classNames === 'boolean' ? {} : props.classNames))
} }
if (props.fade) { if (props.fade) {
const FadePlugin = await import('embla-carousel-fade').then(r => r.default) const FadePlugin = await import('embla-carousel-fade').then(r => r.default)
plugins.push(FadePlugin(typeof props.fade === 'boolean' ? {} : props.fade)) emblaPlugins.push(FadePlugin(typeof props.fade === 'boolean' ? {} : props.fade))
} }
if (props.wheelGestures) { if (props.wheelGestures) {
const { WheelGesturesPlugin } = await import('embla-carousel-wheel-gestures') const { WheelGesturesPlugin } = await import('embla-carousel-wheel-gestures')
plugins.push(WheelGesturesPlugin(typeof props.wheelGestures === 'boolean' ? {} : props.wheelGestures)) emblaPlugins.push(WheelGesturesPlugin(typeof props.wheelGestures === 'boolean' ? {} : props.wheelGestures))
} }
return plugins plugins.value = emblaPlugins
}) }
watch(() => [props.autoplay, props.autoScroll, props.autoHeight, props.classNames, props.fade, props.wheelGestures], loadPlugins, { immediate: true })
const [emblaRef, emblaApi] = useEmblaCarousel(options.value, plugins.value) const [emblaRef, emblaApi] = useEmblaCarousel(options.value, plugins.value)
@@ -310,7 +314,6 @@ defineExpose({
<UButton <UButton
:disabled="!canScrollPrev" :disabled="!canScrollPrev"
:icon="prevIcon" :icon="prevIcon"
size="md"
color="neutral" color="neutral"
variant="outline" variant="outline"
:aria-label="t('carousel.prev')" :aria-label="t('carousel.prev')"
@@ -321,7 +324,6 @@ defineExpose({
<UButton <UButton
:disabled="!canScrollNext" :disabled="!canScrollNext"
:icon="nextIcon" :icon="nextIcon"
size="md"
color="neutral" color="neutral"
variant="outline" variant="outline"
:aria-label="t('carousel.next')" :aria-label="t('carousel.next')"

View File

@@ -26,13 +26,23 @@ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
loading?: boolean loading?: boolean
disabled?: boolean disabled?: boolean
slot?: string slot?: string
/**
* The placeholder to display when the item has children.
*/
placeholder?: string
children?: CommandPaletteItem[]
/**
* Custom view to display instead of children items.
* When defined, clicking this item will show the custom view.
*/
view?: string
onSelect?(e?: Event): void onSelect?(e?: Event): void
class?: any class?: any
ui?: Pick<CommandPalette['slots'], 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemLabelPrefix' | 'itemLabelBase' | 'itemLabelSuffix' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon'> ui?: Pick<CommandPalette['slots'], 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemLabelPrefix' | 'itemLabelBase' | 'itemLabelSuffix' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon'>
[key: string]: any [key: string]: any
} }
export interface CommandPaletteGroup<T> { export interface CommandPaletteGroup<T extends CommandPaletteItem = CommandPaletteItem> {
id: string id: string
label?: string label?: string
slot?: string slot?: string
@@ -52,7 +62,7 @@ export interface CommandPaletteGroup<T> {
highlightedIcon?: string highlightedIcon?: string
} }
export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multiple' | 'disabled' | 'modelValue' | 'defaultValue' | 'highlightOnHover'>, Pick<UseComponentIconsProps, 'loading' | 'loadingIcon'> { export interface CommandPaletteProps<G extends CommandPaletteGroup<T> = CommandPaletteGroup<any>, T extends CommandPaletteItem = CommandPaletteItem> extends Pick<ListboxRootProps, 'multiple' | 'disabled' | 'modelValue' | 'defaultValue' | 'highlightOnHover' | 'selectionBehavior'>, Pick<UseComponentIconsProps, 'loading' | 'loadingIcon'> {
/** /**
* The element or component this component should render as. * The element or component this component should render as.
* @defaultValue 'div' * @defaultValue 'div'
@@ -70,6 +80,12 @@ export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multi
* @IconifyIcon * @IconifyIcon
*/ */
selectedIcon?: string selectedIcon?: string
/**
* The icon displayed when an item has children.
* @defaultValue appConfig.ui.icons.chevronRight
* @IconifyIcon
*/
trailingIcon?: string
/** /**
* The placeholder text for the input. * The placeholder text for the input.
* @defaultValue t('commandPalette.placeholder') * @defaultValue t('commandPalette.placeholder')
@@ -93,6 +109,18 @@ export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multi
* @IconifyIcon * @IconifyIcon
*/ */
closeIcon?: string closeIcon?: string
/**
* Display a button to navigate back in history.
* `{ size: 'md', color: 'neutral', variant: 'link' }`{lang="ts-type"}
* @defaultValue true
*/
back?: boolean | ButtonProps
/**
* The icon displayed in the back button.
* @defaultValue appConfig.ui.icons.arrowLeft
* @IconifyIcon
*/
backIcon?: string
groups?: G[] groups?: G[]
/** /**
* Options for [useFuse](https://vueuse.org/integrations/useFuse). * Options for [useFuse](https://vueuse.org/integrations/useFuse).
@@ -116,25 +144,26 @@ export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multi
ui?: CommandPalette['slots'] ui?: CommandPalette['slots']
} }
export type CommandPaletteEmits<T> = ListboxRootEmits<T> & { export type CommandPaletteEmits<T extends CommandPaletteItem = CommandPaletteItem> = ListboxRootEmits<T> & {
'update:open': [value: boolean] 'update:open': [value: boolean]
} }
type SlotProps<T> = (props: { item: T, index: number }) => any type SlotProps<T> = (props: { item: T, index: number }) => any
export type CommandPaletteSlots<G extends { slot?: string }, T extends { slot?: string }> = { export type CommandPaletteSlots<G extends CommandPaletteGroup<T> = CommandPaletteGroup<any>, T extends CommandPaletteItem = CommandPaletteItem> = {
'empty'(props: { searchTerm?: string }): any 'empty'(props: { searchTerm?: string }): any
'back'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'close'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any 'close'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'item': SlotProps<T> 'item': SlotProps<T>
'item-leading': SlotProps<T> 'item-leading': SlotProps<T>
'item-label': SlotProps<T> 'item-label': SlotProps<T>
'item-trailing': SlotProps<T> 'item-trailing': SlotProps<T>
} & Record<string, SlotProps<G>> & Record<string, SlotProps<T>> } & Record<string, SlotProps<G>> & Record<string, SlotProps<T>> & Record<string, (props: { current: any, searchTerm: string, navigateBack: () => void, close: () => void }) => any>
</script> </script>
<script setup lang="ts" generic="G extends CommandPaletteGroup<T>, T extends CommandPaletteItem"> <script setup lang="ts" generic="G extends CommandPaletteGroup<T>, T extends CommandPaletteItem">
import { computed } from 'vue' import { computed, ref, useTemplateRef } from 'vue'
import { ListboxRoot, ListboxFilter, ListboxContent, ListboxGroup, ListboxGroupLabel, ListboxItem, ListboxItemIndicator, useForwardProps, useForwardPropsEmits } from 'reka-ui' import { ListboxRoot, ListboxFilter, ListboxContent, ListboxGroup, ListboxGroupLabel, ListboxItem, ListboxItemIndicator, useForwardProps, useForwardPropsEmits } from 'reka-ui'
import { defu } from 'defu' import { defu } from 'defu'
import { reactivePick } from '@vueuse/core' import { reactivePick } from '@vueuse/core'
@@ -157,7 +186,8 @@ import UKbd from './Kbd.vue'
const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), { const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), {
modelValue: '', modelValue: '',
labelKey: 'label', labelKey: 'label',
autofocus: true autofocus: true,
back: true
}) })
const emits = defineEmits<CommandPaletteEmits<T>>() const emits = defineEmits<CommandPaletteEmits<T>>()
const slots = defineSlots<CommandPaletteSlots<G, T>>() const slots = defineSlots<CommandPaletteSlots<G, T>>()
@@ -167,7 +197,7 @@ const searchTerm = defineModel<string>('searchTerm', { default: '' })
const { t } = useLocale() const { t } = useLocale()
const appConfig = useAppConfig() as CommandPalette['AppConfig'] const appConfig = useAppConfig() as CommandPalette['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'highlightOnHover'), emits) const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'highlightOnHover', 'selectionBehavior'), emits)
const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon')) const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon'))
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
@@ -183,18 +213,27 @@ const fuse = computed(() => defu({}, props.fuse, {
matchAllWhenSearchEmpty: true matchAllWhenSearchEmpty: true
})) }))
const items = computed(() => props.groups?.filter((group) => { const history = ref<(CommandPaletteGroup & { placeholder?: string, view?: string })[]>([])
const placeholder = computed(() => history.value[history.value.length - 1]?.placeholder || props.placeholder || t('commandPalette.placeholder'))
const groups = computed(() => history.value?.length ? [history.value[history.value.length - 1] as G] : props.groups)
const currentView = computed(() => {
const current = history.value[history.value.length - 1]
return current?.view ? current : null
})
const items = computed(() => groups.value?.filter((group) => {
if (!group.id) { if (!group.id) {
console.warn(`[@nuxt/ui] CommandPalette group is missing an \`id\` property`) console.warn(`[@nuxt/ui] CommandPalette group is missing an \`id\` property`)
return false return false
} }
if (group.ignoreFilter) { if (group.ignoreFilter) {
return false return false
} }
return true return true
}).flatMap(group => group.items?.map(item => ({ ...item, group: group.id })) || []) || []) })?.flatMap(group => group.items?.map(item => ({ ...item, group: group.id })) || []) || [])
const { results: fuseResults } = useFuse<typeof items.value[number]>(searchTerm, items, fuse) const { results: fuseResults } = useFuse<typeof items.value[number]>(searchTerm, items, fuse)
@@ -215,7 +254,7 @@ function getGroupWithItems(group: G, items: (T & { matches?: FuseResult<T>['matc
} }
} }
const groups = computed(() => { const filteredGroups = computed(() => {
const groupsById = fuseResults.value.reduce((acc, result) => { const groupsById = fuseResults.value.reduce((acc, result) => {
const { item, matches } = result const { item, matches } = result
if (!item.group) { if (!item.group) {
@@ -229,7 +268,7 @@ const groups = computed(() => {
}, {} as Record<string, (T & { matches?: FuseResult<T>['matches'] })[]>) }, {} as Record<string, (T & { matches?: FuseResult<T>['matches'] })[]>)
const fuseGroups = Object.entries(groupsById).map(([id, items]) => { const fuseGroups = Object.entries(groupsById).map(([id, items]) => {
const group = props.groups?.find(group => group.id === id) const group = groups.value?.find(group => group.id === id)
if (!group) { if (!group) {
return return
} }
@@ -237,7 +276,7 @@ const groups = computed(() => {
return getGroupWithItems(group, items) return getGroupWithItems(group, items)
}).filter(group => !!group) }).filter(group => !!group)
const nonFuseGroups = props.groups const nonFuseGroups = groups.value
?.map((group, index) => ({ ...group, index })) ?.map((group, index) => ({ ...group, index }))
?.filter(group => group.ignoreFilter && group.items?.length) ?.filter(group => group.ignoreFilter && group.items?.length)
?.map(group => ({ ...getGroupWithItems(group, group.items || []), index: group.index })) || [] ?.map(group => ({ ...getGroupWithItems(group, group.items || []), index: group.index })) || []
@@ -247,26 +286,114 @@ const groups = computed(() => {
return acc return acc
}, [...fuseGroups]) }, [...fuseGroups])
}) })
const listboxRootRef = useTemplateRef('listboxRootRef')
// Exposed methods for programmatic control
function openView(viewName: string) {
history.value.push({
id: `view-${viewName}`,
label: viewName,
view: viewName,
items: []
} as any)
searchTerm.value = ''
listboxRootRef.value?.highlightFirstItem()
}
function closeView() {
if (history.value.length > 0) {
navigateBack()
}
}
defineExpose({
openView,
closeView,
navigateBack
})
function navigate(item: T) {
if (!item.children?.length && !item.view) {
return
}
history.value.push({
id: `history-${history.value.length}`,
label: item.label,
slot: item.slot,
placeholder: item.placeholder,
view: item.view,
items: item.children || []
} as any)
searchTerm.value = ''
listboxRootRef.value?.highlightFirstItem()
}
function navigateBack() {
if (!history.value.length) {
return
}
history.value.pop()
searchTerm.value = ''
listboxRootRef.value?.highlightFirstItem()
}
function onBackspace() {
if (!searchTerm.value) {
navigateBack()
}
}
function onSelect(e: Event, item: T) {
if (item.children?.length || item.view) {
e.preventDefault()
navigate(item)
} else {
item.onSelect?.(e)
}
}
</script> </script>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<template> <template>
<ListboxRoot v-bind="rootProps" :class="ui.root({ class: [props.ui?.root, props.class] })"> <ListboxRoot v-bind="rootProps" ref="listboxRootRef" :class="ui.root({ class: [props.ui?.root, props.class] })">
<ListboxFilter v-model="searchTerm" as-child> <ListboxFilter v-model="searchTerm" as-child>
<UInput <UInput
:placeholder="placeholder || t('commandPalette.placeholder')" :placeholder="placeholder"
variant="none" variant="none"
:autofocus="autofocus" :autofocus="autofocus"
v-bind="inputProps" v-bind="inputProps"
:icon="icon || appConfig.ui.icons.search" :icon="icon || appConfig.ui.icons.search"
:class="ui.input({ class: props.ui?.input })" :class="ui.input({ class: props.ui?.input })"
@keydown.backspace="onBackspace"
> >
<template v-if="history?.length && (back || !!slots.back)" #leading>
<slot name="back" :ui="ui">
<UButton
:icon="backIcon || appConfig.ui.icons.arrowLeft"
color="neutral"
variant="link"
:aria-label="t('commandPalette.back')"
v-bind="(typeof back === 'object' ? back as Partial<ButtonProps> : {})"
:class="ui.back({ class: props.ui?.back })"
@click="navigateBack"
/>
</slot>
</template>
<template v-if="close || !!slots.close" #trailing> <template v-if="close || !!slots.close" #trailing>
<slot name="close" :ui="ui"> <slot name="close" :ui="ui">
<UButton <UButton
v-if="close" v-if="close"
:icon="closeIcon || appConfig.ui.icons.close" :icon="closeIcon || appConfig.ui.icons.close"
size="md"
color="neutral" color="neutral"
variant="ghost" variant="ghost"
:aria-label="t('commandPalette.close')" :aria-label="t('commandPalette.close')"
@@ -280,8 +407,18 @@ const groups = computed(() => {
</ListboxFilter> </ListboxFilter>
<ListboxContent :class="ui.content({ class: props.ui?.content })"> <ListboxContent :class="ui.content({ class: props.ui?.content })">
<div v-if="groups?.length" role="presentation" :class="ui.viewport({ class: props.ui?.viewport })"> <div v-if="currentView" :class="ui.viewport({ class: props.ui?.viewport })">
<ListboxGroup v-for="group in groups" :key="`group-${group.id}`" :class="ui.group({ class: props.ui?.group })"> <slot
:name="currentView.view"
:current="currentView"
:search-term="searchTerm"
:navigate-back="navigateBack"
:close="closeView"
/>
</div>
<div v-else-if="filteredGroups?.length" role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
<ListboxGroup v-for="group in filteredGroups" :key="`group-${group.id}`" :class="ui.group({ class: props.ui?.group })">
<ListboxGroupLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })"> <ListboxGroupLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
{{ get(group, props.labelKey as string) }} {{ get(group, props.labelKey as string) }}
</ListboxGroupLabel> </ListboxGroupLabel>
@@ -289,10 +426,10 @@ const groups = computed(() => {
<ListboxItem <ListboxItem
v-for="(item, index) in group.items" v-for="(item, index) in group.items"
:key="`group-${group.id}-${index}`" :key="`group-${group.id}-${index}`"
:value="omit(item, ['matches' as any, 'group' as any, 'onSelect', 'labelHtml', 'suffixHtml'])" :value="omit(item, ['matches' as any, 'group' as any, 'onSelect', 'labelHtml', 'suffixHtml', 'children'])"
:disabled="item.disabled" :disabled="item.disabled"
as-child as-child
@select="item.onSelect" @select="onSelect($event, item)"
> >
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom> <ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class], active: active || item.active })"> <ULinkBase v-bind="slotProps" :class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class], active: active || item.active })">
@@ -323,13 +460,20 @@ const groups = computed(() => {
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, item.ui?.itemTrailing] })"> <span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, item.ui?.itemTrailing] })">
<slot :name="((item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index"> <slot :name="((item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
<span v-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: [props.ui?.itemTrailingKbds, item.ui?.itemTrailingKbds] })"> <UIcon
v-if="(item.children && item.children.length > 0) || item.view"
:name="trailingIcon || appConfig.ui.icons.chevronRight"
:class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })"
/>
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: [props.ui?.itemTrailingKbds, item.ui?.itemTrailingKbds] })">
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((item.ui?.itemTrailingKbdsSize || props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" /> <UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((item.ui?.itemTrailingKbdsSize || props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
</span> </span>
<UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: [props.ui?.itemTrailingHighlightedIcon, item.ui?.itemTrailingHighlightedIcon] })" /> <UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: [props.ui?.itemTrailingHighlightedIcon, item.ui?.itemTrailingHighlightedIcon] })" />
</slot> </slot>
<ListboxItemIndicator as-child> <ListboxItemIndicator v-if="!item.children?.length" as-child>
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })" /> <UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })" />
</ListboxItemIndicator> </ListboxItemIndicator>
</span> </span>

View File

@@ -47,7 +47,6 @@ import ULink from './Link.vue'
import UAvatar from './Avatar.vue' import UAvatar from './Avatar.vue'
import UIcon from './Icon.vue' import UIcon from './Icon.vue'
import UKbd from './Kbd.vue' import UKbd from './Kbd.vue'
// eslint-disable-next-line import/no-self-import
import UContextMenuContent from './ContextMenuContent.vue' import UContextMenuContent from './ContextMenuContent.vue'
const props = defineProps<ContextMenuContentProps<T>>() const props = defineProps<ContextMenuContentProps<T>>()

View File

@@ -53,7 +53,6 @@ import ULink from './Link.vue'
import UAvatar from './Avatar.vue' import UAvatar from './Avatar.vue'
import UIcon from './Icon.vue' import UIcon from './Icon.vue'
import UKbd from './Kbd.vue' import UKbd from './Kbd.vue'
// eslint-disable-next-line import/no-self-import
import UDropdownMenuContent from './DropdownMenuContent.vue' import UDropdownMenuContent from './DropdownMenuContent.vue'
const props = defineProps<DropdownMenuContentProps<T>>() const props = defineProps<DropdownMenuContentProps<T>>()

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import type { DeepReadonly } from 'vue'
import type { AppConfig } from '@nuxt/schema' import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/form' import theme from '#build/ui/form'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput, FormData } from '../types/form' import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput, FormData } from '../types/form'
@@ -64,7 +63,7 @@ export interface FormSlots {
</script> </script>
<script lang="ts" setup generic="S extends FormSchema, T extends boolean = true"> <script lang="ts" setup generic="S extends FormSchema, T extends boolean = true">
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue' import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly, reactive } from 'vue'
import { useEventBus } from '@vueuse/core' import { useEventBus } from '@vueuse/core'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey } from '../composables/useFormField' import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey } from '../composables/useFormField'
@@ -155,9 +154,9 @@ provide('form-errors', errors)
const inputs = ref<{ [P in keyof I]?: { id?: string, pattern?: RegExp } }>({}) const inputs = ref<{ [P in keyof I]?: { id?: string, pattern?: RegExp } }>({})
provide(formInputsInjectionKey, inputs as any) provide(formInputsInjectionKey, inputs as any)
const dirtyFields = new Set<keyof I>() const dirtyFields: Set<keyof I> = reactive(new Set<keyof I>())
const touchedFields = new Set<keyof I>() const touchedFields: Set<keyof I> = reactive(new Set<keyof I>())
const blurredFields = new Set<keyof I>() const blurredFields: Set<keyof I> = reactive(new Set<keyof I>())
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] { function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
return errs.map(err => ({ return errs.map(err => ({
@@ -302,9 +301,9 @@ defineExpose<Form<S>>({
loading, loading,
dirty: computed(() => !!dirtyFields.size), dirty: computed(() => !!dirtyFields.size),
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof I>>, dirtyFields: readonly(dirtyFields),
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof I>>, blurredFields: readonly(blurredFields),
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof I>> touchedFields: readonly(touchedFields)
}) })
</script> </script>

View File

@@ -47,7 +47,8 @@ export interface FormFieldSlots {
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, inject, provide, type Ref, useId } from 'vue' import { computed, ref, inject, provide, useId } from 'vue'
import type { Ref } from 'vue'
import { Primitive, Label } from 'reka-ui' import { Primitive, Label } from 'reka-ui'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
import { formFieldInjectionKey, inputIdInjectionKey } from '../composables/useFormField' import { formFieldInjectionKey, inputIdInjectionKey } from '../composables/useFormField'

View File

@@ -8,7 +8,7 @@ import type { AcceptableValue, ComponentConfig } from '../types/utils'
type Input = ComponentConfig<typeof theme, AppConfig, 'input'> type Input = ComponentConfig<typeof theme, AppConfig, 'input'>
export interface InputProps extends UseComponentIconsProps { export interface InputProps<T extends AcceptableValue = AcceptableValue> extends UseComponentIconsProps {
/** /**
* The element or component this component should render as. * The element or component this component should render as.
* @defaultValue 'div' * @defaultValue 'div'
@@ -38,6 +38,8 @@ export interface InputProps extends UseComponentIconsProps {
disabled?: boolean disabled?: boolean
/** Highlight the ring color like a focus state. */ /** Highlight the ring color like a focus state. */
highlight?: boolean highlight?: boolean
modelValue?: T
defaultValue?: T
modelModifiers?: { modelModifiers?: {
string?: boolean string?: boolean
number?: boolean number?: boolean
@@ -65,6 +67,7 @@ export interface InputSlots {
<script setup lang="ts" generic="T extends AcceptableValue"> <script setup lang="ts" generic="T extends AcceptableValue">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { Primitive } from 'reka-ui' import { Primitive } from 'reka-ui'
import { useVModel } from '@vueuse/core'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup' import { useButtonGroup } from '../composables/useButtonGroup'
import { useComponentIcons } from '../composables/useComponentIcons' import { useComponentIcons } from '../composables/useComponentIcons'
@@ -76,7 +79,7 @@ import UAvatar from './Avatar.vue'
defineOptions({ inheritAttrs: false }) defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<InputProps>(), { const props = withDefaults(defineProps<InputProps<T>>(), {
type: 'text', type: 'text',
autocomplete: 'off', autocomplete: 'off',
autofocusDelay: 0 autofocusDelay: 0
@@ -84,13 +87,12 @@ const props = withDefaults(defineProps<InputProps>(), {
const emits = defineEmits<InputEmits<T>>() const emits = defineEmits<InputEmits<T>>()
const slots = defineSlots<InputSlots>() const slots = defineSlots<InputSlots>()
// eslint-disable-next-line vue/no-dupe-keys const modelValue = useVModel<InputProps<T>, 'modelValue', 'update:modelValue'>(props, 'modelValue', emits, { defaultValue: props.defaultValue })
const [modelValue, modelModifiers] = defineModel<T>()
const appConfig = useAppConfig() as Input['AppConfig'] const appConfig = useAppConfig() as Input['AppConfig']
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps>(props, { deferInputValidation: true }) const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps<T>>(props, { deferInputValidation: true })
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props) const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps<T>>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value) const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
@@ -111,15 +113,15 @@ const inputRef = ref<HTMLInputElement | null>(null)
// Custom function to handle the v-model properties // Custom function to handle the v-model properties
function updateInput(value: string | null) { function updateInput(value: string | null) {
if (modelModifiers.trim) { if (props.modelModifiers?.trim) {
value = value?.trim() ?? null value = value?.trim() ?? null
} }
if (modelModifiers.number || props.type === 'number') { if (props.modelModifiers?.number || props.type === 'number') {
value = looseToNumber(value) value = looseToNumber(value)
} }
if (modelModifiers.nullify) { if (props.modelModifiers?.nullify) {
value ||= null value ||= null
} }
@@ -128,7 +130,7 @@ function updateInput(value: string | null) {
} }
function onInput(event: Event) { function onInput(event: Event) {
if (!modelModifiers.lazy) { if (!props.modelModifiers?.lazy) {
updateInput((event.target as HTMLInputElement).value) updateInput((event.target as HTMLInputElement).value)
} }
} }
@@ -136,12 +138,12 @@ function onInput(event: Event) {
function onChange(event: Event) { function onChange(event: Event) {
const value = (event.target as HTMLInputElement).value const value = (event.target as HTMLInputElement).value
if (modelModifiers.lazy) { if (props.modelModifiers?.lazy) {
updateInput(value) updateInput(value)
} }
// Update trimmed input so that it has same behavior as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63 // Update trimmed input so that it has same behavior as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
if (modelModifiers.trim) { if (props.modelModifiers?.trim) {
(event.target as HTMLInputElement).value = value.trim() (event.target as HTMLInputElement).value = value.trim()
} }

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { AppConfig } from '@nuxt/schema' import type { AppConfig } from '@nuxt/schema'
import type { TagsInputRootProps, AcceptableInputValue } from 'reka-ui' import type { TagsInputRootProps, TagsInputRootEmits, AcceptableInputValue } from 'reka-ui'
import theme from '#build/ui/input-tags' import theme from '#build/ui/input-tags'
import type { UseComponentIconsProps } from '../composables/useComponentIcons' import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps } from '../types' import type { AvatarProps } from '../types'
@@ -18,6 +18,8 @@ export interface InputTagsProps<T extends InputTagItem = InputTagItem> extends P
as?: any as?: any
/** The placeholder text when the input is empty. */ /** The placeholder text when the input is empty. */
placeholder?: string placeholder?: string
/** The maximum number of character allowed. */
maxLength?: number
/** /**
* @defaultValue 'primary' * @defaultValue 'primary'
*/ */
@@ -44,11 +46,10 @@ export interface InputTagsProps<T extends InputTagItem = InputTagItem> extends P
ui?: InputTags['slots'] ui?: InputTags['slots']
} }
export type InputTagsEmits<T extends InputTagItem> = { export interface InputTagsEmits<T extends InputTagItem> extends TagsInputRootEmits<T> {
'update:modelValue': [payload: T[]] change: [event: Event]
'change': [payload: Event] blur: [event: FocusEvent]
'blur': [payload: FocusEvent] focus: [event: FocusEvent]
'focus': [payload: FocusEvent]
} }
type SlotProps<T extends InputTagItem> = (props: { item: T, index: number }) => any type SlotProps<T extends InputTagItem> = (props: { item: T, index: number }) => any
@@ -72,6 +73,7 @@ import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField' import { useFormField } from '../composables/useFormField'
import { tv } from '../utils/tv' import { tv } from '../utils/tv'
import UIcon from './Icon.vue' import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
defineOptions({ inheritAttrs: false }) defineOptions({ inheritAttrs: false })
@@ -182,6 +184,7 @@ defineExpose({
ref="inputRef" ref="inputRef"
v-bind="{ ...$attrs, ...ariaAttrs }" v-bind="{ ...$attrs, ...ariaAttrs }"
:placeholder="placeholder" :placeholder="placeholder"
:max-length="maxLength"
:class="ui.input({ class: props.ui?.input })" :class="ui.input({ class: props.ui?.input })"
/> />

View File

@@ -88,11 +88,12 @@ export interface LinkSlots {
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { defu } from 'defu'
import { isEqual } from 'ohash/utils' import { isEqual } from 'ohash/utils'
import { useForwardProps } from 'reka-ui' import { useForwardProps } from 'reka-ui'
import { defu } from 'defu'
import { reactiveOmit } from '@vueuse/core' import { reactiveOmit } from '@vueuse/core'
import { useRoute, useAppConfig } from '#imports' import { useRoute, useAppConfig } from '#imports'
import { mergeClasses } from '../utils'
import { tv } from '../utils/tv' import { tv } from '../utils/tv'
import { isPartiallyEqual } from '../utils/link' import { isPartiallyEqual } from '../utils/link'
import ULinkBase from './LinkBase.vue' import ULinkBase from './LinkBase.vue'
@@ -103,9 +104,7 @@ const props = withDefaults(defineProps<LinkProps>(), {
as: 'button', as: 'button',
type: 'button', type: 'button',
ariaCurrentValue: 'page', ariaCurrentValue: 'page',
active: undefined, active: undefined
activeClass: '',
inactiveClass: ''
}) })
defineSlots<LinkSlots>() defineSlots<LinkSlots>()
@@ -119,8 +118,8 @@ const ui = computed(() => tv({
...defu({ ...defu({
variants: { variants: {
active: { active: {
true: props.activeClass, true: mergeClasses(appConfig.ui?.link?.variants?.active?.true, props.activeClass),
false: props.inactiveClass false: mergeClasses(appConfig.ui?.link?.variants?.active?.false, props.inactiveClass)
} }
} }
}, appConfig.ui?.link || {}) }, appConfig.ui?.link || {})

View File

@@ -65,6 +65,7 @@ export interface ModalSlots {
header(props: { close: () => void }): any header(props: { close: () => void }): any
title(props?: {}): any title(props?: {}): any
description(props?: {}): any description(props?: {}): any
actions(props?: {}): any
close(props: { close: () => void, ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any close(props: { close: () => void, ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any
body(props: { close: () => void }): any body(props: { close: () => void }): any
footer(props: { close: () => void }): any footer(props: { close: () => void }): any
@@ -166,12 +167,13 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</DialogDescription> </DialogDescription>
</div> </div>
<slot name="actions" />
<DialogClose v-if="props.close || !!slots.close" as-child> <DialogClose v-if="props.close || !!slots.close" as-child>
<slot name="close" :close="close" :ui="ui"> <slot name="close" :close="close" :ui="ui">
<UButton <UButton
v-if="props.close" v-if="props.close"
:icon="closeIcon || appConfig.ui.icons.close" :icon="closeIcon || appConfig.ui.icons.close"
size="md"
color="neutral" color="neutral"
variant="ghost" variant="ghost"
:aria-label="t('modal.close')" :aria-label="t('modal.close')"

View File

@@ -236,20 +236,13 @@ const lists = computed<NavigationMenuItem[][]>(() =>
: [] : []
) )
function getAccordionDefaultValue(list: NavigationMenuItem[]) { function getAccordionDefaultValue(list: NavigationMenuItem[], level = 0) {
function findItemsWithDefaultOpen(items: NavigationMenuItem[], level = 0): string[] { const indexes = list.reduce((acc: string[], item, index) => {
return items.reduce((acc: string[], item, index) => { if (item.defaultOpen || item.open) {
if (item.defaultOpen || item.open) { acc.push(item.value || (level > 0 ? `item-${level}-${index}` : `item-${index}`))
acc.push(item.value || (level > 0 ? `item-${level}-${index}` : `item-${index}`)) }
} return acc
if (item.children?.length) { }, [])
acc.push(...findItemsWithDefaultOpen(item.children, level + 1))
}
return acc
}, [])
}
const indexes = findItemsWithDefaultOpen(list)
return props.type === 'single' ? indexes[0] : indexes return props.type === 'single' ? indexes[0] : indexes
} }
@@ -378,7 +371,14 @@ function getAccordionDefaultValue(list: NavigationMenuItem[]) {
</ULink> </ULink>
<AccordionContent v-if="orientation === 'vertical' && item.children?.length && !collapsed" :class="ui.content({ class: [props.ui?.content, item.ui?.content] })"> <AccordionContent v-if="orientation === 'vertical' && item.children?.length && !collapsed" :class="ui.content({ class: [props.ui?.content, item.ui?.content] })">
<ul :class="ui.childList({ class: props.ui?.childList })"> <AccordionRoot
v-bind="({
...accordionProps,
defaultValue: getAccordionDefaultValue(item.children, level + 1)
} as AccordionRootProps)"
as="ul"
:class="ui.childList({ class: props.ui?.childList })"
>
<ReuseItemTemplate <ReuseItemTemplate
v-for="(childItem, childIndex) in item.children" v-for="(childItem, childIndex) in item.children"
:key="childIndex" :key="childIndex"
@@ -387,7 +387,7 @@ function getAccordionDefaultValue(list: NavigationMenuItem[]) {
:level="level + 1" :level="level + 1"
:class="ui.childItem({ class: [props.ui?.childItem, childItem.ui?.childItem] })" :class="ui.childItem({ class: [props.ui?.childItem, childItem.ui?.childItem] })"
/> />
</ul> </AccordionRoot>
</AccordionContent> </AccordionContent>
</component> </component>
</DefineItemTemplate> </DefineItemTemplate>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useOverlay, type Overlay } from '../composables/useOverlay' import { useOverlay } from '../composables/useOverlay'
import type { Overlay } from '../composables/useOverlay'
const { overlays, unmount, close } = useOverlay() const { overlays, unmount, close } = useOverlay()

View File

@@ -111,7 +111,6 @@ import { tv } from '../utils/tv'
import UButton from './Button.vue' import UButton from './Button.vue'
const props = withDefaults(defineProps<PaginationProps>(), { const props = withDefaults(defineProps<PaginationProps>(), {
size: 'md',
color: 'neutral', color: 'neutral',
variant: 'outline', variant: 'outline',
activeColor: 'primary', activeColor: 'primary',

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { PopoverRootProps, HoverCardRootProps, PopoverRootEmits, PopoverContentProps, PopoverContentEmits, PopoverArrowProps } from 'reka-ui' import type { PopoverRootProps, HoverCardRootProps, PopoverRootEmits, PopoverContentProps, PopoverContentEmits, PopoverArrowProps, HoverCardTriggerProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema' import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/popover' import theme from '#build/ui/popover'
import type { EmitsToProps, ComponentConfig } from '../types/utils' import type { EmitsToProps, ComponentConfig } from '../types/utils'
@@ -27,6 +27,12 @@ export interface PopoverProps extends PopoverRootProps, Pick<HoverCardRootProps,
* @defaultValue true * @defaultValue true
*/ */
portal?: boolean | string | HTMLElement portal?: boolean | string | HTMLElement
/**
* The reference (or anchor) element that is being referred to for positioning.
*
* If not provided will use the current component as anchor.
*/
reference?: HoverCardTriggerProps['reference']
/** /**
* When `false`, the popover will not close when clicking outside or pressing escape. * When `false`, the popover will not close when clicking outside or pressing escape.
* @defaultValue true * @defaultValue true
@@ -100,7 +106,7 @@ const Component = computed(() => props.mode === 'hover' ? HoverCard : Popover)
<template> <template>
<Component.Root v-slot="{ open }" v-bind="rootProps"> <Component.Root v-slot="{ open }" v-bind="rootProps">
<Component.Trigger v-if="!!slots.default" as-child :class="props.class"> <Component.Trigger v-if="!!slots.default || !!reference" as-child :reference="reference" :class="props.class">
<slot :open="open" /> <slot :open="open" />
</Component.Trigger> </Component.Trigger>

View File

@@ -245,6 +245,10 @@ function onUpdateOpen(value: boolean) {
function isSelectItem(item: SelectItem): item is SelectItemBase { function isSelectItem(item: SelectItem): item is SelectItemBase {
return typeof item === 'object' && item !== null return typeof item === 'object' && item !== null
} }
defineExpose({
triggerRef
})
</script> </script>
<!-- eslint-disable vue/no-template-shadow --> <!-- eslint-disable vue/no-template-shadow -->

View File

@@ -363,6 +363,10 @@ function onSelect(e: Event, item: SelectMenuItem) {
function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem { function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
return typeof item === 'object' && item !== null return typeof item === 'object' && item !== null
} }
defineExpose({
triggerRef
})
</script> </script>
<!-- eslint-disable vue/no-template-shadow --> <!-- eslint-disable vue/no-template-shadow -->
@@ -428,7 +432,7 @@ function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
<slot name="content-top" /> <slot name="content-top" />
<ComboboxInput v-if="!!searchInput" v-model="searchTerm" :display-value="() => searchTerm" as-child> <ComboboxInput v-if="!!searchInput" v-model="searchTerm" :display-value="() => searchTerm" as-child>
<UInput autofocus autocomplete="off" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" /> <UInput autofocus autocomplete="off" :size="size" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" />
</ComboboxInput> </ComboboxInput>
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })"> <ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">

View File

@@ -65,6 +65,7 @@ export interface SlideoverSlots {
header(props: { close: () => void }): any header(props: { close: () => void }): any
title(props?: {}): any title(props?: {}): any
description(props?: {}): any description(props?: {}): any
actions(props?: {}): any
close(props: { close: () => void, ui: { [K in keyof Required<Slideover['slots']>]: (props?: Record<string, any>) => string } }): any close(props: { close: () => void, ui: { [K in keyof Required<Slideover['slots']>]: (props?: Record<string, any>) => string } }): any
body(props: { close: () => void }): any body(props: { close: () => void }): any
footer(props: { close: () => void }): any footer(props: { close: () => void }): any
@@ -174,12 +175,13 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
</DialogDescription> </DialogDescription>
</div> </div>
<slot name="actions" />
<DialogClose v-if="props.close || !!slots.close" as-child> <DialogClose v-if="props.close || !!slots.close" as-child>
<slot name="close" :close="close" :ui="ui"> <slot name="close" :close="close" :ui="ui">
<UButton <UButton
v-if="props.close" v-if="props.close"
:icon="closeIcon || appConfig.ui.icons.close" :icon="closeIcon || appConfig.ui.icons.close"
size="md"
color="neutral" color="neutral"
variant="ghost" variant="ghost"
:aria-label="t('slideover.close')" :aria-label="t('slideover.close')"

View File

@@ -61,13 +61,13 @@ export type TableRow<T> = Row<T>
export type TableData = RowData export type TableData = RowData
export type TableColumn<T extends TableData, D = unknown> = ColumnDef<T, D> export type TableColumn<T extends TableData, D = unknown> = ColumnDef<T, D>
export interface TableOptions<T extends TableData> extends Omit<CoreOptions<T>, 'data' | 'columns' | 'getCoreRowModel' | 'state' | 'onStateChange' | 'renderFallbackValue'> { export interface TableOptions<T extends TableData = TableData> extends Omit<CoreOptions<T>, 'data' | 'columns' | 'getCoreRowModel' | 'state' | 'onStateChange' | 'renderFallbackValue'> {
state?: CoreOptions<T>['state'] state?: CoreOptions<T>['state']
onStateChange?: CoreOptions<T>['onStateChange'] onStateChange?: CoreOptions<T>['onStateChange']
renderFallbackValue?: CoreOptions<T>['renderFallbackValue'] renderFallbackValue?: CoreOptions<T>['renderFallbackValue']
} }
export interface TableProps<T extends TableData> extends TableOptions<T> { export interface TableProps<T extends TableData = TableData> extends TableOptions<T> {
/** /**
* The element or component this component should render as. * The element or component this component should render as.
* @defaultValue 'div' * @defaultValue 'div'
@@ -83,10 +83,10 @@ export interface TableProps<T extends TableData> extends TableOptions<T> {
*/ */
empty?: string empty?: string
/** /**
* Whether the table should have a sticky header. * Whether the table should have a sticky header or footer. True for both, 'header' for header only, 'footer' for footer only.
* @defaultValue false * @defaultValue false
*/ */
sticky?: boolean sticky?: boolean | 'header' | 'footer'
/** Whether the table should be in loading state. */ /** Whether the table should be in loading state. */
loading?: boolean loading?: boolean
/** /**
@@ -165,19 +165,24 @@ export interface TableProps<T extends TableData> extends TableOptions<T> {
*/ */
facetedOptions?: FacetedOptions<T> facetedOptions?: FacetedOptions<T>
onSelect?: (row: TableRow<T>, e?: Event) => void onSelect?: (row: TableRow<T>, e?: Event) => void
onHover?: (e: Event, row: TableRow<T> | null) => void
onContextmenu?: ((e: Event, row: TableRow<T>) => void) | Array<((e: Event, row: TableRow<T>) => void)>
class?: any class?: any
ui?: Table['slots'] ui?: Table['slots']
} }
type DynamicHeaderSlots<T, K = keyof T> = Record<string, (props: HeaderContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-header`, (props: HeaderContext<T, unknown>) => any> type DynamicHeaderSlots<T, K = keyof T> = Record<string, (props: HeaderContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-header`, (props: HeaderContext<T, unknown>) => any>
type DynamicFooterSlots<T, K = keyof T> = Record<string, (props: HeaderContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-footer`, (props: HeaderContext<T, unknown>) => any>
type DynamicCellSlots<T, K = keyof T> = Record<string, (props: CellContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-cell`, (props: CellContext<T, unknown>) => any> type DynamicCellSlots<T, K = keyof T> = Record<string, (props: CellContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-cell`, (props: CellContext<T, unknown>) => any>
export type TableSlots<T> = { export type TableSlots<T extends TableData = TableData> = {
expanded: (props: { row: Row<T> }) => any 'expanded': (props: { row: Row<T> }) => any
empty: (props?: {}) => any 'empty': (props?: {}) => any
loading: (props?: {}) => any 'loading': (props?: {}) => any
caption: (props?: {}) => any 'caption': (props?: {}) => any
} & DynamicHeaderSlots<T> & DynamicCellSlots<T> 'body-top': (props?: {}) => any
'body-bottom': (props?: {}) => any
} & DynamicHeaderSlots<T> & DynamicFooterSlots<T> & DynamicCellSlots<T>
</script> </script>
@@ -212,6 +217,22 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.table || {})
loadingAnimation: props.loadingAnimation loadingAnimation: props.loadingAnimation
})) }))
const hasFooter = computed(() => {
function hasFooterRecursive(columns: TableColumn<T>[]): boolean {
for (const column of columns) {
if ('footer' in column) {
return true
}
if ('columns' in column && hasFooterRecursive(column.columns as TableColumn<T>[])) {
return true
}
}
return false
}
return hasFooterRecursive(columns.value)
})
const globalFilterState = defineModel<string>('globalFilter', { default: undefined }) const globalFilterState = defineModel<string>('globalFilter', { default: undefined })
const columnFiltersState = defineModel<ColumnFiltersState>('columnFilters', { default: [] }) const columnFiltersState = defineModel<ColumnFiltersState>('columnFilters', { default: [] })
const columnOrderState = defineModel<ColumnOrderState>('columnOrder', { default: [] }) const columnOrderState = defineModel<ColumnOrderState>('columnOrder', { default: [] })
@@ -226,12 +247,14 @@ const groupingState = defineModel<GroupingState>('grouping', { default: [] })
const expandedState = defineModel<ExpandedState>('expanded', { default: {} }) const expandedState = defineModel<ExpandedState>('expanded', { default: {} })
const paginationState = defineModel<PaginationState>('pagination', { default: {} }) const paginationState = defineModel<PaginationState>('pagination', { default: {} })
const tableRef = ref<HTMLTableElement>() const tableRef = ref<HTMLTableElement | null>(null)
const tableApi = useVueTable({ const tableApi = useVueTable({
...reactiveOmit(props, 'as', 'data', 'columns', 'caption', 'sticky', 'loading', 'loadingColor', 'loadingAnimation', 'class', 'ui'), ...reactiveOmit(props, 'as', 'data', 'columns', 'caption', 'sticky', 'loading', 'loadingColor', 'loadingAnimation', 'class', 'ui'),
data, data,
columns: columns.value, get columns() {
return columns.value
},
meta: meta.value, meta: meta.value,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
...(props.globalFilterOptions || {}), ...(props.globalFilterOptions || {}),
@@ -309,7 +332,7 @@ function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
ref.value = typeof updaterOrValue === 'function' ? updaterOrValue(ref.value) : updaterOrValue ref.value = typeof updaterOrValue === 'function' ? updaterOrValue(ref.value) : updaterOrValue
} }
function handleRowSelect(row: TableRow<T>, e: Event) { function onRowSelect(e: Event, row: TableRow<T>) {
if (!props.onSelect) { if (!props.onSelect) {
return return
} }
@@ -322,9 +345,30 @@ function handleRowSelect(row: TableRow<T>, e: Event) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
// FIXME: `e` should be the first argument for consistency
props.onSelect(row, e) props.onSelect(row, e)
} }
function onRowHover(e: Event, row: TableRow<T> | null) {
if (!props.onHover) {
return
}
props.onHover(e, row)
}
function onRowContextmenu(e: Event, row: TableRow<T>) {
if (!props.onContextmenu) {
return
}
if (Array.isArray(props.onContextmenu)) {
props.onContextmenu.forEach(fn => fn(e, row))
} else {
props.onContextmenu(e, row)
}
}
watch( watch(
() => props.data, () => { () => props.data, () => {
data.value = props.data ? [...props.data] : [] data.value = props.data ? [...props.data] : []
@@ -352,6 +396,7 @@ defineExpose({
v-for="header in headerGroup.headers" v-for="header in headerGroup.headers"
:key="header.id" :key="header.id"
:data-pinned="header.column.getIsPinned()" :data-pinned="header.column.getIsPinned()"
:scope="header.colSpan > 1 ? 'colgroup' : 'col'"
:colspan="header.colSpan > 1 ? header.colSpan : undefined" :colspan="header.colSpan > 1 ? header.colSpan : undefined"
:class="ui.th({ :class="ui.th({
class: [ class: [
@@ -366,14 +411,18 @@ defineExpose({
</slot> </slot>
</th> </th>
</tr> </tr>
<tr :class="ui.separator({ class: [props.ui?.separator] })" />
</thead> </thead>
<tbody :class="ui.tbody({ class: [props.ui?.tbody] })"> <tbody :class="ui.tbody({ class: [props.ui?.tbody] })">
<slot name="body-top" />
<template v-if="tableApi.getRowModel().rows?.length"> <template v-if="tableApi.getRowModel().rows?.length">
<template v-for="row in tableApi.getRowModel().rows" :key="row.id"> <template v-for="row in tableApi.getRowModel().rows" :key="row.id">
<tr <tr
:data-selected="row.getIsSelected()" :data-selected="row.getIsSelected()"
:data-selectable="!!props.onSelect" :data-selectable="!!props.onSelect || !!props.onHover || !!props.onContextmenu"
:data-expanded="row.getIsExpanded()" :data-expanded="row.getIsExpanded()"
:role="props.onSelect ? 'button' : undefined" :role="props.onSelect ? 'button' : undefined"
:tabindex="props.onSelect ? 0 : undefined" :tabindex="props.onSelect ? 0 : undefined"
@@ -383,7 +432,10 @@ defineExpose({
typeof tableApi.options.meta?.class?.tr === 'function' ? tableApi.options.meta.class.tr(row) : tableApi.options.meta?.class?.tr typeof tableApi.options.meta?.class?.tr === 'function' ? tableApi.options.meta.class.tr(row) : tableApi.options.meta?.class?.tr
] ]
})" })"
@click="handleRowSelect(row, $event)" @click="onRowSelect($event, row)"
@pointerenter="onRowHover($event, row)"
@pointerleave="onRowHover($event, null)"
@contextmenu="onRowContextmenu($event, row)"
> >
<td <td
v-for="cell in row.getVisibleCells()" v-for="cell in row.getVisibleCells()"
@@ -423,7 +475,33 @@ defineExpose({
</slot> </slot>
</td> </td>
</tr> </tr>
<slot name="body-bottom" />
</tbody> </tbody>
<tfoot v-if="hasFooter" :class="ui.tfoot({ class: [props.ui?.tfoot] })">
<tr :class="ui.separator({ class: [props.ui?.separator] })" />
<tr v-for="footerGroup in tableApi.getFooterGroups()" :key="footerGroup.id" :class="ui.tr({ class: [props.ui?.tr] })">
<th
v-for="header in footerGroup.headers"
:key="header.id"
:data-pinned="header.column.getIsPinned()"
:colspan="header.colSpan > 1 ? header.colSpan : undefined"
:class="ui.th({
class: [
props.ui?.th,
typeof header.column.columnDef.meta?.class?.th === 'function' ? header.column.columnDef.meta.class.th(header) : header.column.columnDef.meta?.class?.th
],
pinned: !!header.column.getIsPinned()
})"
>
<slot :name="`${header.id}-footer`" v-bind="header.getContext()">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.footer" :props="header.getContext()" />
</slot>
</th>
</tr>
</tfoot>
</table> </table>
</Primitive> </Primitive>
</template> </template>

View File

@@ -79,7 +79,8 @@ export type TabsSlots<T extends TabsItem = TabsItem> = {
</script> </script>
<script setup lang="ts" generic="T extends TabsItem"> <script setup lang="ts" generic="T extends TabsItem">
import { computed } from 'vue' import type { ComponentPublicInstance } from 'vue'
import { ref, computed } from 'vue'
import { TabsRoot, TabsList, TabsIndicator, TabsTrigger, TabsContent, useForwardPropsEmits } from 'reka-ui' import { TabsRoot, TabsList, TabsIndicator, TabsTrigger, TabsContent, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core' import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
@@ -108,6 +109,12 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.tabs || {})
size: props.size, size: props.size,
orientation: props.orientation orientation: props.orientation
})) }))
const triggersRef = ref<ComponentPublicInstance[]>([])
defineExpose({
triggersRef
})
</script> </script>
<template> <template>
@@ -117,7 +124,14 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.tabs || {})
<slot name="list-leading" /> <slot name="list-leading" />
<TabsTrigger v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :disabled="item.disabled" :class="ui.trigger({ class: [props.ui?.trigger, item.ui?.trigger] })"> <TabsTrigger
v-for="(item, index) of items"
:key="index"
:ref="el => (triggersRef[index] = el as ComponentPublicInstance)"
:value="item.value || String(index)"
:disabled="item.disabled"
:class="ui.trigger({ class: [props.ui?.trigger, item.ui?.trigger] })"
>
<slot name="leading" :item="item" :index="index"> <slot name="leading" :item="item" :index="index">
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: [props.ui?.leadingIcon, item.ui?.leadingIcon] })" /> <UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: [props.ui?.leadingIcon, item.ui?.leadingIcon] })" />
<UAvatar v-else-if="item.avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.leadingAvatar({ class: [props.ui?.leadingAvatar, item.ui?.leadingAvatar] })" /> <UAvatar v-else-if="item.avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.leadingAvatar({ class: [props.ui?.leadingAvatar, item.ui?.leadingAvatar] })" />

View File

@@ -9,7 +9,7 @@ type Textarea = ComponentConfig<typeof theme, AppConfig, 'textarea'>
type TextareaValue = string | number | null type TextareaValue = string | number | null
export interface TextareaProps extends UseComponentIconsProps { export interface TextareaProps<T extends TextareaValue = TextareaValue> extends UseComponentIconsProps {
/** /**
* The element or component this component should render as. * The element or component this component should render as.
* @defaultValue 'div' * @defaultValue 'div'
@@ -41,8 +41,11 @@ export interface TextareaProps extends UseComponentIconsProps {
maxrows?: number maxrows?: number
/** Highlight the ring color like a focus state. */ /** Highlight the ring color like a focus state. */
highlight?: boolean highlight?: boolean
modelValue?: T
defaultValue?: T
modelModifiers?: { modelModifiers?: {
string?: boolean string?: boolean
number?: boolean
trim?: boolean trim?: boolean
lazy?: boolean lazy?: boolean
nullify?: boolean nullify?: boolean
@@ -67,6 +70,7 @@ export interface TextareaSlots {
<script setup lang="ts" generic="T extends TextareaValue"> <script setup lang="ts" generic="T extends TextareaValue">
import { ref, computed, onMounted, nextTick, watch } from 'vue' import { ref, computed, onMounted, nextTick, watch } from 'vue'
import { Primitive } from 'reka-ui' import { Primitive } from 'reka-ui'
import { useVModel } from '@vueuse/core'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
import { useComponentIcons } from '../composables/useComponentIcons' import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField' import { useFormField } from '../composables/useFormField'
@@ -77,7 +81,7 @@ import UAvatar from './Avatar.vue'
defineOptions({ inheritAttrs: false }) defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<TextareaProps>(), { const props = withDefaults(defineProps<TextareaProps<T>>(), {
rows: 3, rows: 3,
maxrows: 0, maxrows: 0,
autofocusDelay: 0, autofocusDelay: 0,
@@ -86,12 +90,11 @@ const props = withDefaults(defineProps<TextareaProps>(), {
const emits = defineEmits<TextareaEmits<T>>() const emits = defineEmits<TextareaEmits<T>>()
const slots = defineSlots<TextareaSlots>() const slots = defineSlots<TextareaSlots>()
// eslint-disable-next-line vue/no-dupe-keys const modelValue = useVModel<TextareaProps<T>, 'modelValue', 'update:modelValue'>(props, 'modelValue', emits, { defaultValue: props.defaultValue })
const [modelValue, modelModifiers] = defineModel<T>()
const appConfig = useAppConfig() as Textarea['AppConfig'] const appConfig = useAppConfig() as Textarea['AppConfig']
const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps>(props, { deferInputValidation: true }) const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps<T>>(props, { deferInputValidation: true })
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.textarea || {}) })({ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.textarea || {}) })({
@@ -109,15 +112,15 @@ const textareaRef = ref<HTMLTextAreaElement | null>(null)
// Custom function to handle the v-model properties // Custom function to handle the v-model properties
function updateInput(value: string | null) { function updateInput(value: string | null) {
if (modelModifiers.trim) { if (props.modelModifiers?.trim) {
value = value?.trim() ?? null value = value?.trim() ?? null
} }
if (modelModifiers.number) { if (props.modelModifiers?.number) {
value = looseToNumber(value) value = looseToNumber(value)
} }
if (modelModifiers.nullify) { if (props.modelModifiers?.nullify) {
value ||= null value ||= null
} }
@@ -128,7 +131,7 @@ function updateInput(value: string | null) {
function onInput(event: Event) { function onInput(event: Event) {
autoResize() autoResize()
if (!modelModifiers.lazy) { if (!props.modelModifiers?.lazy) {
updateInput((event.target as HTMLInputElement).value) updateInput((event.target as HTMLInputElement).value)
} }
} }
@@ -136,12 +139,12 @@ function onInput(event: Event) {
function onChange(event: Event) { function onChange(event: Event) {
const value = (event.target as HTMLInputElement).value const value = (event.target as HTMLInputElement).value
if (modelModifiers.lazy) { if (props.modelModifiers?.lazy) {
updateInput(value) updateInput(value)
} }
// Update trimmed textarea so that it has same behavior as native textarea https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63 // Update trimmed textarea so that it has same behavior as native textarea https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
if (modelModifiers.trim) { if (props.modelModifiers?.trim) {
(event.target as HTMLInputElement).value = value.trim() (event.target as HTMLInputElement).value = value.trim()
} }

View File

@@ -41,6 +41,7 @@ export interface TimelineProps<T extends TimelineItem = TimelineItem> {
*/ */
orientation?: Timeline['variants']['orientation'] orientation?: Timeline['variants']['orientation']
defaultValue?: string | number defaultValue?: string | number
reverse?: boolean
class?: any class?: any
ui?: Timeline['slots'] ui?: Timeline['slots']
} }
@@ -75,16 +76,34 @@ const appConfig = useAppConfig() as Timeline['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.timeline || {}) })({ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.timeline || {}) })({
orientation: props.orientation, orientation: props.orientation,
size: props.size, size: props.size,
color: props.color color: props.color,
reverse: props.reverse
})) }))
const currentStepIndex = computed(() => { const currentStepIndex = computed(() => {
const value = modelValue.value ?? props.defaultValue const value = modelValue.value ?? props.defaultValue
return ((typeof value === 'string') if (typeof value === 'string') {
? props.items.findIndex(item => item.value === value) return props.items.findIndex(item => item.value === value) ?? -1
: value) ?? -1 }
if (props.reverse) {
return value != null ? props.items.length - 1 - value : -1
} else {
return value ?? -1
}
}) })
function getItemState(index: number): 'active' | 'completed' | undefined {
if (currentStepIndex.value === -1) return undefined
if (index === currentStepIndex.value) return 'active'
if (props.reverse) {
return index > currentStepIndex.value ? 'completed' : undefined
} else {
return index < currentStepIndex.value ? 'completed' : undefined
}
}
</script> </script>
<template> <template>
@@ -93,7 +112,7 @@ const currentStepIndex = computed(() => {
v-for="(item, index) in items" v-for="(item, index) in items"
:key="item.value ?? index" :key="item.value ?? index"
:class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class] })" :class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class] })"
:data-state="index < currentStepIndex ? 'completed' : index === currentStepIndex ? 'active' : undefined" :data-state="getItemState(index)"
> >
<div :class="ui.container({ class: [props.ui?.container, item.ui?.container] })"> <div :class="ui.container({ class: [props.ui?.container, item.ui?.container] })">
<UAvatar :size="size" :icon="item.icon" v-bind="typeof item.avatar === 'object' ? item.avatar : {}" :class="ui.indicator({ class: [props.ui?.indicator, item.ui?.indicator] })" :ui="{ icon: 'text-inherit', fallback: 'text-inherit' }"> <UAvatar :size="size" :icon="item.icon" v-bind="typeof item.avatar === 'object' ? item.avatar : {}" :class="ui.indicator({ class: [props.ui?.indicator, item.ui?.indicator] })" :ui="{ icon: 'text-inherit', fallback: 'text-inherit' }">

View File

@@ -173,7 +173,6 @@ defineExpose({
<UButton <UButton
v-if="close" v-if="close"
:icon="closeIcon || appConfig.ui.icons.close" :icon="closeIcon || appConfig.ui.icons.close"
size="md"
color="neutral" color="neutral"
variant="link" variant="link"
:aria-label="t('toast.close')" :aria-label="t('toast.close')"

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { TooltipRootProps, TooltipRootEmits, TooltipContentProps, TooltipContentEmits, TooltipArrowProps } from 'reka-ui' import type { TooltipRootProps, TooltipRootEmits, TooltipContentProps, TooltipContentEmits, TooltipArrowProps, TooltipTriggerProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema' import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/tooltip' import theme from '#build/ui/tooltip'
import type { KbdProps } from '../types' import type { KbdProps } from '../types'
@@ -27,6 +27,12 @@ export interface TooltipProps extends TooltipRootProps {
* @defaultValue true * @defaultValue true
*/ */
portal?: boolean | string | HTMLElement portal?: boolean | string | HTMLElement
/**
* The reference (or anchor) element that is being referred to for positioning.
*
* If not provided will use the current component as anchor.
*/
reference?: TooltipTriggerProps['reference']
class?: any class?: any
ui?: Tooltip['slots'] ui?: Tooltip['slots']
} }
@@ -70,7 +76,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.tooltip || {
<template> <template>
<TooltipRoot v-slot="{ open }" v-bind="rootProps"> <TooltipRoot v-slot="{ open }" v-bind="rootProps">
<TooltipTrigger v-if="!!slots.default" v-bind="$attrs" as-child :class="props.class"> <TooltipTrigger v-if="!!slots.default || !!reference" v-bind="$attrs" as-child :reference="reference" :class="props.class">
<slot :open="open" /> <slot :open="open" />
</TooltipTrigger> </TooltipTrigger>

View File

@@ -36,6 +36,8 @@ interface Shortcut {
const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/ const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/
const combinedShortcutRegex = /^[^_]+.*_.*[^_]+$/ const combinedShortcutRegex = /^[^_]+.*_.*[^_]+$/
// keyboard keys which can be combined with Shift modifier (in addition to alphabet keys)
const shiftableKeys = ['arrowleft', 'arrowright', 'arrowup', 'arrowright', 'tab', 'escape', 'enter', 'backspace']
export function extractShortcuts(items: any[] | any[][]) { export function extractShortcuts(items: any[] | any[][]) {
const shortcuts: Record<string, Handler> = {} const shortcuts: Record<string, Handler> = {}
@@ -76,7 +78,8 @@ export function defineShortcuts(config: MaybeRef<ShortcutsConfig>, options: Shor
return return
} }
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key) const alphabetKey = /^[a-z]{1}$/i.test(e.key)
const shiftableKey = shiftableKeys.includes(e.key.toLowerCase())
let chainedKey let chainedKey
chainedInputs.value.push(e.key) chainedInputs.value.push(e.key)
@@ -109,9 +112,9 @@ export function defineShortcuts(config: MaybeRef<ShortcutsConfig>, options: Shor
if (e.ctrlKey !== shortcut.ctrlKey) { if (e.ctrlKey !== shortcut.ctrlKey) {
continue continue
} }
// shift modifier is only checked in combination with alphabetical keys // shift modifier is only checked in combination with alphabet keys and some extra keys
// (shift with non-alphabetical keys would change the key) // (shift with special characters would change the key)
if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) { if ((alphabetKey || shiftableKey) && e.shiftKey !== shortcut.shiftKey) {
continue continue
} }
// alt modifier changes the combined key anyways // alt modifier changes the combined key anyways

View File

@@ -1,4 +1,5 @@
import { inject, provide, computed, type ComputedRef, type InjectionKey } from 'vue' import { inject, provide, computed } from 'vue'
import type { ComputedRef, InjectionKey } from 'vue'
import type { AvatarGroupProps } from '../types' import type { AvatarGroupProps } from '../types'
export const avatarGroupInjectionKey: InjectionKey<ComputedRef<{ size: AvatarGroupProps['size'] }>> = Symbol('nuxt-ui.avatar-group') export const avatarGroupInjectionKey: InjectionKey<ComputedRef<{ size: AvatarGroupProps['size'] }>> = Symbol('nuxt-ui.avatar-group')

View File

@@ -1,4 +1,5 @@
import { computed, toValue, type MaybeRefOrGetter } from 'vue' import { computed, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
import type { AvatarProps } from '../types' import type { AvatarProps } from '../types'

View File

@@ -1,5 +1,7 @@
import { inject, computed, type InjectionKey, type Ref, type ComputedRef, provide } from 'vue' import { inject, computed, provide } from 'vue'
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core' import type { InjectionKey, Ref, ComputedRef } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import type { UseEventBusReturn } from '@vueuse/core'
import type { FormFieldProps } from '../types' import type { FormFieldProps } from '../types'
import type { FormEvent, FormInputEvents, FormFieldInjectedOptions, FormInjectedOptions } from '../types/form' import type { FormEvent, FormInputEvents, FormFieldInjectedOptions, FormInjectedOptions } from '../types/form'
import type { GetObjectField } from '../types/utils' import type { GetObjectField } from '../types/utils'

View File

@@ -3,9 +3,34 @@ import { reactive, markRaw, shallowReactive } from 'vue'
import { createSharedComposable } from '@vueuse/core' import { createSharedComposable } from '@vueuse/core'
import type { ComponentProps, ComponentEmit } from 'vue-component-type-helpers' import type { ComponentProps, ComponentEmit } from 'vue-component-type-helpers'
// Extracts the first argument of the close event /**
type CloseEventArgType<T> = T extends (event: 'close', args_0: infer R) => void ? R : never * This is a workaround for a design limitation in TypeScript.
*
* Conditional types only match the last function overload, not a union of all possible
* parameter types. This workaround forces TypeScript to properly extract the 'close'
* event argument type from component emits with multiple event signatures.
*
* @see https://github.com/microsoft/TypeScript/issues/32164
*/
type CloseEventArgType<T> = T extends {
(event: 'close', arg_0: infer Arg, ...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
} ? Arg : never
export type OverlayOptions<OverlayAttrs = Record<string, any>> = { export type OverlayOptions<OverlayAttrs = Record<string, any>> = {
defaultOpen?: boolean defaultOpen?: boolean
props?: OverlayAttrs props?: OverlayAttrs

View File

@@ -1,4 +1,5 @@
import { inject, provide, computed, type Ref, type InjectionKey } from 'vue' import { inject, provide, computed } from 'vue'
import type { Ref, InjectionKey } from 'vue'
export const portalTargetInjectionKey: InjectionKey<Ref<string | HTMLElement>> = Symbol('nuxt-ui.portal-target') export const portalTargetInjectionKey: InjectionKey<Ref<string | HTMLElement>> = Symbol('nuxt-ui.portal-target')

View File

@@ -25,7 +25,8 @@ export default defineLocale<Messages>({
placeholder: 'اكتب أمرًا أو ابحث...', placeholder: 'اكتب أمرًا أو ابحث...',
noMatch: 'لا توجد نتائج مطابقة', noMatch: 'لا توجد نتائج مطابقة',
noData: 'لا توجد بيانات', noData: 'لا توجد بيانات',
close: 'إغلاق' close: 'إغلاق',
back: 'رجوع'
}, },
selectMenu: { selectMenu: {
noMatch: 'لا توجد نتائج مطابقة', noMatch: 'لا توجد نتائج مطابقة',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Əmr daxil edin və ya axtarın...', placeholder: 'Əmr daxil edin və ya axtarın...',
noMatch: 'Uyğun məlumat tapılmadı', noMatch: 'Uyğun məlumat tapılmadı',
noData: 'Məlumat yoxdur', noData: 'Məlumat yoxdur',
close: 'Bağla' close: 'Bağla',
back: 'Geri'
}, },
selectMenu: { selectMenu: {
noMatch: 'Uyğun məlumat tapılmadı', noMatch: 'Uyğun məlumat tapılmadı',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Въведете команда или потърсете...', placeholder: 'Въведете команда или потърсете...',
noMatch: 'Няма съвпадение на данни', noMatch: 'Няма съвпадение на данни',
noData: 'Няма данни', noData: 'Няма данни',
close: 'Затворете' close: 'Затворете',
back: 'Назад'
}, },
selectMenu: { selectMenu: {
noMatch: 'Няма съвпадение на данни', noMatch: 'Няма съвпадение на данни',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'কমান্ড টাইপ করুন বা অনুসন্ধান করুন...', placeholder: 'কমান্ড টাইপ করুন বা অনুসন্ধান করুন...',
noMatch: 'কোন মিল পাওয়া যায়নি', noMatch: 'কোন মিল পাওয়া যায়নি',
noData: 'কোন তথ্য নেই', noData: 'কোন তথ্য নেই',
close: 'বন্ধ করুন' close: 'বন্ধ করুন',
back: 'পেছনে'
}, },
selectMenu: { selectMenu: {
noMatch: 'কোন মিল পাওয়া যায়নি', noMatch: 'কোন মিল পাওয়া যায়নি',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Escriu una ordre o cerca...', placeholder: 'Escriu una ordre o cerca...',
noMatch: 'No hi ha dades coincidents', noMatch: 'No hi ha dades coincidents',
noData: 'Sense dades', noData: 'Sense dades',
close: 'Tancar' close: 'Tancar',
back: 'Enrere'
}, },
selectMenu: { selectMenu: {
noMatch: 'No hi ha dades coincidents', noMatch: 'No hi ha dades coincidents',

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