Compare commits

...

48 Commits

Author SHA1 Message Date
Benjamin Canac
a11248fd33 chore(release): 2.6.0 2023-07-18 17:01:45 +02:00
Benjamin Canac
2fc579560e docs: update deps 2023-07-18 17:00:30 +02:00
Marc-Olivier Castagnetti
5f8fe8559f feat(SelectMenu): handle async search (#426)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-07-18 15:58:16 +02:00
John Puaoi Tech
46b444a3e0 fix(Table): fixed row deletion bug on deselect (#425)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-07-18 12:52:53 +02:00
Benjamin Canac
0ea1f310a9 docs: missing disabled in SelectExampleObjects 2023-07-18 12:51:55 +02:00
Benjamin Canac
b1825ffa7d chore(deps): bump 2023-07-17 20:24:34 +02:00
Benjamin Canac
7be48fd6f3 docs: enable experimental.payloadExtraction 2023-07-17 20:24:05 +02:00
9uenther
d292706967 feat(Table): add click event for the entire row (#353)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-07-17 14:51:57 +02:00
Benjamin Canac
31d571abb5 docs: migrate to @nuxthq/ui-kit (#405)
Co-authored-by: Sébastien Chopin <seb@nuxt.com>
2023-07-17 14:49:50 +02:00
Benjamin Canac
2ec28e7cbd chore(Accordion): type for items
Resolves #412
2023-07-17 11:25:59 +02:00
Benjamin Canac
908235e8dd chore(VerticalNavigation): rename Link to VerticalNavigationLink type 2023-07-17 11:18:05 +02:00
Benjamin Canac
5a4d0e1097 chore(Dropdown): rename Item to DropdownItem type 2023-07-17 11:17:39 +02:00
Beautus
773a23f969 feat(Avatar)!: bind component attributes to img element (#421) 2023-07-17 10:52:06 +02:00
Jose Salazar
7554a10206 docs: explicit settings.json path (#411)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-07-17 10:34:47 +02:00
Robin Ambrosius
914cb03a5d docs(Select): disable single select options (#422) 2023-07-17 10:33:16 +02:00
Benjamin Canac
4afdd3bd64 chore(Notification): optional action click type 2023-07-13 16:08:41 +02:00
Benjamin Canac
05b8a22eec chore(Dropdown): add Item type 2023-07-12 18:44:09 +02:00
Benjamin Canac
7e08e5b024 chore(VerticalNavigation): add Link type 2023-07-12 18:43:45 +02:00
Benjamin Canac
d15e8163e7 feat(Slideover): add prevent-close prop 2023-07-12 16:00:00 +02:00
Benjamin Canac
2cc5c0d810 feat(Modal): add prevent-close prop
Resolves #303
2023-07-12 15:44:17 +02:00
Haytham A. Salama
e08263ff38 docs(Dropdown): add an example item slot (#304)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-07-12 14:52:57 +02:00
David De Sloovere
57c3023909 docs: add nuxt.config.ts ui entry example (#407)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-07-12 14:12:20 +02:00
Benjamin Canac
b874bb5061 chore(deps): bump 2023-07-10 10:42:59 +02:00
Benjamin Canac
cbe2b1bfb8 fix(SelectMenu): missing appear on transition
Resolves #400
2023-07-10 10:40:15 +02:00
Benjamin Canac
3b432fde7a chore(Accordion): add transition to trailing icon 2023-07-05 11:43:15 +02:00
HylaruCoder
a79c165eee fix(Range): progress style (#385) 2023-07-04 20:03:12 +02:00
Benjamin Canac
cd2b671075 chore(deps): bump 2023-07-04 15:24:50 +02:00
Benjamin Canac
3de6b349d8 fix(Accordion): missing ref import from vue 2023-07-04 15:21:59 +02:00
Haytham A. Salama
eaf0043da6 fix(Accordion): solve the shift between buttons when they are opened (#379)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-07-03 17:20:08 +02:00
Dominik Opyd
b78fcf91a4 feat(Accordion): add multiple prop and close others by default (#364)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-07-03 14:38:13 +02:00
Benjamin Canac
e0f1798f07 docs(installation): add example for intellisense in ui prop 2023-07-03 12:35:43 +02:00
Benjamin Canac
e4233718a6 docs: improve mobile design 2023-06-29 18:01:21 +02:00
Benjamin Canac
a11abd6347 docs: disable search modal transition on mobile 2023-06-29 17:49:02 +02:00
Benjamin Canac
db346652b8 fix(Modal): disabling transition prop had no effect 2023-06-29 17:48:44 +02:00
Benjamin Canac
52b614fcb0 chore(Table): missing types 2023-06-29 17:42:49 +02:00
Benjamin Canac
5dffa868b1 feat(Table): allow columns class customization
Resolves #366
2023-06-29 17:42:34 +02:00
Benjamin Canac
cbd8cc49fb docs: fix toc on mobile 2023-06-29 16:55:30 +02:00
Benjamin Canac
f3c6f83232 chore(Accordion): transition on height 2023-06-29 16:48:46 +02:00
Benjamin Canac
80a9738490 fix(ButtonGroup): err when no props on buttons
Resolves #360
2023-06-28 15:10:46 +02:00
Benjamin Canac
54b6f734a3 docs(Accordion): headlessui label link as Disclosure 2023-06-28 12:19:21 +02:00
Benjamin Canac
41a5238579 fix(Button): missing disabled state on some variants 2023-06-28 11:04:11 +02:00
Benjamin Canac
c92dc980c9 fix(Range): disabled thumb opacity 2023-06-28 10:55:37 +02:00
Haytham A. Salama
2451541d7d chore: add lint:fix command (#347) 2023-06-27 22:20:25 +02:00
Benjamin Canac
103a20897c chore(README): docs link as https 2023-06-27 21:55:01 +02:00
Haytham A. Salama
e50f377b94 feat(Accordion): new component (#301)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-06-27 21:54:05 +02:00
Benjamin Canac
0bfe4b01bd docs(examples): add pagination rtl github link 2023-06-27 17:37:04 +02:00
Benjamin Canac
80e09df342 docs(examples): update rtl support 2023-06-27 17:36:15 +02:00
Benjamin Canac
7a2845d75e docs: update Edge and New badges 2023-06-27 17:29:00 +02:00
112 changed files with 3550 additions and 2981 deletions

View File

@@ -3,6 +3,7 @@ module.exports = {
extends: ['@nuxt/eslint-config'],
rules: {
'semi': ['error', 'never'],
'quotes': ['error', 'single'],
'comma-dangle': ['error', 'never'],
'space-before-function-paren': ['error', 'always'],
'vue/multi-word-component-names': 0,

1
.npmrc
View File

@@ -1 +1,2 @@
shamefully-hoist=true
auto-install-peers=true

View File

@@ -1,5 +1,36 @@
# Changelog
## [2.6.0](https://github.com/nuxtlabs/ui/compare/v2.5.0...v2.6.0) (2023-07-18)
### ⚠ BREAKING CHANGES
* **Avatar:** bind component attributes to img element (#421)
### Features
* **Accordion:** add `multiple` prop and close others by default ([#364](https://github.com/nuxtlabs/ui/issues/364)) ([b78fcf9](https://github.com/nuxtlabs/ui/commit/b78fcf91a4b592a6ca83ca4333e1d6658ec6458d))
* **Accordion:** new component ([#301](https://github.com/nuxtlabs/ui/issues/301)) ([e50f377](https://github.com/nuxtlabs/ui/commit/e50f377b946996efd4546195e528fbed59dcb22f))
* **Avatar:** bind component attributes to img element ([#421](https://github.com/nuxtlabs/ui/issues/421)) ([773a23f](https://github.com/nuxtlabs/ui/commit/773a23f969d2dbbbcb01582f9e127e02f0248be9))
* **Modal:** add `prevent-close` prop ([2cc5c0d](https://github.com/nuxtlabs/ui/commit/2cc5c0d810e30b889081d1f457d725004bd0b933)), closes [#303](https://github.com/nuxtlabs/ui/issues/303)
* **SelectMenu:** handle async search ([#426](https://github.com/nuxtlabs/ui/issues/426)) ([5f8fe85](https://github.com/nuxtlabs/ui/commit/5f8fe8559f2eb12d3916387d5acf65a391bfa0eb))
* **Slideover:** add `prevent-close` prop ([d15e816](https://github.com/nuxtlabs/ui/commit/d15e8163e7d7eb3eb7624bb982c139581902d596))
* **Table:** add click event for the entire row ([#353](https://github.com/nuxtlabs/ui/issues/353)) ([d292706](https://github.com/nuxtlabs/ui/commit/d2927069673840dad58d388ab982b5488642edec))
* **Table:** allow columns `class` customization ([5dffa86](https://github.com/nuxtlabs/ui/commit/5dffa868b11760610ea0bf9f2ce37931cdac4eb9)), closes [#366](https://github.com/nuxtlabs/ui/issues/366)
### Bug Fixes
* **Accordion:** missing `ref` import from vue ([3de6b34](https://github.com/nuxtlabs/ui/commit/3de6b349d8b043ed2524bd6418f350ebb4557adb))
* **Accordion:** solve the shift between buttons when they are opened ([#379](https://github.com/nuxtlabs/ui/issues/379)) ([eaf0043](https://github.com/nuxtlabs/ui/commit/eaf0043da660dfb168a7d4f2312d4344598c2f86))
* **ButtonGroup:** err when no props on buttons ([80a9738](https://github.com/nuxtlabs/ui/commit/80a97384909891a14edca4ff760d5c81b26b3307)), closes [#360](https://github.com/nuxtlabs/ui/issues/360)
* **Button:** missing `disabled` state on some variants ([41a5238](https://github.com/nuxtlabs/ui/commit/41a523857902b1674ba7f6021938f68d66a2ddbd))
* **Modal:** disabling `transition` prop had no effect ([db34665](https://github.com/nuxtlabs/ui/commit/db346652b829ea02b8b1f5355f7080f5e530dcb2))
* **Range:** `disabled` thumb opacity ([c92dc98](https://github.com/nuxtlabs/ui/commit/c92dc980c984cff8e9f9c38eb9524d151523c16b))
* **Range:** progress style ([#385](https://github.com/nuxtlabs/ui/issues/385)) ([a79c165](https://github.com/nuxtlabs/ui/commit/a79c165eeeb3e8ea76cd3abc1b8f1218d02b446b))
* **SelectMenu:** missing `appear` on transition ([cbe2b1b](https://github.com/nuxtlabs/ui/commit/cbe2b1bfb802f8cb10dd4a0d36a8cefb215debb2)), closes [#400](https://github.com/nuxtlabs/ui/issues/400)
* **Table:** fixed row deletion bug on deselect ([#425](https://github.com/nuxtlabs/ui/issues/425)) ([46b444a](https://github.com/nuxtlabs/ui/commit/46b444a3e0cc988c89bfde7442b42b1e82095fc9))
## [2.5.0](https://github.com/nuxtlabs/ui/compare/v2.4.1...v2.5.0) (2023-06-27)

View File

@@ -47,7 +47,7 @@ If you want latest updates, please use `@nuxthq/ui-edge` in your `package.json`:
## Documentation
Visit http://ui.nuxtlabs.com to view the documentation.
Visit https://ui.nuxtlabs.com to view the documentation.
## Credits

View File

@@ -1,17 +1,41 @@
<template>
<div>
<Header />
<UHeader>
<template #left>
<NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
<UContainer class="grid lg:grid-cols-10 lg:gap-8">
<DocsAside class="lg:col-span-2" />
<span class="hidden sm:block">NuxtLabs</span><span class="sm:text-primary-500 dark:sm:text-primary-400">UI</span>
</NuxtLink>
</template>
<div class="lg:col-span-8 min-h-0 flex flex-col">
<template #center>
<UDocsSearchButton class="ml-1.5 flg:w-64 xl:w-96" />
</template>
<template #right>
<ColorPicker />
<UColorModeButton />
<USocialButton to="https://twitter.com/nuxtlabs" icon="i-simple-icons-twitter" class="hidden lg:inline-flex" />
<USocialButton to="https://github.com/nuxtlabs/ui" icon="i-simple-icons-github" class="hidden lg:inline-flex" />
</template>
<template #links>
<UDocsAsideAnchors :links="anchors" />
<UDocsAsideLinks :links="navigation" />
</template>
</UHeader>
<UContainer>
<UDocsLayout :links="navigation" :anchors="anchors">
<NuxtPage />
</div>
</UDocsLayout>
</UContainer>
<ClientOnly>
<DocsSearch />
<UDocsSearch :files="files" :links="navigation" />
</ClientOnly>
<UNotifications />
@@ -22,9 +46,24 @@
const colorMode = useColorMode()
const { data: navigation } = await useAsyncData('navigation', () => fetchContentNavigation())
const { data: files } = await useLazyAsyncData('files', () => queryContent().where({ _type: 'markdown', navigation: { $ne: false } }).find(), { default: () => [] })
provide('navigation', navigation)
const anchors = [{
label: 'Documentation',
icon: 'i-heroicons-book-open-solid',
to: '/getting-started'
}, {
label: 'Playground',
icon: 'i-simple-icons-stackblitz',
to: 'https://stackblitz.com/edit/nuxtlabs-ui?file=app.config.ts,app.vue'
}, {
label: 'Releases',
icon: 'i-heroicons-rocket-launch-solid',
to: 'https://github.com/nuxtlabs/ui/releases'
}]
// Computed
const color = computed(() => colorMode.value === 'dark' ? '#18181b' : 'white')
@@ -38,7 +77,6 @@ useHead({
{ key: 'theme-color', name: 'theme-color', content: color }
],
link: [
{ rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' },
{ rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' }
],
htmlAttrs: {

View File

@@ -1,48 +0,0 @@
import type { RouterConfig } from '@nuxt/schema'
function findHashPosition (hash): { el: any, behavior: ScrollBehavior, top: number } {
const el = document.querySelector(hash)
// vue-router does not incorporate scroll-margin-top on its own.
if (el) {
const top = parseFloat(getComputedStyle(el).scrollMarginTop)
return {
el: hash,
behavior: 'smooth',
top
}
}
}
// https://router.vuejs.org/api/#routeroptions
export default <RouterConfig>{
scrollBehavior (to, from, savedPosition) {
const nuxtApp = useNuxtApp()
// If history back
if (savedPosition) {
// Handle Suspense resolution
return new Promise((resolve) => {
nuxtApp.hooks.hookOnce('page:finish', () => {
setTimeout(() => resolve(savedPosition), 50)
})
})
}
// Scroll to heading on click
if (to.hash) {
return new Promise((resolve) => {
if (to.path === from.path) {
setTimeout(() => resolve(findHashPosition(to.hash)), 50)
} else {
nuxtApp.hooks.hookOnce('page:finish', () => {
setTimeout(() => resolve(findHashPosition(to.hash)), 50)
})
}
})
}
// Scroll to top of window
return { top: 0 }
}
}

View File

@@ -0,0 +1,23 @@
<template>
<footer class="flex items-center gap-1.5 mt-12">
<div class="flex-1 flex items-baseline gap-1.5 text-sm text-gray-600 dark:text-gray-300 leading-6">
Made by
<NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs">
<LogoLabs class="text-gray-900 dark:text-white w-14 h-auto" />
</NuxtLink>
</div>
<NuxtLink :to="`https://github.com/nuxtlabs/ui/releases/tag/v${config.version}`" target="_blank" class="inline-flex">
<UBadge :label="`v${config.version}`" />
</NuxtLink>
<div class="flex-1 flex items-center justify-end gap-1.5 -my-1 lg:hidden">
<USocialButton to="https://twitter.com/nuxtlabs" icon="i-simple-icons-twitter" />
<USocialButton to="https://github.com/nuxtlabs/ui" icon="i-simple-icons-github" />
</div>
</footer>
</template>
<script setup lang="ts">
const config = useRuntimeConfig().public
</script>

View File

@@ -1,32 +0,0 @@
<template>
<header class="sticky top-0 z-50 w-full backdrop-blur flex-none border-b border-gray-200 dark:border-gray-800 bg-white/75 dark:bg-gray-900/75">
<UContainer>
<HeaderLinks v-model="isDialogOpen" :links="links" />
</UContainer>
<TransitionRoot :show="isDialogOpen" as="template">
<Dialog as="div" @close="isDialogOpen = false">
<DialogPanel class="fixed inset-0 z-50 overflow-y-auto bg-white dark:bg-gray-900 lg:hidden">
<div class="px-4 sm:px-6 sticky top-0 border-b border-gray-200 dark:border-gray-800 bg-white/75 dark:bg-gray-900/75 backdrop-blur z-10">
<HeaderLinks v-model="isDialogOpen" :links="links" />
</div>
<div class="px-4 sm:px-6 py-4 sm:py-6">
<DocsAsideLinks @click="isDialogOpen = false" />
</div>
</DialogPanel>
</Dialog>
</TransitionRoot>
</header>
</template>
<script setup lang="ts">
import { Dialog, DialogPanel, TransitionRoot } from '@headlessui/vue'
const isDialogOpen = ref(false)
const links = [
{ label: 'Documentation', to: '/getting-started' },
{ label: 'Components', to: '/elements/avatar' },
{ label: 'Examples', to: '/examples' }
]
</script>

View File

@@ -1,64 +0,0 @@
<template>
<div class="flex items-center justify-between gap-3 h-16">
<div class="flex items-center gap-6">
<div class="flex items-center gap-3">
<NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
<span class="hidden sm:block">NuxtLabs</span><span class="sm:text-primary-500 dark:sm:text-primary-400">UI</span>
</NuxtLink>
</div>
</div>
<div class="flex items-center justify-end flex-1 -mr-1.5 gap-3">
<DocsSearchButton class="ml-1.5 flex-1 lg:flex-none lg:w-48" />
<div class="flex items-center lg:gap-1.5">
<UPopover>
<template #default="{ open }">
<UButton color="gray" variant="ghost" square :class="[open && 'bg-gray-50 dark:bg-gray-800']">
<UIcon name="i-heroicons-swatch-20-solid" class="w-5 h-5 text-primary-500 dark:text-primary-400" />
</UButton>
</template>
<template #panel>
<ColorPicker />
</template>
</UPopover>
<ColorModeButton />
<UButton
to="https://twitter.com/nuxtlabs"
target="_blank"
color="gray"
variant="ghost"
icon="i-simple-icons-twitter"
/>
<UButton
to="https://github.com/nuxtlabs/ui"
target="_blank"
color="gray"
variant="ghost"
icon="i-simple-icons-github"
/>
<UButton
color="gray"
variant="ghost"
class="lg:hidden"
:icon="isDialogOpen ? 'i-heroicons-x-mark-20-solid' : 'i-heroicons-bars-3-20-solid'"
@click="isDialogOpen = !isDialogOpen"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ modelValue: boolean, links: { to: string, label: string }[] }>()
const emit = defineEmits(['update:modelValue'])
const isDialogOpen = useVModel(props, 'modelValue', emit)
</script>

View File

@@ -1,15 +1,25 @@
<template>
<div class="p-2">
<div class="grid grid-cols-5 gap-px">
<ColorPickerButton v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" />
</div>
<UPopover>
<template #default="{ open }">
<UButton color="gray" variant="ghost" square :class="[open && 'bg-gray-50 dark:bg-gray-800']">
<UIcon name="i-heroicons-swatch-20-solid" class="w-5 h-5 text-primary-500 dark:text-primary-400" />
</UButton>
</template>
<hr class="border-gray-200 dark:border-gray-800 my-2">
<template #panel>
<div class="p-2">
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" />
</div>
<div class="grid grid-cols-5 gap-px">
<ColorPickerButton v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" />
</div>
</div>
<hr class="border-gray-200 dark:border-gray-800 my-2">
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" />
</div>
</div>
</template>
</UPopover>
</template>
<script setup lang="ts">

View File

@@ -1,33 +0,0 @@
<template>
<component
:is="to ? NuxtLink : 'div'"
:to="to"
class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm leading-6 my-5 last:mb-0 font-normal group relative prose-code:bg-white dark:prose-code:bg-gray-900"
:class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed hover:text-gray-800 dark:hover:text-gray-200' : '']"
>
<UIcon v-if="!!to" name="i-heroicons-link-20-solid" class="w-3 h-3 absolute right-2 top-2 text-gray-400 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400" />
<UIcon v-if="icon" :name="icon" class="w-4 h-4 mr-2 inline-flex items-center align-text-top" :class="color" />
<ContentSlot :use="$slots.default" unwrap="p" />
</component>
</template>
<script setup lang="ts">
const NuxtLink = resolveComponent('NuxtLink')
defineProps({
icon: {
type: String,
default: null
},
color: {
type: String,
default: 'text-primary-500 dark:text-primary-400'
},
to: {
type: String,
default: null
}
})
</script>

View File

@@ -1,18 +0,0 @@
<template>
<UKbd class="!my-0 align-text-top">
{{ shortcut }}
</UKbd>
</template>
<script setup lang="ts">
const props = defineProps({
value: {
type: String,
required: true
}
})
const { metaSymbol } = useShortcuts()
const shortcut = computed(() => props.value === 'meta' ? metaSymbol.value : props.value)
</script>

View File

@@ -1,53 +0,0 @@
<template>
<div :selected-index="selectedIndex" @change="changeTab">
<div class="flex border border-gray-200 dark:border-gray-700 border-b-0 rounded-t-md overflow-hidden -mb-px">
<div
v-for="(tab, index) in tabs"
:key="index"
as="template"
@click="selectedIndex = index"
>
<button
class="px-4 py-2 focus:outline-none text-sm border-r border-r-gray-200 dark:border-r-gray-700 transition-colors"
tabindex="-1"
:class="[selectedIndex === index ? 'font-medium text-primary-500 dark:text-primary-400 bg-gray-50 dark:bg-gray-800' : 'hover:bg-gray-50 dark:hover:bg-gray-800']"
>
{{ tab.label }}
</button>
</div>
</div>
<div class="[&>div>pre]:!rounded-t-none">
<component :is="selectedTab.component" />
</div>
</div>
</template>
<script setup lang="ts">
const slots = useSlots()
const selectedIndex = ref(0)
// Computed
const tabs = computed(() => slots.default?.().map((slot, index) => {
return {
label: slot.props?.filename || slot.props?.label || `${index}`,
component: slot
}
}) || [])
const selectedTab = computed(() => tabs.value.find((_, index) => index === selectedIndex.value))
// Methods
function changeTab (index) {
selectedIndex.value = index
}
</script>
<script lang="ts">
export default {
inheritAttrs: false
}
</script>

View File

@@ -0,0 +1,33 @@
<script setup>
const items = [{
label: 'Getting Started',
icon: 'i-heroicons-information-circle',
defaultOpen: true,
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Installation',
icon: 'i-heroicons-arrow-down-tray',
disabled: true,
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Theming',
icon: 'i-heroicons-eye-dropper',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Layouts',
icon: 'i-heroicons-rectangle-group',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Components',
icon: 'i-heroicons-square-3-stack-3d',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Utilities',
icon: 'i-heroicons-wrench-screwdriver',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}]
</script>
<template>
<UAccordion :items="items" />
</template>

View File

@@ -0,0 +1,52 @@
<script setup>
const items = [{
label: 'Getting Started',
icon: 'i-heroicons-information-circle',
defaultOpen: true,
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Installation',
icon: 'i-heroicons-arrow-down-tray',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Theming',
icon: 'i-heroicons-eye-dropper',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Layouts',
icon: 'i-heroicons-rectangle-group',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Components',
icon: 'i-heroicons-square-3-stack-3d',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Utilities',
icon: 'i-heroicons-wrench-screwdriver',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}]
</script>
<template>
<UAccordion :items="items" :ui="{ wrapper: 'flex flex-col w-full' }">
<template #default="{ item, index, open }">
<UButton color="gray" variant="ghost" class="border-b border-gray-200 dark:border-gray-700" :ui="{ rounded :'rounded-none', padding: { sm:'p-3' } }">
<template #leading>
<div class="w-6 h-6 rounded-full bg-primary-500 dark:bg-primary-400 flex items-center justify-center -my-1">
<UIcon :name="item.icon" class="w-4 h-4 text-white dark:text-gray-900" />
</div>
</template>
<span class="truncate">{{ index + 1 }}. {{ item.label }}</span>
<template #trailing>
<UIcon
name="i-heroicons-chevron-right-20-solid"
class="w-5 h-5 ms-auto transform transition-transform duration-200"
:class="[open && 'rotate-90']"
/>
</template>
</UButton>
</template>
</UAccordion>
</template>

View File

@@ -0,0 +1,74 @@
<script setup>
const items = [{
label: 'Getting Started',
icon: 'i-heroicons-information-circle',
defaultOpen: true,
slot: 'getting-started'
}, {
label: 'Installation',
icon: 'i-heroicons-arrow-down-tray',
defaultOpen: true,
slot: 'installation'
}, {
label: 'Theming',
icon: 'i-heroicons-eye-dropper',
defaultOpen: true,
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Layouts',
icon: 'i-heroicons-rectangle-group',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Components',
icon: 'i-heroicons-square-3-stack-3d',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Utilities',
icon: 'i-heroicons-wrench-screwdriver',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}]
</script>
<template>
<UAccordion :items="items">
<template #item="{ item }">
<p class="italic text-gray-900 dark:text-white text-center">
{{ item.description }}
</p>
</template>
<template #getting-started>
<div class="flex flex-col justify-center items-center gap-1">
<NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
<span class="hidden sm:block">NuxtLabs</span><span class="sm:text-primary-500 dark:sm:text-primary-400">UI</span>
</NuxtLink>
<p class="text-sm text-gray-500 dark:text-gray-400">
Fully styled and customizable components for Nuxt.
</p>
</div>
</template>
<template #installation="{ description }">
<div class="flex flex-col justify-center items-center gap-1 mb-4">
<h3 class="text-xl font-bold text-gray-900 dark:text-white">
Installation
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
Install <code>@nuxthq/ui</code> dependency to your project:
</p>
<p>
{{ description }}
</p>
</div>
<div class="flex flex-col items-center">
<code>$ npm install @nuxtlabs/ui</code>
<code>$ nnpm install -D @nuxthq/ui</code>
<code>$ pnpm i -D @nuxthq/ui</code>
</div>
</template>
</UAccordion>
</template>

View File

@@ -8,7 +8,7 @@ const groups = computed(() => {
return []
}
const users = await $fetch(`https://jsonplaceholder.typicode.com/users`, { params: { q } })
const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email }))
}

View File

@@ -0,0 +1,47 @@
<script setup>
const items = [
[{
label: 'ben@example.com',
slot: 'account',
disabled: true
}], [{
label: 'Settings',
icon: 'i-heroicons-cog-8-tooth'
}], [{
label: 'Documentation',
icon: 'i-heroicons-book-open'
}, {
label: 'Changelog',
icon: 'i-heroicons-megaphone'
}, {
label: 'Status',
icon: 'i-heroicons-signal'
}], [{
label: 'Sign out',
icon: 'i-heroicons-arrow-left-on-rectangle'
}]
]
</script>
<template>
<UDropdown :items="items" :ui="{ item: { disabled: 'cursor-text select-text' } }" :popper="{ placement: 'bottom-start' }">
<UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" />
<template #account="{ item }">
<div class="text-left">
<p>
Signed in as
</p>
<p class="truncate font-medium text-gray-900 dark:text-white">
{{ item.label }}
</p>
</div>
</template>
<template #item="{ item }">
<span class="truncate">{{ item.label }}</span>
<UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500 ms-auto" />
</template>
</UDropdown>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<UModal v-model="isOpen" :overlay="false">
<div class="p-4">
<Placeholder class="h-48" />
</div>
</UModal>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<UModal v-model="isOpen" :transition="false">
<div class="p-4">
<Placeholder class="h-48" />
</div>
</UModal>
</div>
</template>

View File

@@ -0,0 +1,24 @@
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<UModal v-model="isOpen" prevent-close>
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Modal
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
</div>
</template>
<Placeholder class="h-32" />
</UCard>
</UModal>
</div>
</template>

View File

@@ -4,8 +4,8 @@ const items = ref(Array(55))
</script>
<template>
<div class="w-full gap-3">
<div class="flex justify-between w-full mb-2 border-b pb-4">
<div class="w-full divide-y divide-gray-200 dark:divide-gray-700 space-y-4">
<div class="flex justify-between w-full">
<div dir="ltr">
<UInput
icon="i-heroicons-magnifying-glass-20-solid"
@@ -27,7 +27,7 @@ const items = ref(Array(55))
</div>
</div>
<div class="flex justify-between w-full mt-4">
<div class="flex justify-between w-full pt-4">
<div dir="ltr">
<UPagination
v-model="page"

View File

@@ -4,7 +4,8 @@ const countries = [{
value: 'US'
}, {
name: 'Canada',
value: 'CA'
value: 'CA',
disabled: true
}, {
name: 'Mexico',
value: 'MX'

View File

@@ -0,0 +1,19 @@
<script setup>
const search = async (q) => {
const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email })).filter(Boolean)
}
const selected = ref([])
</script>
<template>
<USelectMenu
v-model="selected"
:searchable="search"
placeholder="Search for a user..."
multiple
by="id"
/>
</template>

View File

@@ -1,30 +0,0 @@
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen">
<div class="p-4 sm:p-6 flex flex-col flex-1 gap-4 sm:gap-6">
<div class="flex items-center justify-between">
<h2 class="font-semibold text-gray-900 dark:text-white">
Title
</h2>
<UButton
icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="link"
size="md"
:padded="false"
@click="isOpen = false"
/>
</div>
<Placeholder class="flex-1 w-full" />
</div>
</USlideover>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen">
<div class="p-4 flex-1">
<Placeholder class="h-full" />
</div>
</USlideover>
</div>
</template>

View File

@@ -0,0 +1,23 @@
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen">
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<Placeholder class="h-8" />
</template>
<Placeholder class="h-full" />
<template #footer>
<Placeholder class="h-8" />
</template>
</UCard>
</USlideover>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen" :overlay="false">
<div class="p-4 flex-1">
<Placeholder class="h-full" />
</div>
</USlideover>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen" :transition="false">
<div class="p-4 flex-1">
<Placeholder class="h-full" />
</div>
</USlideover>
</div>
</template>

View File

@@ -0,0 +1,24 @@
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen" prevent-close>
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Slideover
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
</div>
</template>
<Placeholder class="h-full" />
</UCard>
</USlideover>
</div>
</template>

View File

@@ -0,0 +1,54 @@
<script setup>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
function select (row) {
const index = selected.value.findIndex((item) => item.id === row.id)
if (index === -1) {
selected.value.push(row)
} else {
selected.value.splice(index, 1)
}
}
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" @select="select" />
</template>

View File

@@ -1,71 +0,0 @@
<script lang="ts">
import { defineComponent } from '#imports'
export default defineComponent({
props: {
code: {
type: String,
default: ''
},
language: {
type: String,
default: null
},
filename: {
type: String,
default: null
},
highlights: {
type: Array as () => number[],
default: () => []
},
meta: {
type: String,
default: null
}
},
setup (props) {
const clipboard = useCopyToClipboard({ timeout: 2000 })
const icon = ref('i-heroicons-clipboard-document')
function copy () {
clipboard.copy(props.code, { title: 'Copied to clipboard!' })
icon.value = 'i-heroicons-clipboard-document-check'
setTimeout(() => {
icon.value = 'i-heroicons-clipboard-document'
}, 2000)
}
return {
icon,
copy
}
}
})
</script>
<template>
<div class="group relative" :class="`language-${language}`">
<UButton
:icon="icon"
variant="solid"
class="absolute right-3 top-3 opacity-0 group-hover:opacity-100 transition-opacity z-[1]"
size="xs"
tabindex="-1"
@click="copy"
/>
<span v-if="filename" class="text-gray-400 dark:text-gray-500 absolute right-3 bottom-3 text-sm group-hover:opacity-0 transition-opacity">{{ filename }}</span>
<slot />
</div>
</template>
<style>
pre code .line {
display: block;
min-height: 1rem;
}
</style>

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
defineProps<{ id: string }>()
</script>
<template>
<h2 :id="id" class="scroll-mt-[161px] lg:scroll-mt-[112px]">
<NuxtLink :href="`#${id}`" class="group">
<div class="-ml-6 pr-2 py-2 inline-flex opacity-0 group-hover:opacity-100 transition-opacity absolute">
<UIcon name="i-heroicons-hashtag-20-solid" class="w-4 h-4 text-primary-500 dark:text-primary-400" />
</div>
<slot />
</NuxtLink>
</h2>
</template>

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
defineProps<{ id: string }>()
</script>
<template>
<h3 :id="id" class="scroll-mt-[145px] lg:scroll-mt-[96px]">
<NuxtLink :href="`#${id}`" class="group">
<div class="-ml-6 pr-2 py-2 inline-flex opacity-0 group-hover:opacity-100 transition-opacity absolute">
<UIcon name="i-heroicons-hashtag-20-solid" class="w-4 h-4 text-primary-500 dark:text-primary-400" />
</div>
<slot />
</NuxtLink>
</h3>
</template>

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
defineProps<{ id: string }>()
</script>
<template>
<h3 :id="id" class="scroll-mt-[145px] lg:scroll-mt-[96px]">
<NuxtLink :href="`#${id}`" class="group">
<div class="-ml-6 pr-2 py-2 inline-flex opacity-0 group-hover:opacity-100 transition-opacity absolute">
<UIcon name="i-heroicons-hashtag-20-solid" class="w-4 h-4 text-primary-500 dark:text-primary-400" />
</div>
<slot />
</NuxtLink>
</h3>
</template>

View File

@@ -1,15 +0,0 @@
<template>
<aside class="hidden py-8 overflow-y-auto lg:block lg:self-start lg:top-16 lg:max-h-[calc(100vh-65px)] lg:sticky lg:pr-8 lg:pl-[2px]">
<div class="relative">
<!-- <div class="sticky top-0 pointer-events-none z-[1]">
<div class="h-8 bg-white dark:bg-gray-900" />
<div class="bg-white dark:bg-gray-900 relative pointer-events-auto">
<DocsSearchButton class="w-full" />
</div>
<div class="h-8 bg-gradient-to-b from-white dark:from-gray-900" />
</div> -->
<DocsAsideLinks />
</div>
</aside>
</template>

View File

@@ -1,40 +0,0 @@
<template>
<div class="space-y-8">
<div v-for="(group, index) in navigation" :key="index" class="space-y-3">
<p class="text-sm font-semibold text-gray-900 dark:text-gray-200 truncate leading-6">
{{ group.title }}
</p>
<UVerticalNavigation
:links="mapContentLinks(group.children)"
class="mt-1"
:ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-l -ml-px lg:leading-6 flex items-center gap-2',
padding: 'pl-4',
rounded: '',
font: '',
ring: '',
active: 'text-primary-500 dark:text-primary-400 border-current',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}"
>
<template #badge="{ link }">
<UBadge v-if="link.badge" size="xs" :ui="{ rounded: 'rounded-full' }">
{{ link.badge }}
</UBadge>
</template>
</UVerticalNavigation>
</div>
</div>
</template>
<script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
const navigation: Ref<NavItem[]> = inject('navigation')
function mapContentLinks (links: NavItem[]) {
return links?.map(link => ({ label: link.title, icon: link.icon, to: link._path, badge: link.badge })) || []
}
</script>

View File

@@ -1,14 +0,0 @@
<template>
<footer class="flex items-center justify-between gap-1.5">
<div class="flex items-baseline gap-1.5 text-sm text-center text-gray-500 dark:text-gray-400">
Made by
<NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs">
<LogoLabs class="text-primary-500 w-14 h-auto dark:text-primary-400" />
</NuxtLink>
</div>
<NuxtLink to="https://github.com/nuxtlabs/ui/releases" target="_blank">
<UBadge label="v2.4.0" />
</NuxtLink>
</footer>
</template>

View File

@@ -1,22 +0,0 @@
<template>
<div class="flex items-center justify-between gap-1.5">
<UButton
v-if="page"
:to="`https://github.com/nuxtlabs/ui/edit/dev/docs/content/${page._file}`"
label="Edit this page on GitHub"
color="primary"
variant="link"
:padded="false"
icon="i-heroicons-pencil-square"
/>
</div>
</template>
<script setup lang="ts">
defineProps({
page: {
type: Object,
default: null
}
})
</script>

View File

@@ -1,42 +0,0 @@
<template>
<header v-if="page" class="relative border-b border-gray-200 dark:border-gray-800 pb-8 mb-12">
<p class="mb-4 text-sm leading-6 font-semibold text-primary-500 dark:text-primary-400 capitalize">
{{ page._dir?.title ? page._dir.title : useLowerCase(page._dir) }}
</p>
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 tracking-tight dark:text-white">
{{ page.title }}
</h1>
<div v-if="page.headlessui || page.github" class="flex items-center gap-2 mt-4 lg:mt-0">
<UButton
v-if="page.headlessui"
:label="page.headlessui.label"
:to="page.headlessui.to"
icon="i-simple-icons-headlessui"
color="white"
/>
<UButton
v-if="page.github"
label="GitHub"
icon="i-simple-icons-github"
color="white"
:to="`https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/${page._dir}/${page.title.replace(' ', '')}${page.github.suffix || '.vue'}`"
/>
</div>
</div>
<p v-if="page.description" class="mt-4 text-lg text-gray-500 dark:text-gray-400">
{{ page.description }}
</p>
</header>
</template>
<script setup lang="ts">
defineProps({
page: {
type: Object,
default: null
}
})
</script>

View File

@@ -1,33 +0,0 @@
<template>
<div class="grid gap-6 sm:grid-cols-2">
<DocsPrevNextCard
v-if="prev"
:title="prev.title"
:description="prev.description"
:to="prev._path"
icon="i-heroicons-arrow-left-20-solid"
/>
<span v-else class="hidden sm:block">&nbsp;</span>
<DocsPrevNextCard
v-if="next"
:title="next.title"
:description="next.description"
:to="next._path"
icon="i-heroicons-arrow-right-20-solid"
class="text-right"
/>
</div>
</template>
<script setup lang="ts">
defineProps({
prev: {
type: Object,
default: null
},
next: {
type: Object,
default: null
}
})
</script>

View File

@@ -1,36 +0,0 @@
<template>
<NuxtLink :to="to" class="block px-5 py-8 border not-prose rounded-lg border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 group">
<div v-if="icon" class="inline-flex items-center rounded-full p-1.5 bg-gray-50 dark:bg-gray-800 group-hover:bg-primary-50 dark:group-hover:bg-primary-400/10 ring-1 ring-gray-300 dark:ring-gray-700 mb-4 group-hover:ring-primary-500/50 dark:group-hover:ring-primary-400/50">
<UIcon :name="icon" class="w-5 h-5 text-gray-900 dark:text-gray-100 group-hover:text-primary-500 dark:group-hover:text-primary-400" />
</div>
<p class="text-gray-900 dark:text-gray-50 font-medium text-[15px] mb-1">
{{ title }}
</p>
<p class="text-sm font-normal text-gray-500 dark:text-gray-400">
{{ description }}
</p>
</NuxtLink>
</template>
<script setup lang="ts">
defineProps({
icon: {
type: String,
default: null
},
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
},
to: {
type: String,
required: true
}
})
</script>

View File

@@ -1,176 +0,0 @@
<template>
<UModal
v-model="isSearchModalOpen"
:ui="{
padding: 'sm:p-4',
rounded: 'sm:rounded-lg',
width: 'sm:max-w-3xl',
height: 'h-screen sm:h-[28rem]'
}"
>
<UCommandPalette
ref="commandPaletteRef"
:groups="groups"
command-attribute="title"
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', color: 'gray', variant: 'ghost', size: 'sm', class: '-mr-1.5' }"
:ui="{ input: { height: 'h-16 sm:h-12', icon: { size: 'h-5 w-5', padding: 'pl-11' } } }"
:fuse="{
fuseOptions: { ignoreLocation: true, includeMatches: true, threshold: 0, keys: ['title', 'description', 'children.children.value', 'children.children.children.value'] },
resultLimit: 10
}"
@update:model-value="onSelect"
@close="isSearchModalOpen = false"
/>
</UModal>
</template>
<script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
import type { Command } from '../../../src/runtime/types'
const navigation: Ref<NavItem[]> = inject('navigation')
const router = useRouter()
const { usingInput } = useShortcuts()
const { isSearchModalOpen } = useDocs()
const commandPaletteRef = ref<HTMLElement & { query: Ref<string>, results: { item: Command }[] }>()
const { data: files } = await useLazyAsyncData('search', () => queryContent().where({ _type: 'markdown' }).find(), { default: () => [] })
// Computed
const defaultGroups = computed(() => navigation.value.map(item => ({
key: item._path,
label: item.title,
commands: files.value.filter(file => file._path.startsWith(item._path)).map(file => ({
id: file._id,
title: file.navigation?.title || file.title,
to: file._path,
suffix: file.description,
icon: file.icon
}))
})))
const queryGroups = computed(() => navigation.value.map(item => ({
key: item._path,
label: item.title,
commands: files.value.filter(file => file._path.startsWith(item._path)).flatMap((file) => {
return [{
id: file._id,
title: file.navigation?.title || file.title,
to: file._path,
description: file.description,
icon: file.icon
},
// @ts-ignore
...Object.entries(groupByHeading(file.body.children)).map(([hash, { title, children }]) => ({
id: `${file._path}${hash}`,
title,
prefix: `${file.navigation?.title || file.title} ->`,
prefixClass: 'text-gray-700 dark:text-gray-200',
to: `${file._path}${hash}`,
children: concatChildren(children),
icon: file.icon
}))]
})
})))
const groups = computed(() => commandPaletteRef.value?.query ? queryGroups.value : defaultGroups.value)
// avoid conflicts between multiple meta_k shortcuts
const canToggleModal = computed(() => isSearchModalOpen.value || !usingInput.value)
// Methods
function remapChildren (children: any[]) {
return children?.map((grandChild) => {
if (['code-inline', 'em', 'a', 'strong'].includes(grandChild.tag)) {
return { type: 'text', value: grandChild.children.find(child => child.type === 'text')?.value || '' }
}
return grandChild
})
}
function concatChildren (children: any[]) {
return children.map((child) => {
if (['alert'].includes(child.tag)) {
child.children = concatChildren(child.children)
}
if (child.tag === 'p') {
child.children = remapChildren(child.children)
child.children = child.children?.reduce((acc, grandChild) => {
if (grandChild.type === 'text') {
if (acc.length && acc[acc.length - 1].type === 'text') {
acc[acc.length - 1].value += grandChild.value
} else {
acc.push(grandChild)
}
} else {
acc.push(grandChild)
}
return acc
}, [])
}
if (['style'].includes(child.tag)) {
return null
}
return child
})
}
function groupByHeading (children: any[]) {
const groups = {} // grouped by path
let hash = '' // file.page with potential `#anchor` concat
let title: string | null
for (const node of children) {
// if heading found, udpate current path
if (['h2', 'h3'].includes(node.tag)) {
// find heading text value
title = node.children?.find(child => child.type === 'text')?.value
if (title) {
hash = `#${node.props.id}`
}
}
// push to existing/new group based on path
if (groups[hash]) {
groups[hash].children.push(node)
} else {
groups[hash] = { children: [node], title }
}
}
return groups
}
function onSelect (option) {
isSearchModalOpen.value = false
if (option.click) {
option.click()
} else if (option.to) {
router.push(option.to)
} else if (option.href) {
window.open(option.href, '_blank')
}
}
// Shortcuts
defineShortcuts({
meta_k: {
usingInput: true,
whenever: [canToggleModal],
handler: () => {
isSearchModalOpen.value = !isSearchModalOpen.value
}
},
escape: {
usingInput: true,
whenever: [isSearchModalOpen],
handler: () => { isSearchModalOpen.value = false }
}
})
</script>

View File

@@ -1,33 +0,0 @@
<template>
<UButton
color="white"
variant="outline"
icon="i-heroicons-magnifying-glass-20-solid"
label="Search..."
truncate
:ui="{
color: {
white: {
outline: 'ring-1 ring-inset ring-gray-200 dark:ring-gray-800 hover:ring-gray-300 dark:hover:ring-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/50 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
}
}
}"
@click="isSearchModalOpen = true"
>
<template #trailing>
<div class="hidden lg:flex items-center gap-0.5 ml-auto -my-1 flex-shrink-0">
<UKbd class="!text-inherit">
{{ metaSymbol }}
</UKbd>
<UKbd class="!text-inherit">
K
</UKbd>
</div>
</template>
</UButton>
</template>
<script setup lang="ts">
const { isSearchModalOpen } = useDocs()
const { metaSymbol } = useShortcuts()
</script>

View File

@@ -1,24 +0,0 @@
<template>
<div class="sticky top-16 bg-white/75 dark:bg-gray-900/75 backdrop-blur group lg:self-start -mx-4 sm:-mx-6 lg:mx-0 px-4 sm:px-6 lg:pl-8 lg:pr-0">
<div class="py-3 lg:py-8 border-b border-dashed border-gray-200 dark:border-gray-800 lg:border-0">
<button class="flex items-center gap-2" tabindex="-1" @click="isTocOpen = !isTocOpen">
<span class="text-sm text-slate-900 font-semibold text-sm leading-6 dark:text-slate-100 truncate">Table of Contents</span>
<UIcon name="i-heroicons-chevron-right-20-solid" class="lg:hidden w-4 h-4 transition-transform duration-100 transform text-gray-400 dark:text-gray-500" :class="[isTocOpen ? 'rotate-90' : 'rotate-0']" />
</button>
<DocsTocLinks class="mt-2 lg:mt-4" :links="toc.links" :class="[isTocOpen ? 'lg:block' : 'hidden lg:block']" />
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
toc: {
type: Object,
default: null
}
})
const isTocOpen = ref(false)
</script>

View File

@@ -1,49 +0,0 @@
<template>
<ul>
<li v-for="link in links" :key="link.text" :class="{ 'ml-3': link.depth === 3 }">
<a
:href="`#${link.id}`"
class="block py-1 font-medium text-sm"
:class="[activeHeadings.includes(link.id) ? 'text-primary-500 dark:text-primary-400' : 'hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300']"
@click.prevent="scrollToHeading(link.id)"
>
{{ link.text }}
</a>
<DocsTocLinks v-if="link.children" :links="link.children" />
</li>
</ul>
</template>
<script setup lang="ts">
import type { TocLink } from '@nuxt/content/dist/runtime/types'
defineProps({
links: {
type: Array as PropType<TocLink[]>,
default: () => []
}
})
const emit = defineEmits(['move'])
const route = useRoute()
const router = useRouter()
const { activeHeadings, updateHeadings } = useScrollspy()
watch(() => route.path, () => {
setTimeout(() => {
if (process.client) {
updateHeadings([
...document.querySelectorAll('h2'),
...document.querySelectorAll('h3')
])
}
}, 300)
}, { immediate: true })
const scrollToHeading = (id: string) => {
router.push(`#${id}`)
emit('move', id)
}
</script>

View File

@@ -1,11 +0,0 @@
import { createSharedComposable } from '@vueuse/core'
const _useDocs = () => {
const isSearchModalOpen = ref(false)
return {
isSearchModalOpen
}
}
export const useDocs = createSharedComposable(_useDocs)

View File

@@ -1,37 +0,0 @@
/**
* Scrollspy allows you to watch visible headings in a specific page.
* Useful for table of contents live style updates.
*/
export const useScrollspy = () => {
const observer = ref() as Ref<IntersectionObserver>
const visibleHeadings = ref([]) as Ref<string[]>
const activeHeadings = ref([]) as Ref<string[]>
const observerCallback = (entries: IntersectionObserverEntry[]) =>
entries.forEach((entry) => {
const id = entry.target.id
if (entry.isIntersecting) { visibleHeadings.value.push(id) } else { visibleHeadings.value = visibleHeadings.value.filter(t => t !== id) }
})
const updateHeadings = (headings: Element[]) =>
headings.forEach((heading) => {
observer.value.observe(heading)
})
watch(visibleHeadings, (val, oldVal) => {
if (val.length === 0) { activeHeadings.value = oldVal } else { activeHeadings.value = val }
})
// Create intersection observer
onBeforeMount(() => (observer.value = new IntersectionObserver(observerCallback)))
// Destroy it
onBeforeUnmount(() => observer.value?.disconnect())
return {
visibleHeadings,
activeHeadings,
updateHeadings
}
}

View File

@@ -8,7 +8,7 @@ head:
This module has been developed by the [NuxtLabs](https://nuxtlabs.com/) team for [Volta](https://volta.net) and [Nuxt Studio](https://nuxt.studio/), 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.
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
[Volta](https://volta.net/) entire UI is built with this module alongside the 50+ keyboard shortcuts defined.
::
@@ -31,7 +31,3 @@ This module has been developed by the [NuxtLabs](https://nuxtlabs.com/) team for
- [tailwindlabs/headlessui](https://github.com/tailwindlabs/headlessui)
- [vueuse/vueuse](https://github.com/vueuse/vueuse)
- [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons)
::alert{icon="i-heroicons-exclamation-triangle"}
This documentation is still a work in progress and will be updated regularly.
::

View File

@@ -2,6 +2,8 @@
description: 'Learn how to install and configure the module in your Nuxt app.'
---
## Quick Start
1. Install `@nuxthq/ui` dependency to your project:
::code-group
@@ -30,21 +32,17 @@ export default defineNuxtConfig({
That's it! You can now use all the components and composables in your Nuxt app ✨
::alert{icon="i-heroicons-exclamation-triangle"}
::callout{icon="i-heroicons-exclamation-triangle"}
As this module installs [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/) and [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) for you, you should remove them from your `modules` and `dependencies` if you've previously installed them manually.
::
## Playground
:u-button{icon="i-simple-icons-stackblitz" label="Play on StackBlitz" size="lg" to="https://stackblitz.com/edit/nuxtlabs-ui?file=app.config.ts,app.vue" target="_blank"}
## IntelliSense
If you're using VSCode, you can install the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension to get autocompletion for the classes.
You can read more on how to set it up on the [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/tailwind/editor-support) module documentation, but to summarize, you'll need to add the following to your `settings.json`:
You can read more on how to set it up on the [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/tailwind/editor-support) module documentation, but to summarize, you'll need to add the following to your `.vscode/settings.json`:
```json [settings.json]
```json [.vscode/settings.json]
{
"files.associations": {
"*.css": "tailwindcss"
@@ -67,17 +65,17 @@ export default <Partial<Config>> {
}
```
If you do so, you'll need to add the following to your `settings.json`:
If you do so, you'll need to add the following to your `.vscode/settings.json`:
```json [settings.json]
```json [.vscode/settings.json]
{
"tailwindCSS.experimental.configFile": "tailwind.config.ts"
}
```
Also, the extension won't work when writing classes in your `app.config.ts` by default. You can add the following to your `settings.json` to fix this:
Also, the extension won't work when writing classes in your `app.config.ts` by default. You can add the following to your `.vscode/settings.json` to fix this:
```json [settings.json]
```json [.vscode/settings.json]
{
"tailwindCSS.experimental.classRegex": [
["ui:\\s*{([^)]*)\\s*}", "[\"'`]([^\"'`]*).*?[\"'`]"]
@@ -85,6 +83,19 @@ Also, the extension won't work when writing classes in your `app.config.ts` by d
}
```
You can also add the following to your `.vscode/settings.json` to enable IntelliSense when using the `ui` prop:
```json [.vscode/settings.json]
{
"tailwindCSS.classAttributes": [
"class",
"className",
"ngClass",
"ui"
]
}
```
## Options
| Key | Default | Description |
@@ -92,7 +103,19 @@ Also, the extension won't work when writing classes in your `app.config.ts` by d
| `prefix` | `u` | Define the prefix of the imported components. |
| `global` | `false` | Expose components globally. |
| `icons` | `['heroicons']` | Icon collections to load. |
| `safelistColors` | `['primary']` | Force safelisting of colors. |
| `safelistColors` | `['primary']` | Force safelisting of colors to need be purged. |
Configure options in your `nuxt.config.ts` as such:
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['@nuxthq/ui'],
ui: {
global: true,
icons: ['mdi', 'simple-icons']
}
})
```
## Edge

View File

@@ -19,7 +19,7 @@ export default defineAppConfig({
})
```
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
Try to change the `primary` and `gray` colors by clicking on the :u-icon{name="i-heroicons-swatch-20-solid" class="w-4 h-4 align-middle text-primary-500 dark:text-primary-400"} button in the header.
::
@@ -29,11 +29,11 @@ To provide dynamic colors that can be changed at runtime, this module uses CSS v
Likewise, you can't define a `primary` color in your `tailwind.config.ts` as it would conflict with the `primary` color defined by the module.
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
We'd advise you to use those colors in your components and pages, e.g. `text-primary-500 dark:text-primary-400`, `bg-gray-100 dark:bg-gray-900`, etc. so your app automatically adapts when changing your `app.config.ts`.
::
Components having a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style), [Input](/elements/input#style) (inherited in [Select](/forms/select) and [SelectMenu](/forms/select-menu)), [Radio](/forms/radio), [Checkbox](/forms/checkbox), [Toggle](/forms/toggle), [Range](/forms/range) and [Notification](/overlays/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors.
Components having a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style), [Input](/forms/input#style) (inherited in [Select](/forms/select) and [SelectMenu](/forms/select-menu)), [Radio](/forms/radio), [Checkbox](/forms/checkbox), [Toggle](/forms/toggle), [Range](/forms/range) and [Notification](/overlays/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors.
Variant classes of those components are defined with a syntax like `bg-{color}-500 dark:bg-{color}-400` so they can be used with any color. However, this means that Tailwind will not find those classes and therefore will not generate the corresponding CSS.
@@ -73,7 +73,7 @@ All the components are styled with dark mode in mind.
Thanks to [Tailwind CSS dark mode](https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually) class strategy and the [@nuxtjs/color-mode](https://github.com/nuxt-modules/color-mode) module, you literally have nothing to do.
::alert{icon="i-heroicons-puzzle-piece"}
::callout{icon="i-heroicons-puzzle-piece"}
Learn how to build a color mode button in the [Examples](/getting-started/examples#color-mode-button) page.
::
@@ -87,7 +87,7 @@ export default defineNuxtConfig({
})
```
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
If you're stuck in dark mode even after changing this setting, you might need to remove the `nuxt-color-mode` entry from your browser's local storage.
::
@@ -115,7 +115,7 @@ Each component has a `ui` prop that allows you to customize everything specifica
</template>
```
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
You can find the default classes for each component under the `Preset` section.
::
@@ -165,7 +165,7 @@ export default defineNuxtConfig({
})
```
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
Search the icon you want to use on https://icones.js.org built by [@antfu](https://github.com/antfu).
::

View File

@@ -21,7 +21,7 @@ Shortcuts are displayed and styled through the [Kbd](/elements/kbd) component.
</template>
```
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
You will have a preview of how shortcuts are rendered in each component page.
::
@@ -48,7 +48,7 @@ defineShortcuts({
</script>
```
Shortcuts keys are written as the literal keyboard key value. Combinations are made with `_` separator. Chained shortcuts are made with `-` separator.
Shortcuts keys are written as the literal keyboard key value. Combinations are made with `_` separator. Chained shortcuts are made with `-` separator. :u-badge{label="New" class="!rounded-full"}
Modifiers are also available:
- `meta`: acts as `Command` for MacOS and `Control` for others

View File

@@ -3,7 +3,7 @@ title: Examples
description: Discover some real-life examples of components you can build.
---
::alert{icon="i-heroicons-wrench-screwdriver"}
::callout{icon="i-heroicons-wrench-screwdriver"}
If you have any ideas of examples you'd like to see, please comment on [this issue](https://github.com/nuxtlabs/ui/issues/297).
::
@@ -153,7 +153,7 @@ padding: false
:command-palette-theme-algolia{class="max-h-[480px] rounded-md"}
::
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeAlgolia.vue#L23"}
::callout{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeAlgolia.vue#L23"}
Take a look at the component!
::
@@ -168,7 +168,7 @@ padding: false
:command-palette-theme-raycast{class="max-h-[480px] rounded-md"}
::
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeRaycast.vue#L30"}
::callout{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeRaycast.vue#L30"}
Take a look at the component!
::
@@ -250,9 +250,17 @@ const items = ref(Array(55))
```
::
### LTR and RTL
## RTL Support
Here are some examples of how components look like in RTL mode.
### Pagination
::component-example
#default
:l-t-r-and-r-t-l-theme
:pagination-example-r-t-l
::
::callout{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/examples/PaginationExampleRTL.vue"}
Take a look at the component!
::

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display an image that represents a resource or a group of resources.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Avatar.vue
---
## Usage
@@ -29,7 +32,7 @@ baseProps:
### Chip
Use the `chip-color`, `chip-text` :u-badge{label="Edge" class="!rounded-full"} and `chip-position` props to display a chip on the Avatar.
Use the `chip-color`, `chip-text` :u-badge{label="New" class="!rounded-full"} and `chip-position` props to display a chip on the Avatar.
::component-card
---

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display a short text to represent a status or a category.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Badge.vue
---
## Usage

View File

@@ -1,6 +1,9 @@
---
github: true
description: Create a button with icon or link capabilities.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Button.vue
---
## Usage

View File

@@ -1,9 +1,12 @@
---
github: true
description: Display a list of actions in a dropdown menu.
headlessui:
label: 'Menu'
to: 'https://headlessui.com/vue/menu'
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Dropdown.vue
- label: Menu
icon: i-simple-icons-headlessui
to: https://headlessui.com/vue/menu
---
## Usage
@@ -12,8 +15,10 @@ Pass an array of arrays to the `items` prop of the Dropdown component. Each arra
- `label` - The label of the item.
- `icon` - The icon of the item.
- `iconClass` - The class of the icon of the item.
- `avatar` - The avatar of the item. You can pass all the props of the [Avatar](/elements/avatar) component.
- `shortcuts` - The shortcuts of the item.
- `slot` - The slot of the item.
- `disabled` - Whether the item is disabled.
- `click` - The click handler of the item.
@@ -95,6 +100,68 @@ const items = [
```
::
## Slots
### `item`
Use the `#item` slot to customize the items content or pass a `slot` property to customize a specific item. You will have access to the `item` property in the slot scope.
::component-example
#default
:dropdown-example-slot
#code
```vue
<script setup>
const items = [
[{
label: 'ben@example.com',
slot: 'account',
disabled: true
}], [{
label: 'Settings',
icon: 'i-heroicons-cog-8-tooth'
}], [{
label: 'Documentation',
icon: 'i-heroicons-book-open'
}, {
label: 'Changelog',
icon: 'i-heroicons-megaphone'
}, {
label: 'Status',
icon: 'i-heroicons-signal'
}], [{
label: 'Sign out',
icon: 'i-heroicons-arrow-left-on-rectangle'
}]
]
</script>
<template>
<UDropdown :items="items" :ui="{ item: { disabled: 'cursor-text select-text' } }" :popper="{ placement: 'bottom-start' }">
<UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" />
<template #account="{ item }">
<div class="text-left">
<p>
Signed in as
</p>
<p class="truncate font-medium text-gray-900 dark:text-white">
{{ item.label }}
</p>
</div>
</template>
<template #item="{ item }">
<span class="truncate">{{ item.label }}</span>
<UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500 ms-auto" />
</template>
</UDropdown>
</template>
```
::
## Props
:component-props

View File

@@ -0,0 +1,287 @@
---
description: Display togglable accordion panels.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Accordion.vue
- label: Disclosure
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/disclosure'
navigation:
badge: 'Edge'
---
## Usage
Pass an array to the `items` prop of the Accordion component. Each item can have any property from the [Button](/elements/button) component such as `label`, `icon`, `color`, `variant`, `size`, etc. but also:
- `slot` - A key to customize the item with a slot.
- `content` - The content to display in the panel by default.
- `disabled` - Determines whether the item is disabled or not.
- `defaultOpen` - Determines whether the item is initially open or closed.
- `closeOthers` - Determines whether the item click close others or not. **It only works with multiple mode**.
::component-example
#default
:accordion-example-basic
#code
```vue
<script setup>
const items = [{
label: 'Getting Started',
icon: 'i-heroicons-information-circle',
defaultOpen: true,
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, {
label: 'Installation',
icon: 'i-heroicons-arrow-down-tray',
disabled: true,
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, ...]
</script>
<template>
<UAccordion :items="items" />
</template>
```
::
### Style
You can also pass any prop from the [Button](/elements/button) component directly to the Accordion component to style the buttons.
::component-card
---
baseProps:
items:
- label: '1. What is NuxtLabs UI?'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '2. Getting Started'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '3. Theming'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '4. Components'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
props:
color: 'primary'
variant: 'soft'
size: 'sm'
ui:
variant:
solid: 1
outline: 1
ghost: 1
soft: 1
link: 1
size:
2xs: ''
xs: ''
sm: ''
md: ''
lg: ''
xl: ''
---
::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `open-icon` and `close-icon` props by using this pattern: `i-{collection_name}-{icon_name}` or change it globally in `ui.accordion.default.openIcon` and `ui.accordion.default.closeIcon`.
You can also set them to `null` to hide the icons.
::component-card
---
baseProps:
items:
- label: '1. What is NuxtLabs UI?'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '2. Getting Started'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '3. Theming'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '4. Components'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
props:
openIcon: 'i-heroicons-plus'
closeIcon: 'i-heroicons-minus'
excludedProps:
- openIcon
- closeIcon
---
::
### Multiple
Use the `multiple` prop to to allow multiple elements to be opened at the same time.
::component-card
---
baseProps:
items:
- label: 'What is NuxtLabs UI?'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: 'Getting Started'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: 'Theming'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: 'Components'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
props:
multiple: true
excludedProps:
- defaultOpen
---
::
### Open
Use the `default-open` prop to open all items by default. Works better when the `multiple` prop is set to `true`.
::component-card
---
baseProps:
items:
- label: 'What is NuxtLabs UI?'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: 'Getting Started'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: 'Theming'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: 'Components'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
props:
defaultOpen: true
multiple: true
excludedProps:
- defaultOpen
- multiple
---
::
## Slots
You can use slots to customize the buttons and items content of the Accordion.
### `default`
Use the `#default` slot to customize the trigger buttons. You will have access to the `item`, `index`, `open` properties and `close` method in the slot scope.
::component-example
#default
:accordion-example-default-slot
#code
```vue
<script setup>
const items = [...]
</script>
<template>
<UAccordion :items="items" :ui="{ wrapper: 'flex flex-col w-full' }">
<template #default="{ item, index, open }">
<UButton color="gray" variant="ghost" class="border-b border-gray-200 dark:border-gray-700" :ui="{ rounded :'rounded-none', padding: { sm:'p-3' } }">
<template #leading>
<div class="w-6 h-6 rounded-full bg-primary-500 dark:bg-primary-400 flex items-center justify-center -my-1">
<UIcon :name="item.icon" class="w-4 h-4 text-white dark:text-gray-900" />
</div>
</template>
<span class="truncate">{{ index + 1 }}. {{ item.label }}</span>
<template #trailing>
<UIcon
name="i-heroicons-chevron-right-20-solid"
class="w-5 h-5 ms-auto transform transition-transform duration-200"
:class="[open && 'rotate-90']"
/>
</template>
</UButton>
</template>
</UAccordion>
</template>
```
::
### `item`
Use the `#item` slot to customize the items content or pass a `slot` property to customize a specific item. You will have access to the `item`, `index`, `open` properties and `close` method in the slot scope.
::component-example
#default
:accordion-example-item-slot
#code
```vue
<script setup>
const items = [{
label: 'Getting Started',
icon: 'i-heroicons-information-circle',
defaultOpen: true,
slot: 'getting-started'
}, {
label: 'Installation',
icon: 'i-heroicons-arrow-down-tray',
defaultOpen: true,
slot: 'installation'
}, {
label: 'Theming',
icon: 'i-heroicons-eye-dropper',
defaultOpen: true,
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
}, ...]
</script>
<template>
<UAccordion :items="items">
<template #item="{ item }">
<p class="italic text-gray-900 dark:text-white text-center">
{{ item.description }}
</p>
</template>
<template #getting-started>
<div class="flex flex-col justify-center items-center gap-1">
<NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
<span class="hidden sm:block">NuxtLabs</span><span class="sm:text-primary-500 dark:sm:text-primary-400">UI</span>
</NuxtLink>
<p class="text-sm text-gray-500 dark:text-gray-400">
Fully styled and customizable components for Nuxt.
</p>
</div>
</template>
<template #installation="{ description }">
<div class="flex flex-col justify-center items-center gap-1 mb-4">
<h3 class="text-xl font-bold text-gray-900 dark:text-white">
Installation
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
Install <code>@nuxthq/ui</code> dependency to your project:
</p>
<p>
{{ description }}
</p>
</div>
<div class="flex flex-col items-center">
<code>$ npm install @nuxtlabs/ui</code>
<code>$ nnpm install -D @nuxthq/ui</code>
<code>$ pnpm i -D @nuxthq/ui</code>
</div>
</template>
</UAccordion>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display an icon from Iconify library.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Icon.vue
---
## Usage
@@ -12,7 +15,7 @@ props:
---
::
::alert{icon="i-heroicons-exclamation-triangle"}
::callout{icon="i-heroicons-exclamation-triangle"}
When playing with the `name` prop above, you won't be able to use any icon you want as icons are bundled on build as explained in the [theming section](/getting-started/theming#icons).
::

View File

@@ -1,7 +1,10 @@
---
github: true
title: 'Keyboard Key'
description: Display a keyboard key in a text block.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Kbd.vue
navigation:
title: 'Kbd'
---

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display an input field.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Input.vue
---
## Usage
@@ -218,7 +221,7 @@ const q = ref('')
```
::
::alert{icon="i-heroicons-exclamation-triangle-20-solid"}
::callout{icon="i-heroicons-exclamation-triangle-20-solid"}
As leading and trailing icons are wrapped around a `pointer-events-none` class, if you inject a clickable element in the slot, you need to remove this class to make it clickable by adding `:ui="{ icon: { trailing: { pointer: '' } } }"` to the Input.
::

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display a textarea field.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Textarea.vue
---
## Usage

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display a select field.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Select.vue
---
## Usage
@@ -27,7 +30,9 @@ const country = ref(countries[0])
```
::
When using objects, you can configure which field will be used for display through the `option-attribute` prop that defaults to `label` and which field will be used for comparison through the `value-attribute` prop that defaults to `value`.
When using objects, you can configure which field will be used for display through the `option-attribute` prop that defaults to `label` and which field will be used for comparison through the `value-attribute` prop that defaults to `value`.
Adding a `disabled` key to the objects will control the disabled state of the option.
::component-example
#default
@@ -41,7 +46,8 @@ const countries = [{
value: 'US'
}, {
name: 'Canada',
value: 'CA'
value: 'CA',
disabled: true
}, {
name: 'Mexico',
value: 'MX'
@@ -191,6 +197,8 @@ props:
---
::
Add a `disabled` key with a truthy value to the `options` array of object to disable a single option.
### Loading
Use the `loading` prop to show a loading icon and disable the Input.

View File

@@ -1,9 +1,12 @@
---
github: true
description: Display a select menu with advanced features.
headlessui:
label: 'Listbox'
to: 'https://headlessui.com/vue/listbox'
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/SelectMenu.vue
- label: 'Listbox'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/listbox'
---
## Usage
@@ -122,7 +125,7 @@ excludedProps:
---
::
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
Learn how to customize icons from the [Select](/forms/select#icon) component.
::
@@ -146,6 +149,40 @@ props:
---
::
### Async search
Pass a function to the `searchable` prop to customize the search behavior and filter options according to your needs. The function will receive the query as its first argument and should return an array.
Use the `debounce` prop to adjust the delay of the function.
::component-example
#default
:select-menu-example-async-search{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const search = async (q) => {
const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email })).filter(Boolean)
}
const selected = ref([])
</script>
<template>
<USelectMenu
v-model="selected"
:searchable="search"
placeholder="Search for a user..."
multiple
by="id"
/>
</template>
```
::
## Slots
### `label`

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display a checkbox field.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Checkbox.vue
---
## Usage
@@ -36,7 +39,7 @@ props:
---
::
### Style
### Style :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
Use the `color` prop to change the style of the Checkbox.

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display a radio field.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Radio.vue
---
## Usage
@@ -50,7 +53,7 @@ props:
---
::
### Style
### Style :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
Use the `color` prop to change the style of the Radio.

View File

@@ -1,9 +1,12 @@
---
github: true
description: Display a toggle field.
headlessui:
label: 'Switch'
to: 'https://headlessui.com/vue/switch'
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Toggle.vue
- label: 'Switch'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/switch'
---
## Usage
@@ -26,7 +29,7 @@ const selected = ref(false)
```
::
### Style
### Style :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
Use the `color` prop to change the style of the Toggle.

View File

@@ -1,8 +1,11 @@
---
github: true
description: Display a range field
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Range.vue
navigation:
badge: "Edge"
badge: New
---
## Usage

View File

@@ -1,7 +1,9 @@
---
github:
suffix: .ts
description: Display a label and additional informations around a form element.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/FormGroup.ts
---
@@ -128,7 +130,7 @@ code: >-
You can also use the `error` prop as a boolean to mark the form element as invalid.
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
The `error` prop will automatically set the `color` prop of the form element to `red`.
::

View File

@@ -1,6 +1,9 @@
---
github: true
description: 'Display data in a table.'
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/data/Table.vue
---
## Usage
@@ -72,6 +75,7 @@ Use the `columns` prop to configure which columns to display. It's an array of o
- `key` - The field to display from the row data.
- `sortable` - Whether the column is sortable. Defaults to `false`.
- `direction` - The sort direction to use on first click. Defaults to `asc`.
- `class` - The class to apply to the column cells.
::component-example{class="grid"}
---
@@ -209,7 +213,7 @@ You can specify a default sort for the table through the `sort` prop. It's an ob
- `column` - The column to sort by.
- `direction` - The sort direction. Can be either `asc` or `desc` and defaults to `asc`.
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
This will set the default sort and will work even if no column is set as `sortable`.
::
@@ -288,7 +292,7 @@ Use the `sort-asc-icon` prop to set a different icon or change it globally in `u
Use the `sort-desc-icon` prop to set a different icon or change it globally in `ui.table.default.sortDescIcon`. Defaults to `i-heroicons-bars-arrow-down-20-solid`.
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
You can also customize the entire header cell, read more in the [Slots](#slots) section.
::
@@ -319,10 +323,46 @@ const selected = ref([people[1]])
```
::
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
You can use the `by` prop to compare objects by a field instead of comparing object instances. We've replicated the behavior of Headless UI [Combobox](https://headlessui.com/vue/combobox#binding-objects-as-values).
::
You can alsso add a `select` listener on your Table to make the rows clickable. The function will receive the row as the first argument.
You can use this to navigate to a page, open a modal or even to select the row manually.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-clickable{class="flex-1"}
#code
```vue
<script setup>
const people = [...]
function select (row) {
const index = selected.value.findIndex((item) => item.id === row.id)
if (index === -1) {
selected.value.push(row)
} else {
selected.value.splice(index, 1)
}
}
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" @select="select" />
</template>
```
::
### Searchable
You can easily use the [Input](/forms/input) component to filter the rows.
@@ -401,7 +441,7 @@ const rows = computed(() => {
```
::
### Loading :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
### Loading
Use the `loading` prop to display a loading state.
@@ -499,7 +539,7 @@ The `sort` property is an object with the following properties:
The `on-sort` property is a function that you can call to sort the table and accepts the column as parameter.
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
Even though you can customize the sort button as mentioned in the [Sortable](#sortable) section, you can use this slot to completely override its behavior, with a custom dropdown for example.
::
@@ -566,7 +606,7 @@ const selected = ref([people[1]])
```
::
### `loading-state` :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
### `loading-state`
Use the `#loading-state` slot to customize the loading state.
@@ -605,7 +645,7 @@ const pending = ref(true)
```
::
### `empty-state` :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
### `empty-state`
Use the `#empty-state` slot to customize the empty state.

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display a vertical navigation.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/navigation/VerticalNavigation.vue
---
## Usage

View File

@@ -1,9 +1,12 @@
---
github: true
description: Add a customizable command palette to your app.
headlessui:
label: 'Combobox'
to: 'https://headlessui.com/vue/combobox'
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/navigation/CommandPalette.vue
- label: 'Combobox'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/combobox'
---
## Usage
@@ -278,8 +281,8 @@ You can also highlight the matches in the command by setting the `fuse.fuseOptio
</template>
```
::alert{icon="i-heroicons-light-bulb"}
Try it yourself in this documentation's search by pressing :badge-shortcut{value="meta"} :badge-shortcut{value="K" class="ml-1"}.
::callout{icon="i-heroicons-light-bulb"}
Try it yourself in this documentation's search by pressing :kbd{value="meta"} :kbd{value="K" class="ml-1"}.
::
## Async search
@@ -319,13 +322,13 @@ const groups = computed(() => {
```
::
::alert{icon="i-heroicons-light-bulb"}
::callout{icon="i-heroicons-light-bulb"}
The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behavior by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`.
::
## Slots
### `empty-state` :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
### `empty-state`
Use the `#empty-state` slot to customize the empty state.

View File

@@ -1,8 +1,9 @@
---
github: true
description: Add a pagination to handle pages.
navigation:
badge: 'New'
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/navigation/Pagination.vue
---
## Usage

View File

@@ -1,13 +1,18 @@
---
github: true
description: Display a modal within your application.
headlessui:
label: 'Dialog'
to: 'https://headlessui.com/vue/dialog'
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/Modal.vue
- label: 'Dialog'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/dialog'
---
## Usage
Use a `v-model` to control the Modal state.
::component-example
#default
:modal-example-basic
@@ -64,6 +69,115 @@ const isOpen = ref(false)
```
::
### Disable overlay
Set the `overlay` prop to `false` to disable it.
::component-example
#default
:modal-example-disable-overlay
#code
```vue
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<UModal v-model="isOpen" :overlay="false">
<div class="p-4">
<Placeholder class="h-48" />
</div>
</UModal>
</div>
</template>
```
::
### Disable transition
Set the `transition` prop to `false` to disable it.
::component-example
#default
:modal-example-disable-transition
#code
```vue
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<UModal v-model="isOpen" :transition="false">
<div class="p-4">
<Placeholder class="h-48" />
</div>
</UModal>
</div>
</template>
```
::
### Prevent close
Use the `prevent-close` prop to disable the outside click alongside the `esc` keyboard shortcut.
::component-example
#default
:modal-example-prevent-close
#code
```vue
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<UModal v-model="isOpen" prevent-close>
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Modal
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
</div>
</template>
<Placeholder class="h-32" />
</UCard>
</UModal>
</div>
</template>
```
::
You can still handle the `esc` shortcut yourself by using our [defineShortcuts](/getting-started/shortcuts#defineshortcuts) composable.
```vue
<script setup>
const isOpen = ref(false)
defineShortcuts({
escape: {
usingInput: true,
whenever: [isOpen],
handler: () => { isOpen.value = false }
}
})
</script>
```
## Props
:component-props

View File

@@ -1,16 +1,21 @@
---
github: true
description: Display a dialog that slides in from the edge of the screen.
headlessui:
label: 'Dialog'
to: 'https://headlessui.com/vue/dialog'
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/Slideover.vue
- label: 'Dialog'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/dialog'
---
## Usage
Use a `v-model` to control the Slideover state.
::component-example
#default
:slideover-example
:slideover-example-basic
#code
```vue
<script setup>
@@ -29,6 +34,149 @@ const isOpen = ref(false)
```
::
You can put a [Card](/layout/card) component inside your Slideover to handle forms and take advantage of `header` and `footer` slots:
::component-example
#default
:slideover-example-card
#code
```vue
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen">
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<!-- Content -->
</template>
<!-- Content -->
<template #footer>
<!-- Content -->
</template>
</UCard>
</USlideover>
</div>
</template>
```
::
### Disable overlay
Set the `overlay` prop to `false` to disable it.
::component-example
#default
:slideover-example-disable-overlay
#code
```vue
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen" :overlay="false">
<div class="p-4 flex-1">
<Placeholder class="h-full" />
</div>
</USlideover>
</div>
</template>
```
::
### Disable transition
Set the `transition` prop to `false` to disable it.
::component-example
#default
:slideover-example-disable-transition
#code
```vue
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen" :transition="false">
<div class="p-4 flex-1">
<Placeholder class="h-full" />
</div>
</USlideover>
</div>
</template>
```
::
### Prevent close
Use the `prevent-close` prop to disable the outside click alongside the `esc` keyboard shortcut.
::component-example
#default
:slideover-example-prevent-close
#code
```vue
<script setup>
const isOpen = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen" prevent-close>
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Slideover
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
</div>
</template>
<Placeholder class="h-full" />
</UCard>
</USlideover>
</div>
</template>
```
::
You can still handle the `esc` shortcut yourself by using our [defineShortcuts](/getting-started/shortcuts#defineshortcuts) composable.
```vue
<script setup>
const isOpen = ref(false)
defineShortcuts({
escape: {
usingInput: true,
whenever: [isOpen],
handler: () => { isOpen.value = false }
}
})
</script>
```
## Props
:component-props

View File

@@ -1,9 +1,12 @@
---
github: true
description: Display a non-modal dialog that floats around a trigger element.
headlessui:
label: 'Popover'
to: 'https://headlessui.com/vue/popover'
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/Popover.vue
- label: 'Popover'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/vue/popover'
---
## Usage

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display content that appears on hover next to an element.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/Tooltip.vue
---
## Usage

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display a menu that appears on right click.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/ContextMenu.vue
---
## Usage

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display a toast notification in your app.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/overlays/Notification.vue
---
## Usage

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display a card for content with a header, body and footer.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/layout/Card.vue
---
## Usage

View File

@@ -1,6 +1,9 @@
---
github: true
description: A container lets you center and constrain the width of your content.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/layout/Container.vue
---
## Usage

View File

@@ -1,6 +1,9 @@
---
github: true
description: Display a placeholder while content is loading.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/layout/Skeleton.vue
---
## Usage

View File

@@ -1,27 +1,24 @@
import ui from '../src/module'
import { excludeColors } from '../src/colors'
import colors from 'tailwindcss/colors'
import module from '../src/module'
import { excludeColors } from '../src/colors'
import pkg from '../package.json'
export default defineNuxtConfig({
// @ts-ignore
extends: '@nuxt-themes/ui-kit',
modules: [
ui,
'@vueuse/nuxt',
'@nuxt/content',
'@nuxt/devtools',
'@nuxthq/studio',
module,
'@nuxtjs/google-fonts',
'@nuxtjs/plausible',
'nuxt-lodash',
'nuxt-component-meta'
'@vueuse/nuxt',
'nuxt-component-meta',
'nuxt-lodash'
],
content: {
highlight: {
theme: {
light: 'material-lighter',
default: 'material-default',
dark: 'material-palenight'
},
preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'diff', 'shell', 'markdown', 'yaml', 'bash', 'ini']
runtimeConfig: {
public: {
version: pkg.version
}
},
ui: {
@@ -29,15 +26,21 @@ export default defineNuxtConfig({
icons: ['heroicons', 'simple-icons'],
safelistColors: excludeColors(colors)
},
typescript: {
strict: false,
includeWorkspace: true
googleFonts: {
families: {
Inter: [400, 500, 600, 700]
}
},
routeRules: {
'/': { redirect: '/getting-started' }
'/': { redirect: '/getting-started', prerender: false }
},
generate: {
routes: ['/getting-started']
nitro: {
prerender: {
routes: ['/getting-started']
}
},
experimental: {
payloadExtraction: true
},
componentMeta: {
metaFields: {
@@ -46,5 +49,9 @@ export default defineNuxtConfig({
events: false,
exposed: false
}
},
typescript: {
strict: false,
includeWorkspace: true
}
})

22
docs/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "@nuxthq/ui-docs",
"private": true,
"devDependencies": {
"@iconify-json/heroicons": "latest",
"@iconify-json/simple-icons": "latest",
"@nuxt-themes/ui-kit": "npm:@nuxt-themes/ui-kit-edge@0.0.1-28161530.eabde2d",
"@nuxt/content": "^2.7.0",
"@nuxt/devtools": "^0.6.7",
"@nuxt/eslint-config": "^0.1.1",
"@nuxthq/studio": "^0.13.4",
"@nuxtjs/google-fonts": "^3.0.1",
"@nuxtjs/plausible": "^0.2.1",
"@vueuse/nuxt": "^10.2.1",
"eslint": "^8.45.0",
"nuxt": "^3.6.3",
"nuxt-component-meta": "^0.5.3",
"nuxt-lodash": "^2.5.0",
"typescript": "^5.1.6",
"v-calendar": "^3.0.3"
}
}

View File

@@ -1,43 +1,17 @@
<template>
<div v-if="page" class="grid lg:grid-cols-10 lg:gap-8">
<div class="pt-8 pb-16" :class="page.body?.toc ? 'lg:col-span-8' : 'lg:col-span-10'">
<DocsPageHeader :page="page" />
<ContentRenderer v-if="page.body" :value="page" class="prose prose-primary dark:prose-invert max-w-none" />
<DocsPageFooter :page="page" class="mt-12" />
<hr class="border-gray-200 dark:border-gray-800 my-6">
<DocsPrevNext :prev="prev" :next="next" />
<DocsFooter class="mt-16" />
</div>
<DocsToc v-if="page.body?.toc" :toc="page.body.toc" class="lg:col-span-2" />
</div>
<div v-else class="flex-1 flex flex-col items-center justify-center">
<div class="text-center">
<p class="text-base font-semibold text-primary-500 dark:text-primary-400">
404
</p>
<h1 class="mt-2 text-4xl tracking-tight font-extrabold u-text-gray-900 sm:text-5xl">
Page not found
</h1>
<p class="mt-2 text-base u-text-gray-500">
Sorry, we couldnt find the page youre looking for.
</p>
<div class="mt-6">
<NuxtLink to="/" class="text-base font-medium text-primary-500 dark:text-primary-400 hover:u-text-gray-900">
Go back home
<span aria-hidden="true"> &rarr;</span>
</NuxtLink>
</div>
</div>
<UDocsPage v-if="page" :page="page" :surround="(surround as ParsedContent[])">
<template #footer>
<Footer />
</template>
</UDocsPage>
<div v-else class="pt-8">
Page not found
</div>
</template>
<script setup lang="ts">
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
const route = useRoute()
const { data: page } = await useAsyncData(`docs-${route.path}`, () => queryContent(route.path).findOne())
@@ -47,7 +21,10 @@ const { data: surround } = await useAsyncData(`docs-${route.path}-surround`, ()
.findSurround(route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
)
const [prev, next] = surround.value
if (process.server && !page.value) {
const event = useRequestEvent()
setResponseStatus(event, 404)
}
useContentHead(page)
</script>

View File

@@ -1,4 +1,4 @@
import { hexToRgb } from "../../src/runtime/utils"
import { hexToRgb } from '../../src/runtime/utils'
import colors from '#tailwind-config/theme/colors'
export default defineNuxtPlugin({

View File

@@ -1,185 +1,11 @@
import type { Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
export default <Partial<Config>> {
content: [
'docs/content/**/*.md'
],
export default <Partial<Config>>{
theme: {
extend: {
fontFamily: {
sans: ['Inter var', ...defaultTheme.fontFamily.sans]
},
maxWidth: {
'8xl': '90rem'
},
typography: (theme) => {
return {
DEFAULT: {
css: {
'h1, h2, h3, h4': {
fontWeight: theme('fontWeight.bold'),
'scroll-margin-top': 'var(--scroll-mt)'
},
'h1 a, h2 a, h3 a, h4 a': {
borderBottom: 'none !important',
color: 'inherit',
fontWeight: 'inherit'
},
a: {
fontWeight: theme('fontWeight.medium'),
textDecoration: 'none',
borderBottom: '1px solid transparent'
},
'a:hover': {
borderColor: 'var(--tw-prose-links)'
},
'a:has(> code)': {
borderColor: 'transparent !important'
},
'a code': {
color: 'var(--tw-prose-code)',
border: '1px dashed var(--tw-prose-pre-border)'
},
'a:hover code': {
color: 'var(--tw-prose-links)',
borderColor: 'var(--tw-prose-links)'
},
pre: {
margin: '0',
borderRadius: '0.375rem',
border: '1px solid var(--tw-prose-pre-border)',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
},
code: {
backgroundColor: 'var(--tw-prose-pre-bg)',
padding: '0.25rem 0.375rem',
borderRadius: '0.375rem',
border: '1px solid var(--tw-prose-pre-border)'
},
'code::before': {
content: ''
},
'code::after': {
content: ''
},
'blockquote p:first-of-type::before': {
content: ''
},
'blockquote p:last-of-type::after': {
content: ''
},
'input[type="checkbox"]': {
color: 'rgb(var(--color-primary-500))',
borderRadius: theme('borderRadius.DEFAULT'),
borderColor: 'rgb(var(--color-gray-300))',
height: theme('spacing.4'),
width: theme('spacing.4'),
marginTop: '-3.5px !important',
marginBottom: '0 !important',
'&:focus': {
'--tw-ring-offset-width': 0
}
},
'input[type="checkbox"]:checked': {
borderColor: 'rgb(var(--color-primary-500))'
},
'input[type="checkbox"]:disabled': {
opacity: 0.5,
cursor: 'not-allowed'
},
'ul.contains-task-list': {
marginLeft: '-1.625em'
},
'ul ul': {
paddingLeft: theme('padding.6')
},
'ul ol': {
paddingLeft: theme('padding.6')
},
'ul > li.task-list-item': {
paddingLeft: '0 !important'
},
'ul > li.task-list-item input': {
marginRight: '7px'
},
'ul > li.task-list-item > ul.contains-task-list': {
marginLeft: 'initial'
},
'ul > li.task-list-item a': {
marginBottom: 0
},
'ul > li.task-list-item::marker': {
content: 'none'
},
'ul > li > p': {
margin: 0
},
'ul > li > span.issue-badge, p > span.issue-badge': {
verticalAlign: 'text-top',
margin: '0 !important'
},
'ul > li > button': {
verticalAlign: 'baseline !important'
},
table: {
wordBreak: 'break-all'
}
}
},
primary: {
css: {
'--tw-prose-body': 'rgb(var(--color-gray-700))',
'--tw-prose-headings': 'rgb(var(--color-gray-900))',
'--tw-prose-lead': 'rgb(var(--color-gray-600))',
'--tw-prose-links': 'rgb(var(--color-primary-500))',
'--tw-prose-bold': 'rgb(var(--color-gray-900))',
'--tw-prose-counters': 'rgb(var(--color-gray-500))',
'--tw-prose-bullets': 'rgb(var(--color-gray-300))',
'--tw-prose-hr': 'rgb(var(--color-gray-100))',
'--tw-prose-quotes': 'rgb(var(--color-gray-900))',
'--tw-prose-quote-borders': 'rgb(var(--color-gray-200))',
'--tw-prose-captions': 'rgb(var(--color-gray-500))',
'--tw-prose-code': 'rgb(var(--color-gray-900))',
'--tw-prose-pre-code': 'rgb(var(--color-gray-900))',
'--tw-prose-pre-bg': 'rgb(var(--color-gray-50))',
'--tw-prose-pre-border': 'rgb(var(--color-gray-200))',
'--tw-prose-th-borders': 'rgb(var(--color-gray-300))',
'--tw-prose-td-borders': 'rgb(var(--color-gray-200))',
'--tw-prose-invert-body': 'rgb(var(--color-gray-200))',
'--tw-prose-invert-headings': theme('colors.white'),
'--tw-prose-invert-lead': 'rgb(var(--color-gray-400))',
'--tw-prose-invert-links': 'rgb(var(--color-primary-400))',
'--tw-prose-invert-bold': theme('colors.white'),
'--tw-prose-invert-counters': 'rgb(var(--color-gray-400))',
'--tw-prose-invert-bullets': 'rgb(var(--color-gray-600))',
'--tw-prose-invert-hr': 'rgb(var(--color-gray-800))',
'--tw-prose-invert-quotes': 'rgb(var(--color-gray-100))',
'--tw-prose-invert-quote-borders': 'rgb(var(--color-gray-700))',
'--tw-prose-invert-captions': 'rgb(var(--color-gray-400))',
'--tw-prose-invert-code': theme('colors.white'),
'--tw-prose-invert-pre-code': theme('colors.white'),
'--tw-prose-invert-pre-bg': 'rgb(var(--color-gray-800))',
'--tw-prose-invert-pre-border': 'rgb(var(--color-gray-700))',
'--tw-prose-invert-th-borders': 'rgb(var(--color-gray-700))',
'--tw-prose-invert-td-borders': 'rgb(var(--color-gray-800))'
}
},
invert: {
css: {
'--tw-prose-pre-border': 'var(--tw-prose-invert-pre-border)',
'input[type="checkbox"]': {
backgroundColor: 'rgb(var(--color-gray-800))',
borderColor: 'rgb(var(--color-gray-700))'
},
'input[type="checkbox"]:checked': {
backgroundColor: 'rgb(var(--color-primary-400))',
borderColor: 'rgb(var(--color-primary-400))'
}
}
}
}
sans: ['Inter', ...defaultTheme.fontFamily.sans]
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@nuxthq/ui",
"version": "2.5.0",
"version": "2.6.0",
"repository": "https://github.com/nuxtlabs/ui",
"license": "MIT",
"exports": {
@@ -23,6 +23,7 @@
"dev": "nuxi dev docs",
"build:docs": "nuxi generate docs",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "nuxi typecheck",
"prepare": "nuxi prepare docs",
"release": "release-it"
@@ -31,41 +32,31 @@
"@egoist/tailwindcss-icons": "^1.1.0",
"@headlessui/vue": "^1.7.14",
"@iconify-json/heroicons": "^1.1.11",
"@nuxt/kit": "^3.6.1",
"@nuxt/kit": "^3.6.3",
"@nuxtjs/color-mode": "^3.3.0",
"@nuxtjs/tailwindcss": "^6.8.0",
"@popperjs/core": "^2.11.8",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/forms": "^0.5.4",
"@tailwindcss/typography": "^0.5.9",
"@vueuse/core": "^10.2.0",
"@vueuse/integrations": "^10.2.0",
"@vueuse/math": "^10.2.0",
"@vueuse/core": "^10.2.1",
"@vueuse/integrations": "^10.2.1",
"@vueuse/math": "^10.2.1",
"defu": "^6.1.2",
"fuse.js": "^6.6.2",
"lodash-es": "^4.17.21",
"tailwindcss": "^3.3.2"
"tailwindcss": "^3.3.3"
},
"devDependencies": {
"@iconify-json/simple-icons": "^1.1.58",
"@nuxt/content": "^2.7.0",
"@nuxt/devtools": "^0.6.4",
"@nuxt/eslint-config": "^0.1.1",
"@nuxt/module-builder": "^0.4.0",
"@nuxthq/studio": "^0.13.2",
"@nuxtjs/plausible": "^0.2.1",
"@release-it/conventional-changelog": "^5.1.1",
"@types/lodash-es": "^4.17.7",
"@types/node": "^20.3.2",
"@vueuse/nuxt": "^10.2.0",
"eslint": "^8.43.0",
"nuxt": "^3.6.1",
"nuxt-component-meta": "^0.5.3",
"nuxt-lodash": "^2.5.0",
"release-it": "^15.11.0",
"@release-it/conventional-changelog": "^7.0.0",
"eslint": "^8.45.0",
"nuxt": "^3.6.3",
"release-it": "^16.1.2",
"unbuild": "^1.2.1",
"v-calendar": "^3.0.3",
"vue-tsc": "^1.8.2"
"typescript": "^5.1.6",
"vue-tsc": "^1.8.5"
}
}

3258
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
packages:
- "docs"
- "./"

View File

@@ -50,7 +50,7 @@ const safelistByComponent = {
}],
button: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-50`),
variants: ['hover']
variants: ['hover', 'disabled']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-100`),
variants: ['hover']
@@ -68,13 +68,13 @@ const safelistByComponent = {
variants: ['dark:hover']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-950`),
variants: ['dark', 'dark:hover']
variants: ['dark', 'dark:hover', 'dark:disabled']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
variants: ['dark', 'dark:disabled']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`),
variants: ['dark:hover']
variants: ['dark:hover', 'disabled']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-600`),
variants: ['hover']

View File

@@ -137,7 +137,7 @@ export default defineNuxtModule<ModuleOptions>({
config: {
darkMode: 'class',
plugins: [
require("@tailwindcss/forms")({ strategy: 'class' }),
require('@tailwindcss/forms')({ strategy: 'class' }),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/typography'),
require('@tailwindcss/container-queries')

View File

@@ -8,7 +8,8 @@ const table = {
tbody: 'divide-y divide-gray-200 dark:divide-gray-800',
tr: {
base: '',
selected: 'bg-gray-50 dark:bg-gray-800/50'
selected: 'bg-gray-50 dark:bg-gray-800/50',
active: 'hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer'
},
th: {
base: 'text-left rtl:text-right',
@@ -182,10 +183,10 @@ const button = {
},
variant: {
solid: 'shadow-sm text-white dark:text-gray-900 bg-{color}-500 hover:bg-{color}-600 disabled:bg-{color}-500 dark:bg-{color}-400 dark:hover:bg-{color}-500 dark:disabled:bg-{color}-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-{color}-500 dark:focus-visible:outline-{color}-400',
outline: 'ring-1 ring-inset ring-current text-{color}-500 dark:text-{color}-400 hover:bg-{color}-50 dark:hover:bg-{color}-950 focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
soft: 'text-{color}-500 dark:text-{color}-400 bg-{color}-50 hover:bg-{color}-100 dark:bg-{color}-950 dark:hover:bg-{color}-900 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
ghost: 'text-{color}-500 dark:text-{color}-400 hover:bg-{color}-50 dark:hover:bg-{color}-950 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
link: 'text-{color}-500 hover:text-{color}-600 dark:text-{color}-400 dark:hover:text-{color}-500 underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400'
outline: 'ring-1 ring-inset ring-current text-{color}-500 dark:text-{color}-400 hover:bg-{color}-50 disabled:bg-transparent dark:hover:bg-{color}-950 dark:disabled:bg-transparent focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
soft: 'text-{color}-500 dark:text-{color}-400 bg-{color}-50 hover:bg-{color}-100 disabled:bg-{color}-50 dark:bg-{color}-950 dark:hover:bg-{color}-900 dark:disabled:bg-{color}-950 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
ghost: 'text-{color}-500 dark:text-{color}-400 hover:bg-{color}-50 disabled:bg-transparent dark:hover:bg-{color}-950 dark:disabled:bg-transparent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
link: 'text-{color}-500 hover:text-{color}-600 disabled:text-{color}-500 dark:text-{color}-400 dark:hover:text-{color}-500 dark:disabled:text-{color}-400 underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400'
},
icon: {
base: 'flex-shrink-0',
@@ -248,7 +249,7 @@ const dropdown = {
enterActiveClass: 'transition duration-100 ease-out',
enterFromClass: 'transform scale-95 opacity-0',
enterToClass: 'transform scale-100 opacity-100',
leaveActiveClass: 'transition duration-75 ease-out',
leaveActiveClass: 'transition duration-75 ease-in',
leaveFromClass: 'transform scale-100 opacity-100',
leaveToClass: 'transform scale-95 opacity-0'
},
@@ -258,6 +259,27 @@ const dropdown = {
}
}
const accordion = {
wrapper: 'w-full flex flex-col',
item: {
base: '',
size: 'text-sm',
color: 'text-gray-500 dark:text-gray-400',
padding: 'pt-1.5 pb-3',
icon: 'ms-auto transform transition-transform duration-200'
},
transition: {
enterActiveClass: 'overflow-hidden transition-[height] duration-200 ease-out',
leaveActiveClass: 'overflow-hidden transition-[height] duration-200 ease-out'
},
default: {
openIcon: 'i-heroicons-chevron-down-20-solid',
closeIcon: '',
class: 'mb-1.5 w-full',
variant: 'soft'
}
}
const kbd = {
base: 'inline-flex items-center justify-center text-gray-900 dark:text-white',
padding: 'px-1',
@@ -387,7 +409,7 @@ const formGroup = {
label: {
wrapper: 'flex content-center justify-between',
base: 'block text-sm font-medium text-gray-700 dark:text-gray-200',
required: `after:content-['*'] after:ms-0.5 after:text-red-500 dark:after:text-red-400`
required: 'after:content-[\'*\'] after:ms-0.5 after:text-red-500 dark:after:text-red-400'
},
description: 'text-sm text-gray-500 dark:text-gray-400',
container: 'mt-1 relative',
@@ -531,17 +553,17 @@ const toggle = {
const range = {
wrapper: 'relative w-full',
base: 'w-full absolute appearance-none cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none [&::-webkit-slider-runnable-track]:h-full [&::-moz-slider-runnable-track]:h-full',
base: 'w-full absolute appearance-none cursor-pointer disabled:cursor-not-allowed disabled:bg-opacity-50 focus:outline-none [&::-webkit-slider-runnable-track]:h-full [&::-moz-slider-runnable-track]:h-full peer',
background: 'bg-gray-200 dark:bg-gray-700',
rounded: 'rounded-lg',
ring: 'focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
progress: {
base: 'absolute inset-0 h-full pointer-events-none',
base: 'absolute inset-0 h-full pointer-events-none peer-disabled:bg-opacity-50',
rounded: 'rounded-s-lg',
background: 'bg-{color}-500 dark:bg-{color}-400'
},
thumb: {
base: `[&::-webkit-slider-thumb]:relative [&::-moz-range-thumb]:relative [&::-webkit-slider-thumb]:z-[1] [&::-moz-range-thumb]:z-[1] [&::-webkit-slider-thumb]:appearance-none [&::-moz-range-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0`,
base: '[&::-webkit-slider-thumb]:relative [&::-moz-range-thumb]:relative [&::-webkit-slider-thumb]:z-[1] [&::-moz-range-thumb]:z-[1] [&::-webkit-slider-thumb]:appearance-none [&::-moz-range-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0',
color: 'text-{color}-500 dark:text-{color}-400',
background: '[&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:dark:bg-gray-900 [&::-moz-range-thumb]:bg-current',
ring: '[&::-webkit-slider-thumb]:ring-2 [&::-webkit-slider-thumb]:ring-current',
@@ -926,6 +948,7 @@ export default {
buttonGroup,
dropdown,
kbd,
accordion,
input,
formGroup,
textarea,

View File

@@ -4,10 +4,10 @@
<thead :class="ui.thead">
<tr :class="ui.tr.base">
<th v-if="modelValue" scope="col" class="ps-4">
<UCheckbox :checked="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" @change="selected = $event.target.checked ? rows : []" />
<UCheckbox :checked="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" @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]">
<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]">
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
<UButton
v-if="column.sortable"
@@ -22,9 +22,9 @@
</tr>
</thead>
<tbody :class="ui.tbody">
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected]">
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected, $attrs.onSelect && ui.tr.active]" @click="() => onSelect(row)">
<td v-if="modelValue" class="ps-4">
<UCheckbox v-model="selected" :value="row" />
<UCheckbox v-model="selected" :value="row" @click.stop />
</td>
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]">
@@ -69,6 +69,7 @@ import { ref, computed, defineComponent, toRaw } from 'vue'
import type { PropType } from 'vue'
import { capitalize, orderBy } from 'lodash-es'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import type { Button } from '../../types/button'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -92,11 +93,11 @@ export default defineComponent({
default: () => defaultComparator
},
rows: {
type: Array as PropType<{ [key: string]: any }[]>,
type: Array as PropType<{ [key: string]: any, click?: Function }[]>,
default: () => []
},
columns: {
type: Array as PropType<{ key: string, sortable?: boolean, [key: string]: any }[]>,
type: Array as PropType<{ key: string, sortable?: boolean, direction?: 'asc' | 'desc', class?: string, [key: string]: any }[]>,
default: null
},
columnAttribute: {
@@ -137,13 +138,13 @@ export default defineComponent({
}
},
emits: ['update:modelValue'],
setup (props, { emit }) {
setup (props, { emit, attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.table>>(() => defu({}, props.ui, appConfig.ui.table))
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: capitalize(key), sortable: false })))
const columns = computed(() => props.columns ?? Object.keys(omit(props.rows[0] ?? {}, ['click'])).map((key) => ({ key, label: capitalize(key), sortable: false })))
const sort = ref(defu({}, props.sort, { column: null, direction: 'asc' }))
@@ -186,7 +187,7 @@ export default defineComponent({
return selected.value.some((item) => compare(toRaw(item), toRaw(row)))
}
function onSort (column) {
function onSort (column: { key: string, direction?: 'asc' | 'desc' }) {
if (sort.value.column === column.key) {
const direction = !column.direction || column.direction === 'asc' ? 'desc' : 'asc'
@@ -200,6 +201,34 @@ export default defineComponent({
}
}
function onSelect (row) {
if (!attrs.onSelect) {
return
}
// @ts-ignore
attrs.onSelect(row)
}
function selectAllRows () {
props.rows.forEach((row) => {
// If the row is already selected, don't select it again
if (selected.value.some((item) => compare(toRaw(item), toRaw(row)))) {
return
}
onSelect(row)
})
}
function onChange (event: any) {
if (event.target.checked) {
selectAllRows()
} else {
selected.value = []
}
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
@@ -214,7 +243,9 @@ export default defineComponent({
// eslint-disable-next-line vue/no-dupe-keys
emptyState,
isSelected,
onSort
onSort,
onSelect,
onChange
}
}
})

View File

@@ -0,0 +1,152 @@
<template>
<div :class="ui.wrapper">
<HDisclosure v-for="(item, index) in items" v-slot="{ open, close }" :key="index" :default-open="defaultOpen || item.defaultOpen">
<HDisclosureButton :ref="() => buttonRefs[index] = close" as="template" :disabled="item.disabled">
<slot :item="item" :index="index" :open="open" :close="close">
<UButton v-bind="{ ...omit(ui.default, ['openIcon', 'closeIcon']), ...$attrs, ...omit(item, ['slot', 'disabled', 'content', 'defaultOpen']) }">
<template #trailing>
<UIcon
:name="!open ? openIcon : closeIcon ? closeIcon : openIcon"
:class="[
open && !closeIcon ? '-rotate-180' : '',
uiButton.icon.size[item.size || uiButton.default.size],
ui.item.icon
]"
/>
</template>
</UButton>
</slot>
</HDisclosureButton>
<StateEmitter :open="open" @open="closeOthers(index)" />
<Transition
v-bind="ui.transition"
@enter="onEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@leave="onLeave"
>
<div v-show="open">
<HDisclosurePanel :class="[ui.item.base, ui.item.size, ui.item.color, ui.item.padding]" static>
<slot :name="item.slot || 'item'" :item="item" :index="index" :open="open" :close="close">
{{ item.content }}
</slot>
</HDisclosurePanel>
</div>
</Transition>
</HDisclosure>
</div>
</template>
<script lang="ts">
import { ref, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { Disclosure as HDisclosure, DisclosureButton as HDisclosureButton, DisclosurePanel as HDisclosurePanel } from '@headlessui/vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import StateEmitter from '../../utils/StateEmitter'
import type { AccordionItem } from '../../types/accordion'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
export default defineComponent({
components: {
HDisclosure,
HDisclosureButton,
HDisclosurePanel,
UIcon,
UButton,
StateEmitter
},
inheritAttrs: false,
props: {
items: {
type: Array as PropType<AccordionItem[]>,
default: () => []
},
defaultOpen: {
type: Boolean,
default: false
},
openIcon: {
type: String,
default: () => appConfig.ui.accordion.default.openIcon
},
closeIcon: {
type: String,
default: () => appConfig.ui.accordion.default.closeIcon
},
multiple: {
type: Boolean,
default: false
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.accordion>>,
default: () => appConfig.ui.accordion
}
},
setup (props) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.accordion>>(() => defu({}, props.ui, appConfig.ui.accordion))
const uiButton = computed<Partial<typeof appConfig.ui.button>>(() => appConfig.ui.button)
const buttonRefs = ref<Function[]>([])
function closeOthers (itemIndex: number) {
if (!props.items[itemIndex].closeOthers && props.multiple) {
return
}
buttonRefs.value.forEach((close, index) => {
if (index === itemIndex) return
close()
})
}
function onEnter (el: HTMLElement, done) {
el.style.height = '0'
el.offsetHeight // Trigger a reflow, flushing the CSS changes
el.style.height = el.scrollHeight + 'px'
el.addEventListener('transitionend', done, { once: true })
}
function onBeforeLeave (el: HTMLElement) {
el.style.height = el.scrollHeight + 'px'
el.offsetHeight // Trigger a reflow, flushing the CSS changes
}
function onAfterEnter (el: HTMLElement) {
el.style.height = 'auto'
}
function onLeave (el: HTMLElement, done) {
el.style.height = '0'
el.addEventListener('transitionend', done, { once: true })
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
uiButton,
buttonRefs,
closeOthers,
omit,
onEnter,
onBeforeLeave,
onAfterEnter,
onLeave
}
}
})
</script>

View File

@@ -1,6 +1,13 @@
<template>
<span :class="wrapperClass">
<img v-if="url && !error" :class="avatarClass" :src="url" :alt="alt" :onerror="() => onError()">
<img
v-if="url && !error"
:class="avatarClass"
:alt="alt"
:src="url"
v-bind="$attrs"
:onerror="() => onError()"
>
<span v-else-if="text || placeholder" :class="ui.placeholder">{{ text || placeholder }}</span>
<span v-if="chipColor" :class="chipClass">
@@ -23,6 +30,7 @@ import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
inheritAttrs: false,
props: {
src: {
type: [String, Boolean],

View File

@@ -50,9 +50,9 @@ export default defineComponent({
vProps.size = props.size
}
vProps.class = node.props.class || ''
vProps.class = node.props?.class || ''
vProps.class += ' !shadow-none'
vProps.ui = node.props.ui || {}
vProps.ui = node.props?.ui || {}
vProps.ui.rounded = ''
if (index === 0) {

View File

@@ -47,7 +47,6 @@
<script lang="ts">
import { defineComponent, ref, computed, onMounted } from 'vue'
import type { PropType } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
@@ -56,7 +55,7 @@ import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue'
import ULinkCustom from '../elements/LinkCustom.vue'
import { usePopper } from '../../composables/usePopper'
import type { Avatar } from '../../types/avatar'
import type { DropdownItem } from '../../types/dropdown'
import type { PopperOptions } from '../../types'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -78,18 +77,7 @@ export default defineComponent({
},
props: {
items: {
type: Array as PropType<{
to?: string | RouteLocationRaw
exact?: boolean
label: string
slot?: string
icon?: string
iconClass?: string
avatar?: Partial<Avatar>
shortcuts?: string[]
disabled?: boolean
click?: Function
}[][]>,
type: Array as PropType<DropdownItem[][]>,
default: () => []
},
mode: {

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