Compare commits

...

91 Commits

Author SHA1 Message Date
Benjamin Canac
df32b3131b chore(release): 2.14.0 2024-02-22 12:06:52 +01:00
Benjamin Canac
d96d17d7e6 chore(readme): update 2024-02-22 11:42:06 +01:00
Benjamin Canac
b6c69441f5 docs(index): invalid component links 2024-02-21 23:11:59 +01:00
Benjamin Canac
33f3372c6b docs(nuxt.config): highlight mdc 2024-02-21 23:11:47 +01:00
renovate[bot]
613ba2db64 chore(deps): update devdependency @nuxt/ui-pro to v0.7.5-28475621.09eb8fa (#1394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-21 20:02:36 +01:00
Benjamin Canac
9f352976ce fix(utils): prevent merge of popper key
Resolves #1393
2024-02-21 16:51:42 +01:00
Benjamin Canac
f83cff7095 chore(utils): prevent default prop merge for chip and badge 2024-02-21 16:34:34 +01:00
Benjamin Canac
433c09a9f3 docs(nuxt.config): highlight postcss lang 2024-02-21 16:34:34 +01:00
renovate[bot]
930337bf88 chore(deps): update all non-major dependencies (#1381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-21 14:21:15 +01:00
Benjamin Canac
81e48ba9fd docs(home): add video 2024-02-21 12:24:30 +01:00
Benjamin Canac
cb2fd1e940 docs: consistent app.vue and error.vue 2024-02-20 10:47:50 +01:00
Benjamin Canac
6d4eac0dec docs: update figma link 2024-02-20 10:43:56 +01:00
Benjamin Canac
f4f6a8fcc1 chore(deps): refresh lock 2024-02-19 16:41:22 +01:00
renovate[bot]
920070cce0 chore(deps): update all non-major dependencies (#1347)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-19 16:23:10 +01:00
Benjamin Canac
d12b00c005 chore(useFormGroup): indentation 2024-02-19 12:23:25 +01:00
Benjamin Canac
3a142896c3 chore(useButtonGroup): indentation 2024-02-19 12:22:50 +01:00
Benjamin Canac
58682cec0c docs(theming): broken link for ui.config file
Resolves #1372
2024-02-18 20:01:21 +01:00
Romain Hamel
37ef7a4e4f docs(form): improve form documentation (#1373) 2024-02-18 18:30:20 +01:00
Amir Reza Dalir
5266591c88 fix(Form): improve validate path type (#1370)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-02-18 11:15:15 +01:00
Romain Hamel
d4b6147fcc fix(Form): return false when silent validation fails (#1371) 2024-02-18 11:11:07 +01:00
Benjamin Canac
31232d4d72 chore(Table): use px-4 in td and th for consistency 2024-02-15 15:29:52 +01:00
Benjamin Canac
3fe35217cb feat(Table): display progress bar when loading (#1362) 2024-02-15 12:37:44 +01:00
renovate[bot]
04ef47376d chore(deps): update dorny/paths-filter action to v3 (#1360)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-15 12:32:02 +01:00
Benjamin Canac
aa2b1cae88 fix(Notification): remove required title to prevent warning when using slot 2024-02-15 12:22:50 +01:00
Benjamin Canac
e545b6f0a1 fix(Alert): remove required title to prevent warning when using slot 2024-02-15 12:22:33 +01:00
Benjamin Canac
db42d9cab7 chore(Progress): define ProgressColor type 2024-02-15 12:14:00 +01:00
Benjamin Canac
b11c773f32 chore(Range): export RangeColor type 2024-02-15 12:14:00 +01:00
Benjamin Canac
c34df13e65 chore(Toggle): export ToggleColor type 2024-02-15 12:14:00 +01:00
Benjamin Canac
a55a08a91e fix(Progress): prevent NaN percent display when indeterminate 2024-02-15 12:13:37 +01:00
Benjamin Canac
c488b28c3c docs: use lang="ts" everywhere 2024-02-14 17:41:10 +01:00
Benjamin Canac
300861a49e docs(deps): remove @nuxthq/studio 2024-02-14 14:47:49 +01:00
Benjamin Canac
09a8e2d8c2 chore(deps): refresh lock 2024-02-12 12:00:48 +01:00
Benjamin Canac
7eba5b539a chore(FormGroup): wrap label & description to ease styling 2024-02-12 11:59:18 +01:00
renovate[bot]
19d15b42f0 chore(deps): update all non-major dependencies (#1334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-12 11:37:32 +01:00
Richard van Driest
e23d4aaf53 docs(table): correct spelling (#1340) 2024-02-09 11:40:02 +01:00
Benjamin Canac
e1fb8e438d chore(Modals): ensure modalState exists 2024-02-08 17:18:48 +01:00
Benjamin Canac
f682905b26 fix(Card): prevent body padding without default slot 2024-02-08 12:49:38 +01:00
Benjamin Canac
f5fa9fe163 docs: add New badges on edge 2024-02-07 21:48:21 +01:00
Benjamin Canac
627a44bb1f docs: remove New badges on edge 2024-02-07 21:42:33 +01:00
Benjamin Canac
ade99a8f05 chore(Modals): client only component 2024-02-07 21:36:51 +01:00
Benjamin Canac
3295954247 docs(table): prevent overflow on mobile 2024-02-07 21:36:32 +01:00
Benjamin Canac
4f532dbb72 docs(vertical-navigation): improve example 2024-02-07 21:36:18 +01:00
Benjamin Canac
ee0a8f01af docs(carousel): improve examples 2024-02-07 21:31:48 +01:00
Benjamin Canac
b8936070f9 Revert "docs: add missing overflow-hidden on components"
This reverts commit 34adcc1c04.
2024-02-07 21:04:56 +01:00
Neil Richter
6f29c620ab feat(Modal): open programmatically (#1319) 2024-02-07 16:53:17 +01:00
renovate[bot]
98a2d0f1af chore(deps): update all non-major dependencies (#1216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-02-07 16:03:42 +01:00
Benjamin Canac
e08601900e docs(DatePicker): wrong version of v-calendar
Fixes #1333
2024-02-07 14:40:21 +01:00
Benjamin Canac
cf818fba47 docs(home): improve pro animation start time 2024-02-07 14:31:57 +01:00
Benjamin Canac
0c8a649035 docs: add postcss shiki lang highlighter 2024-02-07 14:12:54 +01:00
Benjamin Canac
843a978644 feat(Tabs): add unmount prop as false by default
Resolves #663
2024-02-07 14:12:54 +01:00
adjabaev
cbeede66bb feat(Divider): handle size prop (#1307)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-02-07 13:48:11 +01:00
Benjamin Canac
a506cbbcb0 chore(package.json): add missing module type 2024-02-07 12:44:35 +01:00
Benjamin Canac
bb40c31031 fix(module): prevent tailwind warn with bun
Fixes #809
2024-02-07 12:43:56 +01:00
Benjamin Canac
34adcc1c04 docs: add missing overflow-hidden on components 2024-02-06 23:09:27 +01:00
Olusola Olawale
ac42ec106f fix(Link): check disabled prop before navigating (#1321) 2024-02-06 21:45:51 +01:00
Benjamin Canac
c3ed940ac2 docs(deps): bump @nuxt/ui-pro-edge 2024-02-06 19:51:38 +01:00
Benjamin Canac
7c74c2f22a docs: improve config display 2024-02-06 19:51:21 +01:00
Benjamin Canac
d0f4530e85 fix(SelectMenu): revert component is after #1199 2024-02-06 18:15:27 +01:00
Benjamin Canac
f8b296fc60 fix(Meter): missing import of Icon component
Fixes #1328
2024-02-06 18:03:16 +01:00
Benjamin Canac
882247e5f4 fix(Accordion): style disclosure div after #1199 2024-02-06 17:52:58 +01:00
Farnabaz
a297c3b41e docs: move shiki highlighter to composable (#1325) 2024-02-06 16:12:27 +01:00
Benjamin Canac
45121916d0 docs(releases): use head.title
Fixes #1323
2024-02-06 15:38:18 +01:00
Benjamin Canac
6b82429e30 chore(deps): bump 2024-02-06 14:21:11 +01:00
Farnabaz
707753a743 docs(deps): update @nuxt/content (#1310)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-02-06 13:06:20 +01:00
Benjamin Canac
10db14475f fix(components): hydration attribute mismatch with vue 3.4 (#1199) 2024-02-06 12:42:19 +01:00
Inesh Bose
4a5f7b06cf chore(tailwind): put empty object in quotes (#1306) 2024-02-05 16:46:31 +01:00
pierre golfier
f643e7b316 feat(Textarea): add maxrows prop to restrict autoresize (#1302)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-02-05 16:18:20 +01:00
Benjamin Canac
5a5b284e96 fix(RadioGroup): pass help prop to radio children
Resolves #1313
2024-02-05 15:32:36 +01:00
Benjamin Canac
6699a0519d docs(deps): bump @nuxt/ui-pro-edge 2024-02-05 12:24:54 +01:00
Benjamin Canac
8b08edeee7 chore(module): pass resolve and runtimeDir to installTailwind 2024-02-05 12:16:58 +01:00
Benjamin Canac
41ecd2a3d5 feat(Carousel): expose methods to allow autoplay
Resolves #1300
2024-02-01 18:07:39 +01:00
Benjamin Canac
f36158133e docs: bump @nuxt/ui-pro-edge 2024-02-01 16:55:02 +01:00
Inesh Bose
f0ee1893ee refactor(module): provide tailwind config through template (#1272)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-02-01 15:06:28 +01:00
Benjamin Canac
f455dbdd22 docs(releases): lint 2024-02-01 12:57:15 +01:00
Romain Hamel
27c71fa40e feat(Form): use nuxt useId to bind input labels (#1211) 2024-01-31 18:22:02 +01:00
Benjamin Canac
be37daec56 chore: ignore ts errors after nuxt 3.10 2024-01-31 15:05:31 +01:00
Benjamin Canac
9676f51512 chore(deps): update nuxt resolutions 2024-01-31 14:38:22 +01:00
Benjamin Canac
f8ada8042a docs(vercel.json): ignore _payload.json in redirects 2024-01-31 13:52:00 +01:00
Jake
89e15b90b1 docs(carousel): add draggable="false" to image elements (#1297) 2024-01-31 13:04:19 +01:00
Benjamin Canac
5b008b789b docs: put back prerender: false 2024-01-31 13:02:12 +01:00
Benjamin Canac
25d35cf465 docs: display Edge badge in header 2024-01-31 12:08:55 +01:00
Benjamin Canac
ee662986ab docs: remove prerender: false 2024-01-31 11:52:04 +01:00
Benjamin Canac
946a39c739 feat(Input): handle type file
Resolves #563
2024-01-31 11:50:34 +01:00
Benjamin Canac
412cd75edd fix(module): put back all option in icons plugin
Fixes #1237
2024-01-31 10:53:39 +01:00
Benjamin Canac
d0471f66ea chore(deps): update @egoist/tailwindcss-icons 2024-01-31 10:52:14 +01:00
Benjamin Canac
a12f37e4d2 chore(deps): update 2024-01-30 17:32:34 +01:00
Benjamin Canac
b741b42c64 docs: active pro link on /pro/prose 2024-01-30 15:07:44 +01:00
Benjamin Canac
7f8c625b0e docs: add /getting-started/examples redirect 2024-01-30 14:44:00 +01:00
Benjamin Canac
83b6b04eea docs: configure image provider to ipx 2024-01-30 14:26:20 +01:00
Benjamin Canac
aac6fb4334 docs(releases): improve page 2024-01-30 13:14:16 +01:00
Benjamin Canac
ca9f47d7c0 docs: move vercel.json in root dir 2024-01-30 12:37:53 +01:00
112 changed files with 3330 additions and 3241 deletions

View File

@@ -53,7 +53,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: dorny/paths-filter@v2
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |

View File

@@ -1,5 +1,43 @@
# Changelog
## [2.14.0](https://github.com/nuxt/ui/compare/v2.13.0...v2.14.0) (2024-02-22)
### Features
* **Carousel:** expose methods to allow autoplay ([41ecd2a](https://github.com/nuxt/ui/commit/41ecd2a3d553886db3e32d9f48a477268d93f3c6)), closes [#1300](https://github.com/nuxt/ui/issues/1300)
* **Divider:** handle `size` prop ([#1307](https://github.com/nuxt/ui/issues/1307)) ([cbeede6](https://github.com/nuxt/ui/commit/cbeede66bb3bd7778e03c19ebbf55bf7bd753cb8))
* **Form:** use nuxt `useId` to bind input labels ([#1211](https://github.com/nuxt/ui/issues/1211)) ([27c71fa](https://github.com/nuxt/ui/commit/27c71fa40ecb9f8524fee7f3d17a384bc8812d25))
* **Input:** handle type `file` ([946a39c](https://github.com/nuxt/ui/commit/946a39c73990dc352cf7b9a77bfaec339cdcab34)), closes [#563](https://github.com/nuxt/ui/issues/563)
* **Modal:** open programmatically ([#1319](https://github.com/nuxt/ui/issues/1319)) ([6f29c62](https://github.com/nuxt/ui/commit/6f29c620ab758e27be63f8af53674828b59fb6ed))
* **Table:** display progress bar when `loading` ([#1362](https://github.com/nuxt/ui/issues/1362)) ([3fe3521](https://github.com/nuxt/ui/commit/3fe35217cbc0cef7f41550c175e4e7ea2cc939a8))
* **Tabs:** add `unmount` prop as `false` by default ([843a978](https://github.com/nuxt/ui/commit/843a9786445f6170c1380e3b404151da52b5a154)), closes [#663](https://github.com/nuxt/ui/issues/663)
* **Textarea:** add `maxrows` prop to restrict autoresize ([#1302](https://github.com/nuxt/ui/issues/1302)) ([f643e7b](https://github.com/nuxt/ui/commit/f643e7b316639a79cf03da25250ab0fa85f466d5))
### Bug Fixes
* **Accordion:** style disclosure `div` after [#1199](https://github.com/nuxt/ui/issues/1199) ([882247e](https://github.com/nuxt/ui/commit/882247e5f40bf41fdfdffea501de5c898a7fb0b2))
* **Alert:** remove `required` title to prevent warning when using slot ([e545b6f](https://github.com/nuxt/ui/commit/e545b6f0a128475166dcea3c1028798b106805f3))
* **Card:** prevent `body` padding without default slot ([f682905](https://github.com/nuxt/ui/commit/f682905b26a22546634e9adc4b838a7741dbd7c9))
* **components:** hydration attribute mismatch with vue `3.4` ([#1199](https://github.com/nuxt/ui/issues/1199)) ([10db144](https://github.com/nuxt/ui/commit/10db14475f7a527180be3fcf33cc5d3af52452c9))
* **Form:** improve `validate` path type ([#1370](https://github.com/nuxt/ui/issues/1370)) ([5266591](https://github.com/nuxt/ui/commit/5266591c886422d5265e46e08e1276913d12bed1))
* **Form:** return false when silent validation fails ([#1371](https://github.com/nuxt/ui/issues/1371)) ([d4b6147](https://github.com/nuxt/ui/commit/d4b6147fcceb7ff9cebe1586bb3094b10f50acb5))
* **Link:** check `disabled` prop before navigating ([#1321](https://github.com/nuxt/ui/issues/1321)) ([ac42ec1](https://github.com/nuxt/ui/commit/ac42ec106ff259e1d44515e5fb3b5236559ac713))
* **Meter:** missing import of `Icon` component ([f8b296f](https://github.com/nuxt/ui/commit/f8b296fc60b93c4656fd397f8eb6b06b4a1dcd93)), closes [#1328](https://github.com/nuxt/ui/issues/1328)
* **module:** prevent tailwind warn with `bun` ([bb40c31](https://github.com/nuxt/ui/commit/bb40c3103174a039f65b31c65fcc5d40cb29ce6b)), closes [#809](https://github.com/nuxt/ui/issues/809)
* **module:** put back `all` option in icons plugin ([412cd75](https://github.com/nuxt/ui/commit/412cd75eddb6140d7d9b3358b04df1e61f22b481)), closes [#1237](https://github.com/nuxt/ui/issues/1237)
* **Notification:** remove `required` title to prevent warning when using slot ([aa2b1ca](https://github.com/nuxt/ui/commit/aa2b1cae8881dece9a629dc95a8f9df88f9bbd27))
* **Progress:** prevent `NaN` percent display when indeterminate ([a55a08a](https://github.com/nuxt/ui/commit/a55a08a91eca6f4c7ff3ad40ee566b6445d2dfd0))
* **RadioGroup:** pass `help` prop to radio children ([5a5b284](https://github.com/nuxt/ui/commit/5a5b284e967ca9cdb6c7df9809ed4f4569a65cfa)), closes [#1313](https://github.com/nuxt/ui/issues/1313)
* **SelectMenu:** revert component `is` after [#1199](https://github.com/nuxt/ui/issues/1199) ([d0f4530](https://github.com/nuxt/ui/commit/d0f4530e8572a08d544041dec1f24a51bbc3b1e8))
* **utils:** prevent merge of `popper` key ([9f35297](https://github.com/nuxt/ui/commit/9f352976ced5845a5fad00a6630d0166941a8a13)), closes [#1393](https://github.com/nuxt/ui/issues/1393)
### Reverts
* Revert "docs: add missing `overflow-hidden` on components" ([b893607](https://github.com/nuxt/ui/commit/b8936070f9e1f866a21d39f6c45140f86efebec4))
## [2.13.0](https://github.com/nuxt/ui/compare/v2.12.3...v2.13.0) (2024-01-30)

View File

@@ -1,4 +1,4 @@
[![nuxt-ui-social-card](https://repository-images.githubusercontent.com/428329515/43fec891-9030-4601-8233-5d45ba5c6013)](https://ui.nuxt.com)
[![nuxt-ui.png](https://repository-images.githubusercontent.com/428329515/43fec891-9030-4601-8233-5d45ba5c6013)](https://ui.nuxt.com)
# Nuxt UI
@@ -7,9 +7,9 @@
[![License][license-src]][license-href]
[![Nuxt][nuxt-src]][nuxt-href]
Nuxt UI provides everything related to UI when building Nuxt applications: components, icons, colors, dark mode and also keyboard shortcuts.
Nuxt UI is a module that provides a set of Vue components and composables built with [Tailwind CSS](https://tailwindcss.com/) and [Headless UI](https://headlessui.dev/) to help you build beautiful and accessible user interfaces.
Is has been developed by [NuxtLabs](https://nuxtlabs.com/) for [Volta](https://volta.net), [Nuxt Studio](https://nuxt.studio/) and the Nuxt community.
Its goal is to provide everything related to UI when building a Nuxt app. This includes components, icons, colors, dark mode but also keyboard shortcuts.
## Features
@@ -27,14 +27,14 @@ Read more on [ui.nuxt.com](https://ui.nuxt.com)
## Installation
```bash
# Using npm
# npm
npm install @nuxt/ui
# Using yarn
# yarn
yarn add @nuxt/ui
# Using pnpm
# pnpm
pnpm add @nuxt/ui
# bun
bun add @nuxt/ui
```
Then, register the module in your `nuxt.config.ts`:

View File

@@ -20,6 +20,7 @@
<span v-html="title" />
</template>
</UNotifications>
<UModals />
</div>
</template>
@@ -50,7 +51,7 @@ const navigation = computed(() => {
]
}
return nav.value.filter(item => item._path !== '/dev')
return nav.value?.filter(item => item._path !== '/dev') || []
})
const color = computed(() => colorMode.value === 'dark' ? '#18181b' : 'white')
@@ -65,7 +66,7 @@ const links = computed(() => {
label: 'Pro',
icon: 'i-heroicons-square-3-stack-3d',
to: '/pro',
active: route.path.startsWith('/pro/getting-started') || route.path.startsWith('/pro/components')
active: route.path.startsWith('/pro/getting-started') || route.path.startsWith('/pro/components') || route.path.startsWith('/pro/prose')
}, {
label: 'Pricing',
icon: 'i-heroicons-credit-card',

View File

@@ -28,7 +28,7 @@
const links = [{
icon: 'i-simple-icons-figma',
label: 'Figma Kit',
to: 'https://www.figma.com/community/file/1288455405058138934/nuxt-ui',
to: 'https://www.figma.com/community/file/1288455405058138934',
target: '_blank'
}, {
label: 'Playground',

View File

@@ -11,6 +11,7 @@
<Logo class="w-auto h-6" />
<UBadge v-if="$route.path.startsWith('/pro')" label="Pro" variant="subtle" size="xs" class="-mb-[2px] rounded font-semibold" />
<UBadge v-if="$route.path.startsWith('/dev')" label="Edge" variant="subtle" size="xs" class="-mb-[2px] rounded font-semibold" />
</NuxtLink>
</template>

View File

@@ -51,11 +51,9 @@
</template>
<script setup lang="ts">
// @ts-expect-error
import { transformContent } from '@nuxt/content/transformers'
// @ts-ignore
import { useShikiHighlighter } from '@nuxtjs/mdc/runtime'
import { upperFirst, camelCase, kebabCase } from 'scule'
import { useShikiHighlighter } from '~/composables/useShikiHighlighter'
// eslint-disable-next-line vue/no-dupe-keys
const props = defineProps({
@@ -124,6 +122,7 @@ const componentProps = reactive({ ...props.props })
const { $prettier } = useNuxtApp()
const appConfig = useAppConfig()
const route = useRoute()
const highlighter = useShikiHighlighter()
let name = props.slug || `U${upperFirst(camelCase(route.params.slug[route.params.slug.length - 1]))}`
@@ -269,8 +268,6 @@ function renderObject (obj: any) {
return obj
}
const shikiHighlighter = useShikiHighlighter({})
const codeHighlighter = async (code: string, lang: string, theme: any, highlights: number[]) => shikiHighlighter.getHighlightedAST(code, lang, theme, { highlights })
const { data: ast } = await useAsyncData(
`${name}-ast-${JSON.stringify({ props: componentProps, slots: props.slots, code: props.code })}`,
async () => {
@@ -284,7 +281,7 @@ const { data: ast } = await useAsyncData(
return transformContent('content:_markdown.md', formatted, {
markdown: {
highlight: {
highlighter: codeHighlighter,
highlighter,
theme: {
light: 'material-theme-lighter',
default: 'material-theme',

View File

@@ -21,10 +21,8 @@
<script setup lang="ts">
import { camelCase } from 'scule'
import { fetchContentExampleCode } from '~/composables/useContentExamplesCode'
// @ts-expect-error
import { transformContent } from '@nuxt/content/transformers'
// @ts-ignore
import { useShikiHighlighter } from '@nuxtjs/mdc/runtime'
import { useShikiHighlighter } from '~/composables/useShikiHighlighter'
const props = defineProps({
component: {
@@ -78,15 +76,14 @@ if (['command-palette-theme-algolia', 'command-palette-theme-raycast', 'vertical
const instance = getCurrentInstance()
const camelName = camelCase(component)
const data = await fetchContentExampleCode(camelName)
const highlighter = useShikiHighlighter()
const hasCode = computed(() => !props.hiddenCode && (data?.code || instance.slots.code))
const shikiHighlighter = useShikiHighlighter({})
const codeHighlighter = async (code: string, lang: string, theme: any, highlights: number[]) => shikiHighlighter.getHighlightedAST(code, lang, theme, { highlights })
const { data: ast } = await useAsyncData(`content-example-${camelName}-ast`, () => transformContent('content:_markdown.md', `\`\`\`vue\n${data?.code ?? ''}\n\`\`\``, {
markdown: {
highlight: {
highlighter: codeHighlighter,
highlighter,
theme: {
light: 'material-theme-lighter',
default: 'material-theme',

View File

@@ -3,9 +3,10 @@
</template>
<script setup lang="ts">
// @ts-expect-error
import { transformContent } from '@nuxt/content/transformers'
import { upperFirst, camelCase } from 'scule'
import json5 from 'json5'
import { useShikiHighlighter } from '~/composables/useShikiHighlighter'
import * as config from '#ui/ui.config'
const props = defineProps({
@@ -16,6 +17,7 @@ const props = defineProps({
})
const route = useRoute()
const highlighter = useShikiHighlighter()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const camelName = camelCase(slug)
@@ -24,15 +26,18 @@ const name = `U${upperFirst(camelName)}`
const preset = config[camelName]
const { data: ast } = await useAsyncData(`${name}-preset`, () => transformContent('content:_markdown.md', `
\`\`\`json
${JSON.stringify(preset, null, 2)}
\`\`\`yml
${json5.stringify(preset, null, 2)}
\`\`\`\
`, {
highlight: {
theme: {
light: 'material-theme-lighter',
default: 'material-theme',
dark: 'material-theme-palenight'
markdown: {
highlight: {
highlighter,
theme: {
light: 'material-theme-lighter',
default: 'material-theme',
dark: 'material-theme-palenight'
}
}
}
}))

View File

@@ -11,6 +11,6 @@ const items = [
<template>
<UCarousel v-slot="{ item }" :items="items">
<img :src="item" width="300" height="400">
<img :src="item" width="300" height="400" draggable="false">
</UCarousel>
</template>

View File

@@ -11,6 +11,6 @@ const items = [
<template>
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full' }" class="rounded-lg overflow-hidden" arrows>
<img :src="item" class="w-full">
<img :src="item" class="w-full" draggable="false">
</UCarousel>
</template>

View File

@@ -30,6 +30,6 @@ const items = [
arrows
class="w-64 mx-auto"
>
<img :src="item" class="w-full">
<img :src="item" class="w-full" draggable="false">
</UCarousel>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
const items = [
'https://picsum.photos/1920/1080?random=1',
'https://picsum.photos/1920/1080?random=2',
'https://picsum.photos/1920/1080?random=3',
'https://picsum.photos/1920/1080?random=4',
'https://picsum.photos/1920/1080?random=5',
'https://picsum.photos/1920/1080?random=6'
]
const carouselRef = ref()
onMounted(() => {
setInterval(() => {
if (!carouselRef.value) return
if (carouselRef.value.page === carouselRef.value.pages) {
return carouselRef.value.select(0)
}
carouselRef.value.next()
}, 3000)
})
</script>
<template>
<UCarousel
ref="carouselRef"
v-slot="{ item }"
:items="items"
:ui="{ item: 'basis-full' }"
class="rounded-lg overflow-hidden"
indicators
>
<img :src="item" class="w-full" draggable="false">
</UCarousel>
</template>

View File

@@ -11,6 +11,6 @@ const items = [
<template>
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full' }" class="rounded-lg overflow-hidden" indicators>
<img :src="item" class="w-full">
<img :src="item" class="w-full" draggable="false">
</UCarousel>
</template>

View File

@@ -11,6 +11,6 @@ const items = [
<template>
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full md:basis-1/2 lg:basis-1/3' }" indicators class="rounded-lg overflow-hidden">
<img :src="item" class="w-full">
<img :src="item" class="w-full" draggable="false">
</UCarousel>
</template>

View File

@@ -11,6 +11,6 @@ const items = [
<template>
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full md:basis-1/2 lg:basis-1/3' }">
<img :src="item" class="w-full">
<img :src="item" class="w-full" draggable="false">
</UCarousel>
</template>

View File

@@ -11,6 +11,6 @@ const items = [
<template>
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full' }" class="w-64 mx-auto rounded-lg overflow-hidden">
<img :src="item" class="w-full">
<img :src="item" class="w-full" draggable="false">
</UCarousel>
</template>

View File

@@ -11,6 +11,6 @@ const items = [
<template>
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'basis-full' }" class="rounded-lg overflow-hidden">
<img :src="item" class="w-full">
<img :src="item" class="w-full" draggable="false">
</UCarousel>
</template>

View File

@@ -19,15 +19,13 @@ const items = [{
</script>
<template>
<UCarousel :items="items" :ui="{ item: 'w-full' }">
<template #default="{ item, index }">
<div class="text-center mx-auto">
<img :src="item.avatar.src" :alt="item.name" class="rounded-full w-48 h-48 mb-2">
<UCarousel v-slot="{ item, index }" :items="items" :ui="{ item: 'w-full' }">
<div class="text-center mx-auto">
<img :src="item.avatar.src" :alt="item.name" class="rounded-full w-48 h-48 mb-2" draggable="false">
<p class="font-semibold">
{{ index + 1 }}. {{ item.name }}
</p>
</div>
</template>
<p class="font-semibold">
{{ index + 1 }}. {{ item.name }}
</p>
</div>
</UCarousel>
</template>

View File

@@ -23,11 +23,11 @@ const items = [
class="w-64 mx-auto"
>
<template #default="{ item }">
<img :src="item" class="w-full">
<img :src="item" class="w-full" draggable="false">
</template>
<template #indicator="{ onClick, index, active }">
<UButton :label="String(index)" :variant="active ? 'solid' : 'outline'" size="2xs" class="rounded-full min-w-6 justify-center" @click="onClick(index)" />
<template #indicator="{ onClick, page, active }">
<UButton :label="String(page)" :variant="active ? 'solid' : 'outline'" size="2xs" class="rounded-full min-w-6 justify-center" @click="onClick(page)" />
</template>
</UCarousel>
</template>

View File

@@ -20,7 +20,7 @@ const items = [
class="w-64 mx-auto"
>
<template #default="{ item }">
<img :src="item" class="w-full">
<img :src="item" class="w-full" draggable="false">
</template>
<template #prev="{ onClick, disabled }">
@@ -30,7 +30,7 @@ const items = [
</template>
<template #next="{ onClick, disabled }">
<button :disabled="disabled" class="" @click="onClick">
<button :disabled="disabled" @click="onClick">
Next
</button>
</template>

View File

@@ -11,6 +11,6 @@ const items = [
<template>
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'snap-end' }">
<img :src="item" width="200" height="300">
<img :src="item" width="200" height="300" draggable="false">
</UCarousel>
</template>

View File

@@ -11,6 +11,6 @@ const items = [
<template>
<UCarousel v-slot="{ item }" :items="items" :ui="{ item: 'snap-start' }">
<img :src="item" width="200" height="300">
<img :src="item" width="200" height="300" draggable="false">
</UCarousel>
</template>

View File

@@ -10,8 +10,8 @@ const schema = objectAsync({
type Schema = Input<typeof schema>
const state = reactive({
email: undefined,
password: undefined
email: '',
password: ''
})
async function onSubmit (event: FormSubmitEvent<Schema>) {

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
const route = useRoute()
const links = [{

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
const route = useRoute()
const links = [{

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
const route = useRoute()
const links = [

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
defineProps({
count: {
type: Number,
default: 0
}
})
</script>
<template>
<UModal>
<UCard>
<p>This modal was opened programmatically !</p>
<p>Count: {{ count }}</p>
</UCard>
</UModal>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { ModalExampleComponent } from '#components'
const modal = useModal()
const count = ref(0)
function openModal () {
count.value += 1
modal.open(ModalExampleComponent, {
count: count.value
})
}
</script>
<template>
<UButton label="Reveal modal" @click="openModal" />
</template>

View File

@@ -19,7 +19,7 @@ const links = [{
:links="links"
:ui="{
wrapper: 'border-s border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-s -ms-px lg:leading-6 before:hidden',
base: 'group block border-s -ms-px leading-6 before:hidden',
padding: 'p-0 ps-4',
rounded: '',
font: '',

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { DatePicker as VCalendarDatePicker } from 'v-calendar'
// @ts-ignore
import type { DatePickerDate, DatePickerRangeObject } from 'v-calendar/dist/types/src/use/datePicker'
import 'v-calendar/dist/style.css'

View File

@@ -26,7 +26,7 @@
</Transition>
</template>
<script setup>
<script setup lang="ts">
import { useElementSize } from '@vueuse/core'
const el = ref(null)

View File

@@ -0,0 +1,31 @@
import { createShikiHighlighter } from '@nuxtjs/mdc/runtime/highlighter/shiki'
import MaterialTheme from 'shiki/themes/material-theme.mjs'
import MaterialThemeLighter from 'shiki/themes/material-theme-lighter.mjs'
import MaterialThemePalenight from 'shiki/themes/material-theme-palenight.mjs'
import HtmlLang from 'shiki/langs/html.mjs'
import MdcLang from 'shiki/langs/mdc.mjs'
import VueLang from 'shiki/langs/vue.mjs'
import YamlLang from 'shiki/langs/yaml.mjs'
import PostcssLang from 'shiki/langs/postcss.mjs'
let highlighter
export const useShikiHighlighter = () => {
if (!highlighter) {
highlighter = createShikiHighlighter({
bundledThemes: {
'material-theme': MaterialTheme,
'material-theme-lighter': MaterialThemeLighter,
'material-theme-palenight': MaterialThemePalenight
},
bundledLangs: {
html: HtmlLang,
mdc: MdcLang,
vue: VueLang,
yml: YamlLang,
postcss: PostcssLang
}
})
}
return highlighter
}

View File

@@ -206,7 +206,7 @@ An example SFC using IntelliSense (note the `/*ui*/` prefix, also works with `re
<UCard :ui="ui" />
</template>
<script setup>
<script setup lang="ts">
const ui = /*ui*/ {
background: 'bg-white dark:bg-slate-900'
}

View File

@@ -25,7 +25,7 @@ Try to change the `primary` and `gray` colors by clicking on the :u-icon{name="i
As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors. By default, the `primary` color is `green` and the `gray` color is `cool`.
When [using custom colors](https://tailwindcss.com/docs/customizing-colors#using-custom-colors) or [adding additional colors](https://tailwindcss.com/docs/customizing-colors#adding-additional-colors) through the `extend` key in your `tailwind.config.ts`, you'll need to make sure to define all the shades from `50` to `950` as most of them are used in the components config defined in [`ui.config.ts`](https://github.com/nuxt/ui/blob/dev/src/runtime/ui.config.ts). You can [generate your colors](https://tailwindcss.com/docs/customizing-colors#generating-colors) using tools such as https://uicolors.app/ for example.
When [using custom colors](https://tailwindcss.com/docs/customizing-colors#using-custom-colors) or [adding additional colors](https://tailwindcss.com/docs/customizing-colors#adding-additional-colors) through the `extend` key in your `tailwind.config.ts`, you'll need to make sure to define all the shades from `50` to `950` as most of them are used in the components config defined in [`ui.config/`](https://github.com/nuxt/ui/tree/dev/src/runtime/ui.config) directory. You can [generate your colors](https://tailwindcss.com/docs/customizing-colors#generating-colors) using tools such as https://uicolors.app/ for example.
```ts [tailwind.config.ts]
import type { Config } from 'tailwindcss'
@@ -106,7 +106,7 @@ This can also happen when you bind a dynamic color to a component: `<UBadge :col
### `app.config.ts`
Components are styled with Tailwind CSS but classes are all defined in the default [ui.config.ts](https://github.com/nuxt/ui/blob/dev/src/runtime/ui.config.ts) file. You can override those in your own `app.config.ts`.
You can find the config of each component in the [`ui.config/`](https://github.com/nuxt/ui/tree/dev/src/runtime/ui.config) directory. You can override those classes in your own `app.config.ts`.
```ts [app.config.ts]
export default defineAppConfig({
@@ -247,7 +247,7 @@ You can easily build a color mode button by using the `useColorMode` composable
#code
```vue
<script setup>
<script setup lang="ts">
const colorMode = useColorMode()
const isDark = computed({
get () {

View File

@@ -130,7 +130,7 @@ defineShortcuts({
To display shortcuts in your app according to the user's OS, you can use the `useShortcuts` composable.
```vue
<script setup>
<script setup lang="ts">
const { metaSymbol } = useShortcuts()
</script>

View File

@@ -4,8 +4,6 @@ links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Carousel.vue
navigation:
badge: New
---
## Usage
@@ -100,6 +98,12 @@ The number of indicators will be automatically generated based on the number of
:component-example{component="carousel-example-indicators-size"}
## Autoplay :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
You can easily implement an autoplay behavior using the exposed [API](#api) through a template ref.
:component-example{component="carousel-example-autoplay"}
## Slots
### `default`
@@ -120,7 +124,7 @@ You can customize the position of the buttons through `ui.arrows.wrapper`.
### `indicator`
With the `indicators` prop enabled, use the `#indicator` slot to set the content of the indicators. You will have access to the `active`, `index` properties and `on-click` method in the slot scope.
With the `indicators` prop enabled, use the `#indicator` slot to set the content of the indicators. You will have access to the `active`, `page` properties and `on-click` method in the slot scope.
:component-example{component="carousel-example-slots-indicator"}
@@ -132,6 +136,28 @@ You can customize the position of the buttons through `ui.indicators.wrapper`.
:component-props
## API
When accessing the component via a template ref, you can use the following:
::field-group
::field{name="page" type="number"}
The current page.
::
::field{name="pages" type="number"}
The total number of pages.
::
::field{name="select (page)"}
Go to a specific page.
::
::field{name="next ()"}
Go to the next page.
::
::field{name="prev ()"}
Go to the previous page.
::
::
## Config
:component-preset

View File

@@ -16,15 +16,15 @@ Let's start by installing the `v-calendar` and `date-fns` dependency:
::code-group
```bash [pnpm]
pnpm add v-calendar
pnpm add v-calendar@next
```
```bash [yarn]
yarn add v-calendar
yarn add v-calendar@next
```
```bash [npm]
npm install v-calendar
npm install v-calendar@next
```
::

View File

@@ -66,21 +66,17 @@ excludedProps:
---
::
### Size
### Size :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
You can change the size of the divider by using the `ui` prop
Use the `size` prop to change the size of the divider.
::component-card
---
props:
label: Nuxt UI
ui:
border:
size:
horizontal: border-t-2
size: sm
excludedProps:
- label
- ui
---
::

View File

@@ -8,14 +8,63 @@ links:
## Usage
Use the Form component to validate form data using schema libraries such as [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi), [Valibot](https://valibot.dev/) or your own validation logic. It works seamlessly with the FormGroup component to automatically display error messages around form elements.
Use the Form component to validate form data using schema libraries such as [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi), [Valibot](https://valibot.dev/), or your own validation logic.
The Form component requires the `validate` and `state` props for form validation.
It works with the [FormGroup](/components/input) component to display error messages around form elements automatically.
- `state` - a reactive object that holds the current state of the form.
- `validate` - a function that takes the form's state as input and returns an array of `FormError` objects with the following fields:
- `message` - the error message to display.
- `path` - the path to the form element matching the `name`.
The form component requires two props:
- `state` - a reactive object holding the form's state.
- `schema` - a schema object from [Yup](#yup), [Zod](#zod), [Joi](#joi), or [Valibot](#valibot).
::callout{icon="i-heroicons-light-bulb"}
Note that **no validation library is included** by default, so ensure you **install the one you need**.
::
::tabs
::component-example{label="Yup"}
---
component: 'form-example-yup'
componentProps:
class: 'w-60'
---
::
::component-example{label="Zod"}
---
component: 'form-example-zod'
componentProps:
class: 'w-60'
---
::
::component-example{label="Joi"}
---
component: 'form-example-joi'
componentProps:
class: 'w-60'
---
::
::component-example{label="Valibot"}
---
component: 'form-example-valibot'
componentProps:
class: 'w-60'
---
::
::
## Custom validation
Use the `validate` prop to apply your own validation logic.
The validation function must return a list of errors with the following attributes:
- `message` - Error message for display.
- `path` - Path to the form element corresponding to the `name` attribute.
::callout{icon="i-heroicons-light-bulb"}
Note: this can be used alongside the `schema` prop to handle complex use cases.
::
::component-example
---
@@ -25,55 +74,7 @@ componentProps:
---
::
## Schema
You can provide a schema from [Yup](#yup), [Zod](#zod) or [Joi](#joi), [Valibot](#valibot) through the `schema` prop to validate the state. It's important to note that **none of these libraries are included** by default, so make sure to **install the one you need**.
### Yup
::component-example
---
component: 'form-example-yup'
componentProps:
class: 'w-60'
---
::
### Zod
::component-example
---
component: 'form-example-zod'
componentProps:
class: 'w-60'
---
::
### Joi
::component-example
---
component: 'form-example-joi'
componentProps:
class: 'w-60'
---
::
### Valibot
::component-example
---
component: 'form-example-valibot'
componentProps:
class: 'w-60'
---
::
## Other libraries
For other validation libraries, you can define your own component with custom validation logic.
Here is an example with [Vuelidate](https://github.com/vuelidate/vuelidate):
This can also be used to integrate with other validation libraries. Here is an example with [Vuelidate](https://github.com/vuelidate/vuelidate):
```vue
<script setup lang="ts">
@@ -132,9 +133,11 @@ async function onSubmit (event: FormSubmitEvent<any>) {
// ...
} catch (err) {
if (err.statusCode === 422) {
form.value.setErrors(err.data.errors.map((err) => {
form.value.setErrors(err.data.errors.map((err) => ({
// Map validation errors to { path: string, message: string }
}))
message: err.message,
path: err.path,
})))
}
}
}
@@ -156,10 +159,11 @@ async function onSubmit (event: FormSubmitEvent<any>) {
</UForm>
</template>
```
## Input events
The Form component automatically triggers validation upon `submit`, `input`, `blur` or `change` events. This ensures that any errors are displayed as soon as the user interacts with the form elements. You can control when validation happens this using the `validate-on` prop.
The Form component automatically triggers validation upon `submit`, `input`, `blur` or `change` events.
This ensures that any errors are displayed as soon as the user interacts with the form elements. You can control when validation happens this using the `validate-on` prop.
::callout{icon="i-heroicons-light-bulb"}
Note that the `input` event is not triggered until after the initial `blur` event. This is to prevent the form from being validated as the user is typing. You can override this behavior by setting the [`eager-validation`](/components/form-group#eager-validation) prop on [`FormGroup`](/components/form-group) to `true`.

View File

@@ -5,8 +5,6 @@ links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/navigation/HorizontalNavigation.vue
navigation:
badge: New
---
## Usage

View File

@@ -71,12 +71,20 @@ props:
Use the `type` prop to change the input type, the default `type` is set to `text`, you can check all the available types at [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types).
We have improved the implementation of certain types such as [Checkbox](/components/checkbox), [Radio](/components/radio-group), etc.
Some types have been implemented in their own components, such as [Checkbox](/components/checkbox), [Radio](/components/radio-group), etc. and others have been styled like `file` for example. :u-badge{label="New" class="!rounded-full" variant="subtle"}
::component-card
---
props:
type: 'password'
type: 'file'
size: sm
options:
- name: type
restriction: included
values:
- file
- password
- number
---
::

View File

@@ -40,7 +40,7 @@ Use the `prevent-close` prop to disable the outside click alongside the `esc` ke
You can still handle the `esc` shortcut yourself by using our [defineShortcuts](/getting-started/shortcuts#defineshortcuts) composable.
```vue
<script setup>
<script setup lang="ts">
const isOpen = ref(false)
defineShortcuts({
@@ -59,6 +59,26 @@ Set the `fullscreen` prop to `true` to enable it.
:component-example{component="modal-example-fullscreen"}
### Control programmatically :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
First of all, add the `Modals` component to your app, preferably inside `app.vue`.
```vue [app.vue]
<template>
<div>
<UContainer>
<NuxtPage />
</UContainer>
<UModals />
</div>
</template>
```
Then, you can use the `useModal` composable to control your modals within your app.
:component-example{component="modal-example-composable"}
## Props
:component-props

View File

@@ -42,7 +42,7 @@ Then, you can use the `useToast` composable to add notifications to your app:
When using `toast.add`, this will push a new notification to the stack displayed in `<UNotifications />`. All the props of the `Notification` component can be passed to `toast.add`.
```vue
<script setup>
<script setup lang="ts">
const toast = useToast()
onMounted(() => {

View File

@@ -40,7 +40,7 @@ Use the `prevent-close` prop to disable the outside click alongside the `esc` ke
You can still handle the `esc` shortcut yourself by using our [defineShortcuts](/getting-started/shortcuts#defineshortcuts) composable.
```vue
<script setup>
<script setup lang="ts">
const isOpen = ref(false)
defineShortcuts({

View File

@@ -12,6 +12,7 @@ Use the `rows` prop to set the data to display in the table. By default, the tab
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-basic'
componentProps:
@@ -32,6 +33,7 @@ Use the `columns` prop to configure which columns to display. It's an array of o
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-columns'
componentProps:
@@ -43,6 +45,7 @@ You can easily use the [SelectMenu](/components/select-menu) component to change
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-columns-selectable'
componentProps:
@@ -56,6 +59,7 @@ Also, you can apply styles to `th` elements by passing a `class` to columns.
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-style'
componentProps:
@@ -71,6 +75,7 @@ You may specify the default direction of each column through the `direction` pro
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-columns-sortable'
componentProps:
@@ -88,7 +93,7 @@ You can specify a default sort for the table through the `sort` prop. It's an ob
This will set the default sort and will work even if no column is set as `sortable`.
```vue
<script setup>
<script setup lang="ts">
const sort = ref({
column: 'name',
direction: 'desc'
@@ -129,7 +134,7 @@ When fetching data from an API, we can take advantage of the [`useFetch`](https:
When doing so, you might want to set the `sort-mode` prop to `manual` to disable the automatic sorting and return the rows as is.
```vue
<script setup>
<script setup lang="ts">
// Ensure it uses `ref` instead of `reactive`.
const sort = ref({
column: 'name',
@@ -151,7 +156,7 @@ const { data, pending } = await useLazyFetch(() => `/api/users?orderBy=${sort.va
```
::callout{icon="i-heroicons-light-bulb" to="https://nuxt.com/docs/api/composables/use-fetch#params" target="_blank"}
We pass a function to `useLazyFetch` here make the url reactive but you can use the `query` / `params` options alongside `watch`.
We pass a function to `useLazyFetch` here to make the url reactive but you can use the `query` / `params` options alongside `watch`.
::
#### Custom sorting
@@ -160,6 +165,7 @@ Use the `sort-button` prop to customize the sort button in the header. You can p
::component-card{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
baseProps:
class: 'w-full'
@@ -240,6 +246,7 @@ Use a `v-model` to make the table selectable. The `v-model` will be an array of
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-selectable'
componentProps:
@@ -257,6 +264,7 @@ You can use this to navigate to a page, open a modal or even to select the row m
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-clickable'
componentProps:
@@ -270,6 +278,7 @@ You can easily use the [Input](/components/input) component to filter the rows.
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-searchable'
componentProps:
@@ -283,6 +292,7 @@ You can easily use the [Pagination](/components/pagination) component to paginat
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-paginable'
componentProps:
@@ -292,14 +302,15 @@ componentProps:
### Loading
Use the `loading` prop to display a loading state.
Use the `loading` prop to indicate that data is currently loading with an indeterminate [Progress](/components/progress#indeterminate) bar.
Use the `loading-state` prop to customize the `icon` and `label` or change them globally in `ui.table.default.loadingState`.
You can use the `progress` prop to customize the `color` and `animation` of the progress bar or change them globally in `ui.table.default.progress` (you can set it to `null` to hide the progress bar).
You can also set it to `null` to hide the loading state.
If there is no rows provided, a loading state will also be displayed. You can use the `loading-state` prop to customize the `icon` and `label` or change them globally in `ui.table.default.loadingState` (you can set it to `null` to hide the loading state).
::component-card
---
extraClass: 'overflow-hidden'
padding: false
baseProps:
class: 'w-full'
@@ -319,15 +330,19 @@ props:
loadingState:
icon: 'i-heroicons-arrow-path-20-solid'
label: "Loading..."
progress:
color: 'primary'
animation: 'carousel'
excludedProps:
- loadingState
- progress
---
::
This can be easily used with Nuxt `useAsyncData` composable.
```vue
<script setup>
<script setup lang="ts">
const columns = [...]
const { pending, data: people } = await useLazyAsyncData('people', () => $fetch('/api/people'))
@@ -348,6 +363,7 @@ You can also set it to `null` to hide the empty state.
::component-card
---
extraClass: 'overflow-hidden'
padding: false
baseProps:
class: 'w-full'
@@ -398,6 +414,7 @@ You can for example create an extra column for actions with a [Dropdown](/compon
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-slots'
componentProps:
@@ -411,6 +428,7 @@ Use the `#loading-state` slot to customize the loading state.
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-loading-slot'
componentProps:
@@ -424,6 +442,7 @@ Use the `#empty-state` slot to customize the empty state.
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-empty-slot'
componentProps:
@@ -443,7 +462,16 @@ componentProps:
Here is an example of a Table component with all its features implemented.
:component-example{component="table-example-advanced" hiddenCode :padding="false" }
::component-example
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-advanced'
componentProps:
class: 'flex-1'
hiddenCode: true
---
::
::callout{icon="i-simple-icons-github" to="https://github.com/nuxt/ui/blob/dev/docs/components/content/examples/TableExampleAdvanced.vue" target="_blank"}
Take a look at the component!

View File

@@ -119,6 +119,19 @@ props:
---
::
Use the `maxrows` prop to set a maximum number of rows when autoresizing. If set to `0`, the Textarea will infinitely grow up. :u-badge{label="New" class="!rounded-full" variant="subtle"}
::component-card
---
baseProps:
placeholder: 'Search...'
modelValue: 'Here is an autoresize Textarea, write new lines to make the Textarea grow up at a maximum of 5 rows...'
props:
autoresize: true
maxrows: 5
---
::
### Resize
Use the `resize` prop to enable the resize control.

View File

@@ -1,7 +1,13 @@
navigation: false
title: Releases
description: Follow the latest releases and updates of Nuxt UI.
title: '<span class="text-primary">Nuxt UI</span> Releases'
head.title: Releases
description: Follow the latest releases and updates happening on the nuxt/ui repository.
links:
- label: Get Started
trailingIcon: i-heroicons-arrow-right-20-solid
to: /getting-started
size: md
color: black
- label: View on GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/releases

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div>
<NuxtLoadingIndicator />
@@ -12,11 +13,18 @@
</UMain>
</UContainer>
<Footer />
<ClientOnly>
<LazyUDocsSearch :files="files" :navigation="navigation" :links="links" :fuse="{ resultLimit: 1000 }" />
</ClientOnly>
<UNotifications />
<UNotifications>
<template #title="{ title }">
<span v-html="title" />
</template>
</UNotifications>
<UModals />
</div>
</template>
@@ -34,6 +42,7 @@ defineProps<{
}>()
const route = useRoute()
const colorMode = useColorMode()
const { branch } = useContentSource()
const { data: nav } = await useAsyncData('navigation', () => fetchContentNavigation())
@@ -52,9 +61,11 @@ const navigation = computed(() => {
]
}
return nav.value.filter(item => item._path !== '/dev')
return nav.value?.filter(item => item._path !== '/dev') || []
})
const color = computed(() => colorMode.value === 'dark' ? '#18181b' : 'white')
const links = computed(() => {
return [{
label: 'Docs',
@@ -65,7 +76,7 @@ const links = computed(() => {
label: 'Pro',
icon: 'i-heroicons-square-3-stack-3d',
to: '/pro',
active: route.path.startsWith('/pro/getting-started') || route.path.startsWith('/pro/components')
active: route.path.startsWith('/pro/getting-started') || route.path.startsWith('/pro/components') || route.path.startsWith('/pro/prose')
}, {
label: 'Pricing',
icon: 'i-heroicons-credit-card',
@@ -81,6 +92,21 @@ const links = computed(() => {
}].filter(Boolean)
})
// Head
useHead({
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ key: 'theme-color', name: 'theme-color', content: color }
],
link: [
{ rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' }
],
htmlAttrs: {
lang: 'en'
}
})
// Provide
provide('navigation', navigation)

View File

@@ -1,7 +1,7 @@
import { createResolver } from '@nuxt/kit'
import colors from 'tailwindcss/colors'
import module from '../src/module'
import { excludeColors } from '../src/colors'
import { excludeColors } from '../src/runtime/utils/colors'
import pkg from '../package.json'
const { resolve } = createResolver(import.meta.url)
@@ -19,7 +19,6 @@ export default defineNuxtConfig({
'@nuxt/content',
'@nuxt/image',
'nuxt-og-image',
// '@nuxthq/studio',
module,
'@nuxtjs/fontaine',
'@nuxtjs/google-fonts',
@@ -40,6 +39,12 @@ export default defineNuxtConfig({
safelistColors: excludeColors(colors)
},
content: {
highlight: {
langs: [
'postcss',
'mdc'
]
},
sources: {
dev: {
prefix: '/dev',
@@ -67,6 +72,9 @@ export default defineNuxtConfig({
} : undefined
}
},
image: {
provider: 'ipx'
},
fontMetrics: {
fonts: ['DM Sans']
},

View File

@@ -1,35 +1,35 @@
{
"name": "@nuxt/ui-docs",
"private": true,
"type": "module",
"dependencies": {
"@nuxt/ui": "workspace:latest"
},
"devDependencies": {
"@iconify-json/heroicons": "^1.1.19",
"@iconify-json/simple-icons": "^1.1.90",
"@nuxt/content": "^2.11.0",
"@iconify-json/heroicons": "^1.1.20",
"@iconify-json/simple-icons": "^1.1.92",
"@nuxt/content": "^2.12.0",
"@nuxt/devtools": "^1.0.8",
"@nuxt/eslint-config": "^0.2.0",
"@nuxt/image": "^1.3.0",
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@0.7.3-28443525.f773d8f",
"@nuxthq/studio": "^1.0.10",
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@0.7.5-28475621.09eb8fa",
"@nuxtjs/fontaine": "^0.4.1",
"@nuxtjs/google-fonts": "^3.1.3",
"@nuxtjs/plausible": "^0.2.4",
"@octokit/rest": "^20.0.2",
"@vueuse/nuxt": "^10.7.2",
"@vueuse/nuxt": "^10.8.0",
"date-fns": "^3.3.1",
"eslint": "^8.56.0",
"joi": "^17.11.1",
"nuxt": "^3.9.3",
"joi": "^17.12.1",
"nuxt": "^3.10.2",
"nuxt-cloudflare-analytics": "^1.0.8",
"nuxt-component-meta": "^0.6.3",
"nuxt-og-image": "^2.2.4",
"prettier": "^3.2.4",
"prettier": "^3.2.5",
"typescript": "^5.3.3",
"ufo": "^1.3.2",
"ufo": "^1.4.0",
"v-calendar": "^3.1.2",
"valibot": "^0.25.0",
"valibot": "^0.29.0",
"yup": "^1.3.3",
"zod": "^3.22.4"
}

View File

@@ -105,7 +105,7 @@ const communityLinks = computed(() => [{
const resourcesLinks = [{
icon: 'i-simple-icons-figma',
label: 'Figma Kit',
to: 'https://www.figma.com/community/file/1288455405058138934/nuxt-ui',
to: 'https://www.figma.com/community/file/1288455405058138934',
target: '_blank'
}, {
label: 'Playground',

View File

@@ -164,7 +164,7 @@
</ULandingSection>
<template v-if="navigation.find(item => item._path === '/pro')">
<ULandingHero :links="page.pro.links" :ui="{ title: 'sm:text-6xl' }">
<ULandingHero id="pro" :links="page.pro.links" :ui="{ title: 'sm:text-6xl' }">
<template #title>
<span v-html="page.pro.title" />
</template>
@@ -172,6 +172,12 @@
<template #description>
<span v-html="page.pro.description" />
</template>
<video poster="https://res.cloudinary.com/nuxt/video/upload/so_14.8/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.jpg" controls class="rounded-lg">
<source src="https://res.cloudinary.com/nuxt/video/upload/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.webm" type="video/webm">
<source src="https://res.cloudinary.com/nuxt/video/upload/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.mp4" type="video/mp4">
<source src="https://res.cloudinary.com/nuxt/video/upload/v1708511800/ui-pro/video-nuxt-ui-pro_kwfbdh.ogg" type="video/ogg">
</video>
</ULandingHero>
<ULandingSection v-for="(section, index) in page.pro.sections" :key="index" v-bind="section" class="!pt-0">
@@ -454,7 +460,7 @@ const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(
inactive: true,
children: [{
name: 'ULandingHero',
to: '/pro/components/landing/landing-hero',
to: '/pro/components/landing-hero',
class: [
'inset-4',
isAfterStep(steps.landing + 2) && '-top-[calc(var(--y)-var(--step-y)-1rem)] bottom-[calc(var(--y)-var(--step-y)+1rem)]'
@@ -469,7 +475,7 @@ const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(
}]
}, isAfterStep(steps.landing + 2) && {
name: 'ULandingSection',
to: '/pro/components/landing/landing-section',
to: '/pro/components/landing-section',
class: [
'inset-4',
isBeforeStep(steps.landing + 6) && '-top-[calc(var(--y)-var(--prev-step-y)-var(--height)-1rem)] bottom-[calc(var(--y)-var(--prev-step-y)-var(--height)+1rem)]',
@@ -486,7 +492,7 @@ const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(
class: 'inset-x-4 top-16'
}, {
name: 'ULandingGrid',
to: '/pro/components/landing/landing-grid',
to: '/pro/components/landing-grid',
class: ['inset-x-4 bottom-4 top-48', isAfterStep(steps.landing + 8) && 'grid grid-cols-4 gap-4 p-4'].filter(Boolean).join(' '),
inactive: isAfterStep(steps.landing + 8),
children: [isAfterStep(steps.landing + 9) ? {
@@ -494,7 +500,7 @@ const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(
class: '!relative'
} : {
name: 'ULandingCard',
to: '/pro/components/landing/landing-card',
to: '/pro/components/landing-card',
class: '!relative h-full',
inactive: false
}, isAfterStep(steps.landing + 9) ? {
@@ -502,7 +508,7 @@ const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(
class: '!relative h-full'
} : {
name: 'ULandingCard',
to: '/pro/components/landing/landing-card',
to: '/pro/components/landing-card',
class: '!relative h-full',
inactive: false
}, isAfterStep(steps.landing + 9) ? {
@@ -510,7 +516,7 @@ const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(
class: '!relative h-full'
} : {
name: 'ULandingCard',
to: '/pro/components/landing/landing-card',
to: '/pro/components/landing-card',
class: '!relative h-full',
inactive: false
}, isAfterStep(steps.landing + 9) ? {
@@ -518,14 +524,14 @@ const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(
class: '!relative h-full'
} : {
name: 'ULandingCard',
to: '/pro/components/landing/landing-card',
to: '/pro/components/landing-card',
class: '!relative h-full',
inactive: false
}]
}]
}, isAfterStep(steps.landing + 10) && {
name: 'ULandingSection',
to: '/pro/components/landing/landing-section',
to: '/pro/components/landing-section',
class: [
'inset-4',
isBeforeStep(steps.landing + 14) && '-top-[calc(var(--y)-var(--prev-step-y)-var(--height)-1rem)] bottom-[calc(var(--y)-var(--prev-step-y)-var(--height)+1rem)]'
@@ -550,12 +556,12 @@ const landingBlocks = computed(() => isAfterStep(steps.landing) && isBeforeStep(
const docsBlocks = computed(() => [isAfterStep(steps.docs) && {
name: 'UPage',
to: '/pro/components/page/page',
to: '/pro/components/page',
class: 'inset-x-0 top-20 bottom-20',
inactive: isAfterStep(steps.docs + 1),
children: [isAfterStep(steps.docs + 2) ? {
name: 'UAside',
to: '/pro/components/aside/aside',
to: '/pro/components/aside',
class: 'left-4 inset-y-4 w-64',
inactive: isAfterStep(steps.docs + 3),
children: [isAfterStep(steps.docs + 4) ? {
@@ -566,7 +572,7 @@ const docsBlocks = computed(() => [isAfterStep(steps.docs) && {
class: 'inset-x-4 top-4 h-9'
}, isAfterStep(steps.docs + 5) ? {
name: 'UNavigationTree',
to: '/pro/components/navigation/navigation-tree',
to: '/pro/components/navigation-tree',
class: ['inset-x-4 top-[4.25rem] bottom-4', isAfterStep(steps.docs + 6) && '!bg-transparent !border-0'].join(' '),
inactive: isAfterStep(steps.docs + 6),
children: [{
@@ -582,12 +588,12 @@ const docsBlocks = computed(() => [isAfterStep(steps.docs) && {
class: 'left-4 inset-y-4 w-64'
}, isAfterStep(steps.docs + 7) ? {
name: 'UPage',
to: '/pro/components/page/page',
to: '/pro/components/page',
class: 'left-72 right-4 inset-y-4',
inactive: isAfterStep(steps.docs + 8),
children: [...(isAfterStep(steps.docs + 9) ? [{
name: 'UPageHeader',
to: '/pro/components/page/page-header',
to: '/pro/components/page-header',
class: 'top-4 left-4 right-72 h-32',
inactive: isAfterStep(steps.docs + 10),
children: [{
@@ -596,7 +602,7 @@ const docsBlocks = computed(() => [isAfterStep(steps.docs) && {
}]
}, {
name: 'UPageBody',
to: '/pro/components/page/page-body',
to: '/pro/components/page-body',
class: 'top-40 left-4 right-72 bottom-4 overflow-y-auto',
inactive: isAfterStep(steps.docs + 11),
children: [{
@@ -607,7 +613,7 @@ const docsBlocks = computed(() => [isAfterStep(steps.docs) && {
class: 'bottom-4 inset-x-4 h-28'
} : {
name: 'UDocsSurround',
to: '/pro/components/docs/docs-surround',
to: '/pro/components/docs-surround',
class: 'bottom-4 inset-x-4 h-28',
inactive: false
}]
@@ -616,7 +622,7 @@ const docsBlocks = computed(() => [isAfterStep(steps.docs) && {
class: 'left-4 right-72 inset-y-4'
}]), isAfterStep(steps.docs + 13) ? {
name: 'UDocsToc',
to: '/pro/components/docs/docs-toc',
to: '/pro/components/docs-toc',
class: 'right-4 inset-y-4 w-64',
inactive: isAfterStep(steps.docs + 14),
children: [{
@@ -635,7 +641,7 @@ const docsBlocks = computed(() => [isAfterStep(steps.docs) && {
const blocks = computed(() => [isAfterStep(steps.header) && {
name: 'UHeader',
to: '/pro/components/header/header',
to: '/pro/components/header',
class: 'h-16 inset-x-0 top-0',
inactive: isAfterStep(steps.header + 1),
children: [isAfterStep(steps.header + 2) ? {
@@ -659,7 +665,7 @@ const blocks = computed(() => [isAfterStep(steps.header) && {
}]
}, isAfterStep(steps.footer) && {
name: 'UFooter',
to: '/pro/components/footer/footer',
to: '/pro/components/footer',
class: 'h-16 inset-x-0 bottom-0',
inactive: isAfterStep(steps.footer + 1),
children: [isAfterStep(steps.footer + 2) ? {
@@ -700,9 +706,9 @@ function getStepY (step: number) {
// Hooks
onMounted(() => {
nextTick(() => {
start.value = top.value
})
setTimeout(() => {
start.value = top.value + y.value
}, 100)
})
// Slots Data

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
const title = 'Playground'
const description = 'Play online with our interactive Nuxt Image playground.'

View File

@@ -6,7 +6,12 @@
<div class="h-px w-px rounded-full bg-transparent" />
</div>
<UPageHero :title="page.title" :description="page.description" :links="page.links" align="center" />
<ULandingHero :description="page.description" :links="page.links" align="center" :ui="{ title: 'sm:text-6xl' }" class="md:py-32">
<template #title>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="page.title" />
</template>
</ULandingHero>
<UPageBody>
<div class="h-[96px] w-0.5 bg-gray-200 dark:bg-gray-800 mx-auto rounded-t-full" />
@@ -54,18 +59,20 @@ const dates = computed(() => {
})
})
const title = page.value.head?.title || page.value.title
const description = page.value.head?.description || page.value.description
useSeoMeta({
titleTemplate: '%s - Nuxt UI',
title: page.value.title,
ogTitle: `${page.value.title} - Nuxt UI`,
description: page.value.description,
ogDescription: page.value.description
title,
description,
ogTitle: `${title} - Nuxt UI`,
ogDescription: description
})
defineOgImage({
component: 'Docs',
title: page.value.title,
description: page.value.description
title,
description
})
</script>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
const title = 'Roadmap'
const description = 'Discover our Volta board for @nuxt/ui development status.'

View File

@@ -1,8 +1,9 @@
{
"name": "@nuxt/ui",
"version": "2.13.0",
"version": "2.14.0",
"repository": "nuxt/ui",
"homepage": "https://ui.nuxt.com",
"type": "module",
"license": "MIT",
"exports": {
".": {
@@ -32,27 +33,27 @@
"test": "vitest"
},
"dependencies": {
"@egoist/tailwindcss-icons": "^1.7.2",
"@egoist/tailwindcss-icons": "^1.7.4",
"@headlessui/tailwindcss": "^0.2.0",
"@headlessui/vue": "1.7.16",
"@iconify-json/heroicons": "^1.1.19",
"@nuxt/kit": "^3.9.3",
"@headlessui/vue": "^1.7.19",
"@iconify-json/heroicons": "^1.1.20",
"@nuxt/kit": "^3.10.2",
"@nuxtjs/color-mode": "^3.3.2",
"@nuxtjs/tailwindcss": "^6.11.2",
"@nuxtjs/tailwindcss": "^6.11.4",
"@popperjs/core": "^2.11.8",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@vueuse/core": "^10.7.2",
"@vueuse/integrations": "^10.7.2",
"@vueuse/math": "^10.7.2",
"@vueuse/core": "^10.8.0",
"@vueuse/integrations": "^10.8.0",
"@vueuse/math": "^10.8.0",
"defu": "^6.1.4",
"fuse.js": "^6.6.2",
"nuxt-icon": "^0.6.8",
"ohash": "^1.1.3",
"pathe": "^1.1.2",
"scule": "^1.2.0",
"scule": "^1.3.0",
"tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1"
},
@@ -63,23 +64,24 @@
"@release-it/conventional-changelog": "^8.0.1",
"@vue/test-utils": "^2.4.4",
"eslint": "^8.56.0",
"happy-dom": "^13.3.5",
"joi": "^17.11.1",
"nuxt": "^3.9.3",
"release-it": "^17.0.3",
"happy-dom": "^13.3.8",
"joi": "^17.12.1",
"nuxt": "^3.10.2",
"release-it": "^17.1.1",
"typescript": "^5.3.3",
"unbuild": "^2.0.0",
"valibot": "^0.25.0",
"vitest": "^1.2.2",
"valibot": "^0.29.0",
"vitest": "^1.3.1",
"vitest-environment-nuxt": "^1.0.0",
"vue-tsc": "^1.8.27",
"yup": "^1.3.3",
"zod": "^3.22.4"
},
"resolutions": {
"@nuxt/kit": "3.9.3",
"@nuxt/schema": "3.9.3",
"@nuxt/kit": "^3.10.2",
"@nuxt/schema": "3.10.2",
"tailwindcss": "3.4.1",
"vue": "3.3.13"
"@headlessui/vue": "^1.7.19",
"vue": "3.4.19"
}
}

4867
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,10 @@
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, createResolver, addPlugin } from '@nuxt/kit'
import defaultColors from 'tailwindcss/colors.js'
import { defaultExtractor as createDefaultExtractor } from 'tailwindcss/lib/lib/defaultExtractor.js'
import { iconsPlugin, getIconCollections, type CollectionNames, type IconsPluginOptions } from '@egoist/tailwindcss-icons'
import type { CollectionNames, IconsPluginOptions } from '@egoist/tailwindcss-icons'
import { name, version } from '../package.json'
import { generateSafelist, excludeColors, customSafelistExtractor } from './colors'
import createTemplates from './templates'
import * as config from './runtime/ui.config'
import type { DeepPartial, Strategy } from './runtime/types/utils'
const defaultExtractor = createDefaultExtractor({ tailwindConfig: { separator: ':' } })
// @ts-ignore
delete defaultColors.lightBlue
// @ts-ignore
delete defaultColors.warmGray
// @ts-ignore
delete defaultColors.trueGray
// @ts-ignore
delete defaultColors.coolGray
// @ts-ignore
delete defaultColors.blueGray
import installTailwind from './tailwind'
type UI = {
primary?: string
@@ -31,11 +16,13 @@ type UI = {
declare module 'nuxt/schema' {
interface AppConfigInput {
// @ts-ignore
ui?: UI
}
}
declare module '@nuxt/schema' {
interface AppConfigInput {
// @ts-ignore
ui?: UI
}
}
@@ -66,7 +53,7 @@ export default defineNuxtModule<ModuleOptions>({
version,
configKey: 'ui',
compatibility: {
nuxt: '^3.0.0-rc.8'
nuxt: '^3.10.0'
}
},
defaults: {
@@ -89,109 +76,13 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.css.push(resolve(runtimeDir, 'ui.css'))
}
// @ts-ignore
nuxt.hook('tailwindcss:config', function (tailwindConfig) {
tailwindConfig.theme = tailwindConfig.theme || {}
tailwindConfig.theme.extend = tailwindConfig.theme.extend || {}
tailwindConfig.theme.extend.colors = tailwindConfig.theme.extend.colors || {}
const globalColors: any = {
...(tailwindConfig.theme.colors || defaultColors),
...tailwindConfig.theme.extend?.colors
}
// @ts-ignore
globalColors.primary = tailwindConfig.theme.extend.colors.primary = {
50: 'rgb(var(--color-primary-50) / <alpha-value>)',
100: 'rgb(var(--color-primary-100) / <alpha-value>)',
200: 'rgb(var(--color-primary-200) / <alpha-value>)',
300: 'rgb(var(--color-primary-300) / <alpha-value>)',
400: 'rgb(var(--color-primary-400) / <alpha-value>)',
500: 'rgb(var(--color-primary-500) / <alpha-value>)',
600: 'rgb(var(--color-primary-600) / <alpha-value>)',
700: 'rgb(var(--color-primary-700) / <alpha-value>)',
800: 'rgb(var(--color-primary-800) / <alpha-value>)',
900: 'rgb(var(--color-primary-900) / <alpha-value>)',
950: 'rgb(var(--color-primary-950) / <alpha-value>)',
DEFAULT: 'rgb(var(--color-primary-DEFAULT) / <alpha-value>)'
}
if (globalColors.gray) {
// @ts-ignore
globalColors.cool = tailwindConfig.theme.extend.colors.cool = defaultColors.gray
}
// @ts-ignore
globalColors.gray = tailwindConfig.theme.extend.colors.gray = {
50: 'rgb(var(--color-gray-50) / <alpha-value>)',
100: 'rgb(var(--color-gray-100) / <alpha-value>)',
200: 'rgb(var(--color-gray-200) / <alpha-value>)',
300: 'rgb(var(--color-gray-300) / <alpha-value>)',
400: 'rgb(var(--color-gray-400) / <alpha-value>)',
500: 'rgb(var(--color-gray-500) / <alpha-value>)',
600: 'rgb(var(--color-gray-600) / <alpha-value>)',
700: 'rgb(var(--color-gray-700) / <alpha-value>)',
800: 'rgb(var(--color-gray-800) / <alpha-value>)',
900: 'rgb(var(--color-gray-900) / <alpha-value>)',
950: 'rgb(var(--color-gray-950) / <alpha-value>)'
}
const colors = excludeColors(globalColors)
// @ts-ignore
nuxt.options.appConfig.ui = {
primary: 'green',
gray: 'cool',
colors,
strategy: 'merge'
}
tailwindConfig.safelist = tailwindConfig.safelist || []
tailwindConfig.safelist.push(...generateSafelist(options.safelistColors || [], colors))
tailwindConfig.plugins = tailwindConfig.plugins || []
tailwindConfig.plugins.push(iconsPlugin(Array.isArray(options.icons) ? { collections: getIconCollections(options.icons) } : typeof options.icons === 'object' ? options.icons as IconsPluginOptions : {}))
})
createTemplates(nuxt)
// Modules
await installModule('nuxt-icon')
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
await installModule('@nuxtjs/tailwindcss', {
exposeConfig: true,
config: {
darkMode: 'class',
plugins: [
require('@tailwindcss/forms')({ strategy: 'class' }),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/typography'),
require('@tailwindcss/container-queries'),
require('@headlessui/tailwindcss')
],
content: {
files: [
resolve(runtimeDir, 'components/**/*.{vue,mjs,ts}'),
resolve(runtimeDir, 'ui.config/**/*.{mjs,js,ts}')
],
transform: {
vue: (content) => {
return content.replaceAll(/(?:\r\n|\r|\n)/g, ' ')
}
},
extract: {
vue: (content) => {
return [
...defaultExtractor(content),
// @ts-ignore
...customSafelistExtractor(options.prefix, content, nuxt.options.appConfig.ui.colors, options.safelistColors)
]
}
}
}
}
})
await installTailwind(options, nuxt, { resolve, runtimeDir })
// Plugins
@@ -199,6 +90,10 @@ export default defineNuxtModule<ModuleOptions>({
src: resolve(runtimeDir, 'plugins', 'colors')
})
addPlugin({
src: resolve(runtimeDir, 'plugins', 'modals')
})
// Components
addComponentsDir({

View File

@@ -4,7 +4,7 @@
<thead :class="ui.thead">
<tr :class="ui.tr.base">
<th v-if="modelValue" scope="col" :class="ui.checkbox.padding">
<UCheckbox :checked="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" aria-label="Select all" @change="onChange" />
<UCheckbox :model-value="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" aria-label="Select all" @change="onChange" />
</th>
<th v-for="(column, index) in columns" :key="index" scope="col" :class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size, column.class]">
@@ -20,9 +20,15 @@
</slot>
</th>
</tr>
<tr v-if="loading && progress">
<td :colspan="0" :class="ui.progress.wrapper">
<UProgress v-bind="{ ...(ui.default.progress || {}), ...progress }" size="2xs" />
</td>
</tr>
</thead>
<tbody :class="ui.tbody">
<tr v-if="loadingState && loading">
<tr v-if="loadingState && loading && !rows.length">
<td :colspan="columns.length + (modelValue ? 1 : 0)">
<slot name="loading-state">
<div :class="ui.loadingState.wrapper">
@@ -72,12 +78,13 @@ import type { PropType } from 'vue'
import { upperFirst } from 'scule'
import { defu } from 'defu'
import { useVModel } from '@vueuse/core'
import UButton from '../elements/Button.vue'
import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import UProgress from '../elements/Progress.vue'
import UCheckbox from '../forms/Checkbox.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig, get } from '../../utils'
import type { Strategy, Button } from '../../types'
import type { Strategy, Button, ProgressColor, ProgressAnimation } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { table } from '#ui/ui.config'
@@ -102,8 +109,9 @@ function defaultSort (a: any, b: any, direction: 'asc' | 'desc') {
export default defineComponent({
components: {
UButton,
UIcon,
UButton,
UProgress,
UCheckbox
},
inheritAttrs: false,
@@ -160,6 +168,10 @@ export default defineComponent({
type: Object as PropType<{ icon: string, label: string }>,
default: () => config.default.emptyState
},
progress: {
type: Object as PropType<{ color: ProgressColor, animation: ProgressAnimation }>,
default: () => config.default.progress
},
class: {
type: [String, Object, Array] as PropType<any>,
default: () => ''

View File

@@ -1,6 +1,13 @@
<template>
<div :class="ui.wrapper">
<HDisclosure v-for="(item, index) in items" v-slot="{ open, close }" :key="index" :default-open="defaultOpen || item.defaultOpen">
<HDisclosure
v-for="(item, index) in items"
v-slot="{ open, close }"
:key="index"
as="div"
:class="ui.container"
:default-open="defaultOpen || item.defaultOpen"
>
<HDisclosureButton
:ref="() => buttonRefs[index] = { open, close }"
as="template"
@@ -47,7 +54,7 @@
<script lang="ts">
import { ref, computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { Disclosure as HDisclosure, DisclosureButton as HDisclosureButton, DisclosurePanel as HDisclosurePanel } from '@headlessui/vue'
import { Disclosure as HDisclosure, DisclosureButton as HDisclosureButton, DisclosurePanel as HDisclosurePanel, provideUseId } from '@headlessui/vue'
import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import { useUI } from '../../composables/useUI'
@@ -56,6 +63,7 @@ import type { AccordionItem, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { accordion, button } from '#ui/ui.config'
import { useId } from '#imports'
const config = mergeConfig<typeof accordion>(appConfig.ui.strategy, appConfig.ui.accordion, accordion)
@@ -146,6 +154,8 @@ export default defineComponent({
el.addEventListener('transitionend', done, { once: true })
}
provideUseId(() => useId())
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,

View File

@@ -5,7 +5,7 @@
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
<div :class="ui.inner">
<p :class="ui.title">
<p v-if="(title || $slots.title)" :class="ui.title">
<slot name="title" :title="title">
{{ title }}
</slot>
@@ -57,7 +57,7 @@ export default defineComponent({
props: {
title: {
type: String,
required: true
default: null
},
description: {
type: String,

View File

@@ -35,16 +35,16 @@
</div>
<div v-if="indicators" :class="ui.indicators.wrapper">
<template v-for="index in indicatorsCount" :key="index">
<slot name="indicator" :on-click="onClick" :active="index === currentIndex" :index="index">
<template v-for="page in pages" :key="page">
<slot name="indicator" :on-click="onClick" :active="page === currentPage" :page="page">
<button
type="button"
:class="[
ui.indicators.base,
index === currentIndex ? ui.indicators.active : ui.indicators.inactive
page === currentPage ? ui.indicators.active : ui.indicators.inactive
]"
:aria-label="`set slide ${index}`"
@click="onClick(index)"
:aria-label="`set slide ${page}`"
@click="onClick(page)"
/>
</slot>
</template>
@@ -103,7 +103,7 @@ export default defineComponent({
default: undefined
}
},
setup (props) {
setup (props, { expose }) {
const { ui, attrs } = useUI('carousel', toRef(props, 'ui'), config, toRef(props, 'class'))
const carouselRef = ref<HTMLElement>()
@@ -122,9 +122,9 @@ export default defineComponent({
itemWidth.value = entry?.target?.firstElementChild?.clientWidth || 0
})
const currentIndex = computed(() => Math.round(x.value / itemWidth.value) + 1)
const currentPage = computed(() => Math.round(x.value / itemWidth.value) + 1)
const indicatorsCount = computed(() => {
const pages = computed(() => {
if (!itemWidth.value) {
return 0
}
@@ -140,10 +140,18 @@ export default defineComponent({
x.value -= itemWidth.value
}
function onClick (index: number) {
x.value = (index - 1) * itemWidth.value
function onClick (page: number) {
x.value = (page - 1) * itemWidth.value
}
expose({
pages,
page: currentPage,
prev: onClickPrev,
next: onClickNext,
select: onClick
})
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
@@ -151,8 +159,8 @@ export default defineComponent({
isFirst,
isLast,
carouselRef,
indicatorsCount,
currentIndex,
pages,
currentPage,
onClickNext,
onClickPrev,
onClick,

View File

@@ -57,7 +57,7 @@
<script lang="ts">
import { defineComponent, ref, computed, watch, toRef, onMounted, resolveComponent } from 'vue'
import type { PropType } from 'vue'
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue'
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem, provideUseId } from '@headlessui/vue'
import { defu } from 'defu'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
@@ -70,6 +70,7 @@ import type { DropdownItem, PopperOptions, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { dropdown } from '#ui/ui.config'
import { useId } from '#imports'
const config = mergeConfig<typeof dropdown>(appConfig.ui.strategy, appConfig.ui.dropdown, dropdown)
@@ -251,6 +252,8 @@ export default defineComponent({
const NuxtLink = resolveComponent('NuxtLink')
provideUseId(() => useId())
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,

View File

@@ -23,7 +23,7 @@
:rel="rel"
:target="target"
:class="active !== undefined ? (active ? activeClass : inactiveClass) : resolveLinkClass(route, $route, { isActive, isExactActive })"
@click="(e) => !isExternal && navigate(e)"
@click="(e) => (!isExternal && !disabled) && navigate(e)"
>
<slot v-bind="{ isActive: active !== undefined ? active : (exact ? isExactActive : isActive) }" />
</a>

View File

@@ -31,6 +31,7 @@
import { computed, defineComponent, toRef } from 'vue'
import type { SlotsType, PropType } from 'vue'
import { twJoin } from 'tailwind-merge'
import UIcon from './Icon.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Strategy, MeterColor, MeterSize } from '../../types'
@@ -41,6 +42,9 @@ import { meter } from '#ui/ui.config'
const config = mergeConfig<typeof meter>(appConfig.ui.strategy, appConfig.ui.meter, meter)
export default defineComponent({
components: {
UIcon
},
inheritAttrs: false,
slots: Object as SlotsType<{
indicator?: { percent: number, value: number },

View File

@@ -9,7 +9,7 @@
</slot>
<progress :class="progressClass" v-bind="{ value, max: realMax }">
{{ Math.round(percent) }}%
{{ percent !== undefined ? `${Math.round(percent)}%` : undefined }}
</progress>
<div v-if="isSteps" :class="stepsClass">
@@ -28,7 +28,7 @@ import type { PropType } from 'vue'
import { twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Strategy, ProgressSize, ProgressAnimation } from '../../types'
import type { Strategy, ProgressSize, ProgressAnimation, ProgressColor } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { progress } from '#ui/ui.config'
@@ -65,7 +65,7 @@ export default defineComponent({
}
},
color: {
type: String,
type: String as PropType<ProgressColor>,
default: () => config.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
@@ -157,7 +157,7 @@ export default defineComponent({
return index === 0
}
function stepClasses (index: string|number) {
function stepClasses (index: string | number) {
index = Number(index)
const classes = [stepClass.value]
@@ -189,6 +189,10 @@ export default defineComponent({
})
const percent = computed(() => {
if (isIndeterminate.value) {
return undefined
}
switch (true) {
case props.value < 0: return 0
case props.value > (realMax.value as number): return 100
@@ -237,9 +241,11 @@ progress:indeterminate {
&:after {
animation: carousel 2s ease-in-out infinite;
}
&::-webkit-progress-value {
animation: carousel 2s ease-in-out infinite;
}
&::-moz-progress-bar {
animation: carousel 2s ease-in-out infinite;
}
@@ -249,9 +255,11 @@ progress:indeterminate {
&:after {
animation: carousel-inverse 2s ease-in-out infinite;
}
&::-webkit-progress-value {
animation: carousel-inverse 2s ease-in-out infinite;
}
&::-moz-progress-bar {
animation: carousel-inverse 2s ease-in-out infinite;
}
@@ -261,9 +269,11 @@ progress:indeterminate {
&:after {
animation: swing 3s ease-in-out infinite;
}
&::-webkit-progress-value {
animation: swing 3s ease-in-out infinite;
}
&::-moz-progress-bar {
animation: swing 3s ease-in-out infinite;
}
@@ -273,9 +283,11 @@ progress:indeterminate {
&::after {
animation: elastic 3s ease-in-out infinite;
}
&::-webkit-progress-value {
animation: elastic 3s ease-in-out infinite;
}
&::-moz-progress-bar {
animation: elastic 3s ease-in-out infinite;
}
@@ -283,26 +295,65 @@ progress:indeterminate {
}
@keyframes carousel {
0%, 100% { width: 50% }
0% { transform: translateX(-100%) }
100% { transform: translateX(200%) }
0%,
100% {
width: 50%
}
0% {
transform: translateX(-100%)
}
100% {
transform: translateX(200%)
}
}
@keyframes carousel-inverse {
0%, 100% { width: 50% }
0% { transform: translateX(200%) }
100% { transform: translateX(-100%) }
0%,
100% {
width: 50%
}
0% {
transform: translateX(200%)
}
100% {
transform: translateX(-100%)
}
}
@keyframes swing {
0%, 100% { width: 50% }
0%, 100% { transform: translateX(-25%) }
50% { transform: translateX(125%) }
0%,
100% {
width: 50%
}
0%,
100% {
transform: translateX(-25%)
}
50% {
transform: translateX(125%)
}
}
@keyframes elastic {
/* Firefox doesn't do "margin: 0 auto", we have to play with margin-left */
0%, 100% { width: 50%; margin-left: 25%; }
50% { width: 90%; margin-left: 5% }
}
</style>
0%,
100% {
width: 50%;
margin-left: 25%;
}
50% {
width: 90%;
margin-left: 5%
}
}</style>

View File

@@ -8,7 +8,6 @@
:required="required"
:value="value"
:disabled="disabled"
:checked="checked"
:indeterminate="indeterminate"
type="checkbox"
:class="inputClass"
@@ -66,13 +65,9 @@ export default defineComponent({
type: Boolean,
default: false
},
checked: {
type: Boolean,
default: false
},
indeterminate: {
type: Boolean,
default: false
default: undefined
},
help: {
type: String,

View File

@@ -12,7 +12,7 @@ import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
import type { ObjectSchemaAsync as ValibotObjectSchema } from 'valibot'
import type { FormError, FormEvent, FormEventType, FormSubmitEvent, FormErrorEvent, Form } from '../../types/form'
import { uid } from '../../utils/uid'
import { useId } from '#imports'
class FormException extends Error {
constructor (message: string) {
@@ -49,7 +49,8 @@ export default defineComponent({
},
emits: ['submit', 'error'],
setup (props, { expose, emit }) {
const bus = useEventBus<FormEvent>(`form-${uid()}`)
const formId = useId()
const bus = useEventBus<FormEvent>(`form-${formId}`)
onMounted(() => {
bus.on(async (event) => {
@@ -108,11 +109,14 @@ export default defineComponent({
errors.value = await getErrors()
}
if (!opts.silent && errors.value.length > 0) {
if (errors.value.length > 0) {
if (opts.silent) return false
throw new FormException(
`Form validation failed: ${JSON.stringify(errors.value, null, 2)}`
)
}
return props.state
}

View File

@@ -1,22 +1,24 @@
<template>
<div :class="ui.wrapper" v-bind="attrs">
<div v-if="label || $slots.label" :class="[ui.label.wrapper, size]">
<label :for="inputId" :class="[ui.label.base, required ? ui.label.required : '']">
<slot v-if="$slots.label" name="label" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>{{ label }}</template>
</label>
<span v-if="hint || $slots.hint" :class="[ui.hint]">
<slot v-if="$slots.hint" name="hint" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>{{ hint }}</template>
</span>
</div>
<div :class="ui.inner">
<div v-if="label || $slots.label" :class="[ui.label.wrapper, size]">
<label :for="inputId" :class="[ui.label.base, required ? ui.label.required : '']">
<slot v-if="$slots.label" name="label" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>{{ label }}</template>
</label>
<span v-if="hint || $slots.hint" :class="[ui.hint]">
<slot v-if="$slots.hint" name="hint" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>{{ hint }}</template>
</span>
</div>
<p v-if="description || $slots.description" :class="[ui.description, size]">
<slot v-if="$slots.description" name="description" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>
{{ description }}
</template>
</p>
<p v-if="description || $slots.description" :class="[ui.description, size]">
<slot v-if="$slots.description" name="description" v-bind="{ error, label, name, hint, description, help }" />
<template v-else>
{{ description }}
</template>
</p>
</div>
<div :class="[label ? ui.container : '']">
<slot v-bind="{ error }" />
@@ -46,6 +48,7 @@ import type { FormError, InjectedFormGroupValue, FormGroupSize, Strategy } from
// @ts-expect-error
import appConfig from '#build/app.config'
import { formGroup } from '#ui/ui.config'
import { useId } from '#imports'
const config = mergeConfig<typeof formGroup>(appConfig.ui.strategy, appConfig.ui.formGroup, formGroup)
@@ -112,7 +115,7 @@ export default defineComponent({
})
const size = computed(() => ui.value.size[props.size ?? config.default.size])
const inputId = ref()
const inputId = ref(useId())
provide<InjectedFormGroupValue>('form-group', {
error,

View File

@@ -236,6 +236,7 @@ export default defineComponent({
ui.value.form,
rounded.value,
ui.value.placeholder,
props.type === 'file' && [ui.value.file.base, ui.value.file.padding[size.value]],
ui.value.size[size.value],
props.padded ? ui.value.padding[size.value] : 'p-0',
variant?.replaceAll('{color}', color.value),

View File

@@ -98,7 +98,8 @@ import {
ComboboxButton as HComboboxButton,
ComboboxOptions as HComboboxOptions,
ComboboxOption as HComboboxOption,
ComboboxInput as HComboboxInput
ComboboxInput as HComboboxInput,
provideUseId
} from '@headlessui/vue'
import { computedAsync, useDebounceFn } from '@vueuse/core'
import { defu } from 'defu'
@@ -114,6 +115,7 @@ import type { InputSize, InputColor, InputVariant, PopperOptions, Strategy } fro
// @ts-expect-error
import appConfig from '#build/app.config'
import { input, inputMenu } from '#ui/ui.config'
import { useId } from '#imports'
const config = mergeConfig<typeof input>(appConfig.ui.strategy, appConfig.ui.input, input)
@@ -275,7 +277,6 @@ export default defineComponent({
emits: ['update:modelValue', 'update:query', 'open', 'close', 'change'],
setup (props, { emit, slots }) {
const { ui, attrs } = useUI('input', toRef(props, 'ui'), config, toRef(props, 'class'))
const { ui: uiMenu } = useUI('inputMenu', toRef(props, 'uiMenu'), configMenu)
const popper = computed<PopperOptions>(() => defu({}, props.popper, uiMenu.value.popper as PopperOptions))
@@ -428,6 +429,8 @@ export default defineComponent({
query.value = event.target.value
}
provideUseId(() => useId())
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,

View File

@@ -26,27 +26,26 @@
</template>
<script lang="ts">
import { computed, defineComponent, inject, toRef, onMounted, ref } from 'vue'
import { computed, defineComponent, inject, toRef } from 'vue'
import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { radio } from '#ui/ui.config'
import colors from '#ui-colors'
import { uid } from '../../utils/uid'
import { useFormGroup } from '../../composables/useFormGroup'
import { useId } from '#imports'
const config = mergeConfig<typeof radio>(appConfig.ui.strategy, appConfig.ui.radio, radio)
export default defineComponent({
inheritAttrs: false,
props: {
id: {
type: String,
default: () => null
default: null
},
value: {
type: [String, Number, Boolean],
@@ -100,15 +99,10 @@ export default defineComponent({
setup (props, { emit }) {
const { ui, attrs } = useUI('radio', toRef(props, 'ui'), config, toRef(props, 'class'))
const inputId = props.id ?? useId()
const radioGroup = inject('radio-group', null)
const { emitFormChange, color, name } = radioGroup ?? useFormGroup(props, config)
const inputId = ref(props.id)
onMounted(() => {
if (!inputId.value) {
inputId.value = uid()
}
})
const pick = computed({
get () {

View File

@@ -12,6 +12,7 @@
:label="option.label"
:model-value="modelValue"
:value="option.value"
:help="option.help"
:disabled="option.disabled || disabled"
:ui="uiRadio"
@change="onUpdate(option.value)"

View File

@@ -26,11 +26,10 @@ import { twMerge, twJoin } from 'tailwind-merge'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig } from '../../utils'
import type { RangeSize, Strategy } from '../../types'
import type { RangeSize, RangeColor, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { range } from '#ui/ui.config'
import colors from '#ui-colors'
const config = mergeConfig<typeof range>(appConfig.ui.strategy, appConfig.ui.range, range)
@@ -73,7 +72,7 @@ export default defineComponent({
}
},
color: {
type: String as PropType<typeof colors[number]>,
type: String as PropType<RangeColor>,
default: () => config.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)

View File

@@ -134,7 +134,8 @@ import {
Listbox as HListbox,
ListboxButton as HListboxButton,
ListboxOptions as HListboxOptions,
ListboxOption as HListboxOption
ListboxOption as HListboxOption,
provideUseId
} from '@headlessui/vue'
import { computedAsync, useDebounceFn } from '@vueuse/core'
import { defu } from 'defu'
@@ -150,6 +151,7 @@ import type { SelectSize, SelectColor, SelectVariant, PopperOptions, Strategy }
// @ts-expect-error
import appConfig from '#build/app.config'
import { select, selectMenu } from '#ui/ui.config'
import { useId } from '#imports'
const config = mergeConfig<typeof select>(appConfig.ui.strategy, appConfig.ui.select, select)
@@ -331,7 +333,6 @@ export default defineComponent({
emits: ['update:modelValue', 'update:query', 'open', 'close', 'change'],
setup (props, { emit, slots }) {
const { ui, attrs } = useUI('select', toRef(props, 'ui'), config, toRef(props, 'class'))
const { ui: uiMenu } = useUI('selectMenu', toRef(props, 'uiMenu'), configMenu)
const popper = computed<PopperOptions>(() => defu({}, props.popper, uiMenu.value.popper as PopperOptions))
@@ -512,6 +513,8 @@ export default defineComponent({
query.value = event.target.value
}
provideUseId(() => useId())
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,

View File

@@ -66,6 +66,10 @@ export default defineComponent({
type: Number,
default: 3
},
maxrows: {
type: Number,
default: 0
},
autoresize: {
type: Boolean,
default: false
@@ -160,7 +164,7 @@ export default defineComponent({
const newRows = (scrollHeight - padding) / lineHeight
if (newRows > props.rows) {
textarea.value.rows = newRows
textarea.value.rows = props.maxrows ? Math.min(newRows, props.maxrows) : newRows
}
}
}

View File

@@ -21,17 +21,17 @@
<script lang="ts">
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { Switch as HSwitch } from '@headlessui/vue'
import { Switch as HSwitch, provideUseId } from '@headlessui/vue'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { useUI } from '../../composables/useUI'
import { useFormGroup } from '../../composables/useFormGroup'
import { mergeConfig } from '../../utils'
import type { ToggleSize, Strategy } from '../../types'
import type { ToggleSize, ToggleColor, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { toggle } from '#ui/ui.config'
import colors from '#ui-colors'
import { useId } from '#imports'
const config = mergeConfig<typeof toggle>(appConfig.ui.strategy, appConfig.ui.toggle, toggle)
@@ -67,7 +67,7 @@ export default defineComponent({
default: () => config.default.offIcon
},
color: {
type: String as PropType<typeof colors[number]>,
type: String as PropType<ToggleColor>,
default: () => config.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
@@ -137,6 +137,8 @@ export default defineComponent({
)
})
provideUseId(() => useId())
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,

View File

@@ -7,7 +7,7 @@
<div v-if="$slots.header" :class="[ui.header.base, ui.header.padding, ui.header.background]">
<slot name="header" />
</div>
<div :class="[ui.body.base, ui.body.padding, ui.body.background]">
<div v-if="$slots.default" :class="[ui.body.base, ui.body.padding, ui.body.background]">
<slot />
</div>
<div v-if="$slots.footer" :class="[ui.footer.base, ui.footer.padding, ui.footer.background]">

View File

@@ -26,7 +26,7 @@ import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Avatar, Strategy } from '../../types'
import type { Avatar, DividerSize, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { divider } from '#ui/ui.config'
@@ -52,6 +52,13 @@ export default defineComponent({
type: Object as PropType<Avatar>,
default: null
},
size: {
type: String as PropType<DividerSize>,
default: () => config.default.size,
validator (value: string) {
return Object.keys(config.border.size.horizontal).includes(value) || Object.keys(config.border.size.vertical).includes(value)
}
},
orientation: {
type: String as PropType<'horizontal' | 'vertical'>,
default: 'horizontal',
@@ -92,7 +99,7 @@ export default defineComponent({
return twJoin(
ui.value.border.base,
ui.value.border[props.orientation],
ui.value.border.size[props.orientation],
ui.value.border.size[props.orientation][props.size],
ui.value.border.type[props.type]
)
})

View File

@@ -63,7 +63,7 @@
<script lang="ts">
import { ref, computed, watch, toRef, onMounted, defineComponent } from 'vue'
import { Combobox as HCombobox, ComboboxInput as HComboboxInput, ComboboxOptions as HComboboxOptions } from '@headlessui/vue'
import { Combobox as HCombobox, ComboboxInput as HComboboxInput, ComboboxOptions as HComboboxOptions, provideUseId } from '@headlessui/vue'
import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import { useFuse } from '@vueuse/integrations/useFuse'
@@ -79,6 +79,7 @@ import type { Group, Command, Button, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { commandPalette } from '#ui/ui.config'
import { useId } from '#imports'
const config = mergeConfig<typeof commandPalette>(appConfig.ui.strategy, appConfig.ui.commandPalette, commandPalette)
@@ -366,6 +367,8 @@ export default defineComponent({
results
})
provideUseId(() => useId())
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,

View File

@@ -72,12 +72,13 @@
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { ComboboxOption as HComboboxOption } from '@headlessui/vue'
import { ComboboxOption as HComboboxOption, provideUseId } from '@headlessui/vue'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue'
import type { Group } from '../../types'
import { commandPalette } from '#ui/ui.config'
import { useId } from '#imports'
export default defineComponent({
components: {
@@ -151,6 +152,8 @@ export default defineComponent({
return content
}
provideUseId(() => useId())
return {
label,
highlight

View File

@@ -33,13 +33,7 @@
</HTabList>
<HTabPanels :class="ui.container">
<HTabPanel
v-for="(item, index) of items"
:key="index"
v-slot="{ selected }"
:class="ui.base"
tabindex="-1"
>
<HTabPanel v-for="(item, index) of items" :key="index" v-slot="{ selected }" :class="ui.base" :unmount="unmount">
<slot :name="item.slot || 'item'" :item="item" :index="index" :selected="selected">
{{ item.content }}
</slot>
@@ -51,7 +45,7 @@
<script lang="ts">
import { toRef, ref, watch, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { TabGroup as HTabGroup, TabList as HTabList, Tab as HTab, TabPanels as HTabPanels, TabPanel as HTabPanel } from '@headlessui/vue'
import { TabGroup as HTabGroup, TabList as HTabList, Tab as HTab, TabPanels as HTabPanels, TabPanel as HTabPanel, provideUseId } from '@headlessui/vue'
import { useResizeObserver } from '@vueuse/core'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
@@ -59,6 +53,7 @@ import type { TabItem, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { tabs } from '#ui/ui.config'
import { useId } from '#imports'
const config = mergeConfig<typeof tabs>(appConfig.ui.strategy, appConfig.ui.tabs, tabs)
@@ -89,6 +84,10 @@ export default defineComponent({
type: Array as PropType<TabItem[]>,
default: () => []
},
unmount: {
type: Boolean,
default: false
},
class: {
type: [String, Object, Array] as PropType<any>,
default: () => ''
@@ -151,6 +150,8 @@ export default defineComponent({
onMounted(() => calcMarkerSize(selectedIndex.value))
provideUseId(() => useId())
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,

View File

@@ -29,13 +29,14 @@
<script lang="ts">
import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { Dialog as HDialog, DialogPanel as HDialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { Dialog as HDialog, DialogPanel as HDialogPanel, TransitionRoot, TransitionChild, provideUseId } from '@headlessui/vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { modal } from '#ui/ui.config'
import { useId } from '#imports'
const config = mergeConfig<typeof modal>(appConfig.ui.strategy, appConfig.ui.modal, modal)
@@ -107,7 +108,7 @@ export default defineComponent({
function close (value: boolean) {
if (props.preventClose) {
emit('close-prevented')
return
}
@@ -116,6 +117,8 @@ export default defineComponent({
emit('close')
}
provideUseId(() => useId())
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,

View File

@@ -0,0 +1,12 @@
<template>
<component :is="modalState.component" v-if="modalState" v-bind="modalState.props" v-model="isOpen" />
</template>
<script lang="ts" setup>
import { inject } from 'vue'
import { useModal, modalInjectionKey } from '../../composables/useModal'
const modalState = inject(modalInjectionKey)
const { isOpen } = useModal()
</script>

View File

@@ -13,7 +13,7 @@
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
<div :class="ui.inner">
<p :class="ui.title">
<p v-if="(title || $slots.title)" :class="ui.title">
<slot name="title" :title="title">
{{ title }}
</slot>
@@ -73,7 +73,7 @@ export default defineComponent({
},
title: {
type: String,
required: true
default: null
},
description: {
type: String,

View File

@@ -38,7 +38,7 @@
import { computed, ref, toRef, onMounted, defineComponent, watch } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { Popover as HPopover, PopoverButton as HPopoverButton, PopoverPanel as HPopoverPanel } from '@headlessui/vue'
import { Popover as HPopover, PopoverButton as HPopoverButton, PopoverPanel as HPopoverPanel, provideUseId } from '@headlessui/vue'
import { useUI } from '../../composables/useUI'
import { usePopper } from '../../composables/usePopper'
import { mergeConfig } from '../../utils'
@@ -46,6 +46,7 @@ import type { PopperOptions, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { popover } from '#ui/ui.config'
import { useId } from '#imports'
const config = mergeConfig<typeof popover>(appConfig.ui.strategy, appConfig.ui.popover, popover)
@@ -210,6 +211,8 @@ export default defineComponent({
emit('update:open', newValue === 0)
})
provideUseId(() => useId())
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,

View File

@@ -17,13 +17,14 @@
<script lang="ts">
import { computed, toRef, defineComponent } from 'vue'
import type { WritableComputedRef, PropType } from 'vue'
import { Dialog as HDialog, DialogPanel as HDialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { Dialog as HDialog, DialogPanel as HDialogPanel, TransitionRoot, TransitionChild, provideUseId } from '@headlessui/vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { slideover } from '#ui/ui.config'
import { useId } from '#imports'
const config = mergeConfig<typeof slideover>(appConfig.ui.strategy, appConfig.ui.slideover, slideover)
@@ -100,7 +101,7 @@ export default defineComponent({
function close (value: boolean) {
if (props.preventClose) {
emit('close-prevented')
return
}
@@ -108,6 +109,8 @@ export default defineComponent({
emit('close')
}
provideUseId(() => useId())
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,

View File

@@ -12,8 +12,8 @@ type ButtonGroupProps = {
// make a ButtonGroupContext type for injection. Should include ButtonGroupProps
type ButtonGroupContext = {
children: ComponentInternalInstance[]
register(child: ComponentInternalInstance): void
unregister(child: ComponentInternalInstance): void
register (child: ComponentInternalInstance): void
unregister (child: ComponentInternalInstance): void
orientation: 'horizontal' | 'vertical'
size: string
ui: Partial<typeof buttonGroup>

View File

@@ -1,7 +1,6 @@
import { inject, ref, computed, onMounted } from 'vue'
import { inject, ref, computed } from 'vue'
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core'
import type { FormEvent, FormEventType, InjectedFormGroupValue } from '../types/form'
import { uid } from '../utils/uid'
type InputProps = {
id?: string
@@ -13,59 +12,54 @@ type InputProps = {
}
export const useFormGroup = (inputProps?: InputProps, config?: any) => {
const formBus = inject<UseEventBusReturn<FormEvent, string> | undefined>('form-events', undefined)
const formGroup = inject<InjectedFormGroupValue | undefined>('form-group', undefined)
const formInputs = inject<any>('form-inputs', undefined)
const formBus = inject<UseEventBusReturn<FormEvent, string> | undefined>('form-events', undefined)
const formGroup = inject<InjectedFormGroupValue | undefined>('form-group', undefined)
const formInputs = inject<any>('form-inputs', undefined)
const inputId = ref(inputProps?.id)
onMounted(() => {
// Remove FormGroup label bindings for RadioGroup elements to avoid label conflicts
inputId.value = inputProps?.legend === null || inputProps.legend ? undefined : inputProps?.id ?? uid()
if (formGroup) {
// Updates for="..." attribute on label if inputProps.id is provided
formGroup.inputId.value = inputId.value
if (formInputs) {
formInputs.value[formGroup.name.value] = inputId
}
}
})
const blurred = ref(false)
function emitFormEvent (type: FormEventType, path: string) {
if (formBus) {
formBus.emit({ type, path })
}
if (formGroup) {
if (inputProps?.id) {
// Updates for="..." attribute on label if inputProps.id is provided
formGroup.inputId.value = inputProps?.id
}
function emitFormBlur () {
emitFormEvent('blur', formGroup?.name.value as string)
blurred.value = true
if (formInputs) {
formInputs.value[formGroup.name.value] = formGroup.inputId.value
}
}
function emitFormChange () {
emitFormEvent('change', formGroup?.name.value as string)
const blurred = ref(false)
function emitFormEvent (type: FormEventType, path: string) {
if (formBus) {
formBus.emit({ type, path })
}
}
const emitFormInput = useDebounceFn(() => {
if (blurred.value || formGroup?.eagerValidation.value) {
emitFormEvent('input', formGroup?.name.value as string)
}
}, 300)
function emitFormBlur () {
emitFormEvent('blur', formGroup?.name.value as string)
blurred.value = true
}
return {
inputId,
name: computed(() => inputProps?.name ?? formGroup?.name.value),
size: computed(() => {
const formGroupSize = config.size[formGroup?.size.value as string] ? formGroup?.size.value : null
return inputProps?.size ?? formGroupSize ?? config?.default?.size
}),
color: computed(() => formGroup?.error?.value ? 'red' : inputProps?.color),
emitFormBlur,
emitFormInput,
emitFormChange
function emitFormChange () {
emitFormEvent('change', formGroup?.name.value as string)
}
const emitFormInput = useDebounceFn(() => {
if (blurred.value || formGroup?.eagerValidation.value) {
emitFormEvent('input', formGroup?.name.value as string)
}
}, 300)
return {
inputId: computed(() => inputProps?.id ?? formGroup?.inputId.value),
name: computed(() => inputProps?.name ?? formGroup?.name.value),
size: computed(() => {
const formGroupSize = config.size[formGroup?.size.value as string] ? formGroup?.size.value : null
return inputProps?.size ?? formGroupSize ?? config?.default?.size
}),
color: computed(() => formGroup?.error?.value ? 'red' : inputProps?.color),
emitFormBlur,
emitFormInput,
emitFormChange
}
}

View File

@@ -0,0 +1,51 @@
import { ref, inject } from 'vue'
import type { ShallowRef, Component, InjectionKey } from 'vue'
import { createSharedComposable } from '@vueuse/core'
import type { ComponentProps } from '../types/component'
import type { Modal, ModalState } from '../types/modal'
export const modalInjectionKey: InjectionKey<ShallowRef<ModalState>> = Symbol('nuxt-ui.modal')
function _useModal () {
const modalState = inject(modalInjectionKey)
const isOpen = ref(false)
function open<T extends Component> (component: T, props?: Modal & ComponentProps<T>) {
modalState.value = {
component,
props: props ?? {}
}
isOpen.value = true
}
function close () {
isOpen.value = false
modalState.value = {
component: 'div',
props: {}
}
}
/**
* Allows updating the modal props
*/
function patch <T extends Component = {}> (props: Partial<Modal & ComponentProps<T>>) {
modalState.value = {
...modalState.value,
props: {
...modalState.value.props,
...props
}
}
}
return {
isOpen,
open,
close,
patch
}
}
export const useModal = createSharedComposable(_useModal)

View File

@@ -0,0 +1,13 @@
import { defineNuxtPlugin } from '#imports'
import { shallowRef } from 'vue'
import { modalInjectionKey } from '../composables/useModal'
import type { ModalState } from '../types/modal'
export default defineNuxtPlugin((nuxtApp) => {
const modalState = shallowRef<ModalState>({
component: 'div',
props: {}
})
nuxtApp.vueApp.provide(modalInjectionKey, modalState)
})

14
src/runtime/types/component.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
export type ComponentProps<T> =
T extends new () => { $props: infer P } ? NonNullable<P> :
T extends (props: infer P, ...args: any) => any ? P :
{}
export type ComponentSlots<T> =
T extends new () => { $slots: infer S } ? NonNullable<S> :
T extends (props: any, ctx: { slots: infer S; attrs: any; emit: any }, ...args: any) => any ? NonNullable<S> :
{}
export type ComponentEmit<T> =
T extends new () => { $emit: infer E } ? NonNullable<E> :
T extends (props: any, ctx: { slots: any; attrs: any; emit: infer E }, ...args: any) => any ? NonNullable<E> :
{}

3
src/runtime/types/divider.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
import { divider } from '#ui/ui.config'
export type DividerSize = keyof typeof divider.border.size.horizontal | keyof typeof divider.border.size.vertical

View File

@@ -10,7 +10,8 @@ export interface FormErrorWithId extends FormError {
}
export interface Form<T> {
validate(path?: string, opts?: { silent?: boolean }): Promise<T>
validate(path?: string | string[], opts?: { silent?: true }): Promise<T | false>;
validate(path?: string | string[], opts?: { silent?: false }): Promise<T>;
clear(path?: string): void
errors: Ref<FormError[]>
setErrors(errs: FormError[], path?: string): void

View File

@@ -7,6 +7,7 @@ export * from './button'
export * from './chip'
export * from './clipboard'
export * from './command-palette'
export * from './divider'
export * from './dropdown'
export * from './form-group'
export * from './form'
@@ -15,6 +16,7 @@ export * from './input'
export * from './kbd'
export * from './link'
export * from './meter'
export * from './modal'
export * from './notification'
export * from './popper'
export * from './progress'

18
src/runtime/types/modal.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
import type { Component } from 'vue'
export interface Modal {
appear?: boolean
overlay?: boolean
transition?: boolean
preventClose?: boolean
fullscreen?: boolean
class?: string | Object | string[]
ui?: any
onClose?: () => void
onClosePrevented?: () => void
}
export interface ModalState {
component: Component | string
props: Modal
}

View File

@@ -1,4 +1,6 @@
import { progress } from '../ui.config'
import colors from '#ui-colors'
export type ProgressSize = keyof typeof progress.progress.size
export type ProgressAnimation = keyof typeof progress.animation
export type ProgressColor = typeof colors[number]

View File

@@ -1,5 +1,7 @@
import { range } from '../ui.config'
import type { ExtractDeepKey } from '.'
import type { AppConfig } from 'nuxt/schema'
import colors from '#ui-colors'
export type RangeSize = keyof typeof range.size | ExtractDeepKey<AppConfig, ['ui', 'range', 'size']>
export type RangeColor = typeof colors[number]

View File

@@ -1,5 +1,7 @@
import { toggle } from '../ui.config'
import type { ExtractDeepKey } from '.'
import type { AppConfig } from 'nuxt/schema'
import colors from '#ui-colors'
export type ToggleSize = keyof typeof toggle.size | ExtractDeepKey<AppConfig, ['ui', 'toggle', 'size']>
export type ToggleColor = typeof colors[number]

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