mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
feat: rewrite to use app config and rework docs (#143)
Co-authored-by: Daniel Roe <daniel@roe.dev> Co-authored-by: Sébastien Chopin <seb@nuxt.com>
This commit is contained in:
6
.github/workflows/ci-dev.yml
vendored
6
.github/workflows/ci-dev.yml
vendored
@@ -15,10 +15,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest] # macos-latest, windows-latest
|
||||
node: [16]
|
||||
node: [18]
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -15,10 +15,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest] # macos-latest, windows-latest
|
||||
node: [16]
|
||||
node: [18]
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
|
||||
|
||||
42
README.md
42
README.md
@@ -1,6 +1,6 @@
|
||||
# @nuxthq/ui
|
||||
|
||||
Components library as a Nuxt module using [TailwindCSS](https://tailwindcss.com).
|
||||
Components library as a Nuxt module using [TailwindCSS](https://tailwindcss.com) and [HeadlessUI](https://headlessui.com).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -29,43 +29,3 @@ If you want latest updates, please use `@nuxthq/ui-edge` in your `package.json`:
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
- `primary`
|
||||
|
||||
Define the primary variant. Defaults to `indigo`. You can specify your own object of colors like here:
|
||||
|
||||
**Example:**
|
||||
|
||||
```js
|
||||
import { defineNuxtConfig } from 'nuxt'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
buildModules: [
|
||||
'@nuxthq/ui'
|
||||
],
|
||||
ui: {
|
||||
primary: 'blue'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- `prefix`
|
||||
|
||||
Define the prefix of the imported components. Defaults to `u`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```js
|
||||
import { defineNuxtConfig } from 'nuxt'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
buildModules: [
|
||||
'@nuxthq/ui'
|
||||
],
|
||||
ui: {
|
||||
prefix: 'tw'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { defineBuildConfig } from 'unbuild'
|
||||
|
||||
export default defineBuildConfig({
|
||||
hooks: {
|
||||
'rollup:done': async (ctx) => {
|
||||
// copy env.d.ts to dist
|
||||
await fs.copyFile(resolve('src/env.d.ts'), join(ctx.options.outDir, 'env.d.ts'))
|
||||
}
|
||||
}
|
||||
})
|
||||
100
docs/app.vue
100
docs/app.vue
@@ -1,95 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<nav class="u-bg-white border-b u-border-gray-200 fixed top-0 inset-x-0 z-10">
|
||||
<UContainer padded>
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<NuxtLink to="/" class="block font-bold text-lg u-text-gray-900">
|
||||
@nuxthq/ui
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<Header />
|
||||
|
||||
<div class="flex items-center">
|
||||
<ColorScheme placeholder="" tag="span">
|
||||
<UButton variant="transparent" :icon="colorMode.value === 'dark' ? 'i-heroicons-moon' : 'i-heroicons-sun'" @click="toggleDark" />
|
||||
</ColorScheme>
|
||||
<UButton to="https://github.com/nuxtlabs/ui" target="_blank" variant="transparent" icon="i-mdi-github" />
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</nav>
|
||||
|
||||
<UContainer class="mt-16">
|
||||
<div class="lg:grid lg:grid-cols-10 lg:gap-10 lg:relative">
|
||||
<aside class="lg:flex lg:flex-col pb-8 lg:pb-0 lg:sticky lg:top-0 px-4 sm:px-6 lg:px-0 lg:pt-16 lg:-mt-16 lg:self-start lg:col-span-2 lg:overflow-hidden lg:h-screen">
|
||||
<nav class="overflow-y-auto h-auto pt-8 lg:py-12">
|
||||
<ul class="space-y-6">
|
||||
<li v-for="section of sections" :key="section">
|
||||
<h5 class="mb-3 uppercase tracking-wide font-semibold text-xs u-text-gray-900">
|
||||
{{ section.label }}
|
||||
</h5>
|
||||
<ul class="space-y-1.5">
|
||||
<li v-for="(link, index) of section.links" :key="index">
|
||||
<ULink
|
||||
:to="link.to"
|
||||
class="relative block text-sm rounded-md"
|
||||
active-class="text-primary-600"
|
||||
inactive-class="u-text-gray-500 hover:u-text-gray-700"
|
||||
exact
|
||||
>
|
||||
{{ link.label }}
|
||||
</ULink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="space-y-6 sm:px-6 lg:px-0 lg:col-span-8 lg:py-12">
|
||||
<UContainer>
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
|
||||
<ClientOnly>
|
||||
<DocsSearch />
|
||||
|
||||
<UNotifications />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
const colorScheme = usePreferredColorScheme()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
// Computed
|
||||
|
||||
const href = computed(() => colorScheme.value === 'dark' ? '/icon-dark.svg' : '/icon-light.svg')
|
||||
const color = computed(() => colorMode.value === 'dark' ? '#18181b' : 'white')
|
||||
|
||||
// Head
|
||||
|
||||
useHead({
|
||||
title: '@nuxthq/ui',
|
||||
titleTemplate: title => title && title !== 'nuxthq/ui' ? `${title} - nuxthq/ui` : 'nuxthq/ui',
|
||||
meta: [
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' }
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' },
|
||||
{ key: 'theme-color', name: 'theme-color', content: color }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' }
|
||||
{ rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' },
|
||||
{ rel: 'icon', type: 'image/svg+xml', href }
|
||||
],
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
},
|
||||
bodyAttrs: {
|
||||
class: 'antialiased font-sans text-gray-700 bg-gray-50 dark:bg-gray-900 dark:text-gray-200 bg-white dark:bg-black'
|
||||
class: 'antialiased font-sans text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-900'
|
||||
}
|
||||
})
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const toggleDark = () => {
|
||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ label: 'Getting Started', links: [{ label: 'Usage', to: '/' }, { label: 'Examples', to: '/examples' }, { label: 'Migration', to: '/migration' }, { label: 'Dark mode', to: '/dark' }] },
|
||||
{ label: 'Elements', links: [{ label: 'Avatar', to: '/components/Avatar' }, { label: 'AvatarGroup', to: '/components/AvatarGroup' }, { label: 'Badge', to: '/components/Badge' }, { label: 'Button', to: '/components/Button' }, { label: 'Dropdown', to: '/components/Dropdown' }] },
|
||||
{ label: 'Feedback', links: [{ label: 'Alert', to: '/components/Alert' }, { label: 'AlertDialog', to: '/components/AlertDialog' }] },
|
||||
{ label: 'Forms', links: [{ label: 'Checkbox', to: '/components/Checkbox' }, { label: 'Input', to: '/components/Input' }, { label: 'FormGroup', to: '/components/FormGroup' }, { label: 'Radio', to: '/components/Radio' }, { label: 'Select', to: '/components/Select' }, { label: 'SelectCustom', to: '/components/SelectCustom' }, { label: 'Textarea', to: '/components/Textarea' }, { label: 'Toggle', to: '/components/Toggle' }] },
|
||||
{ label: 'Layout', links: [{ label: 'Card', to: '/components/Card' }, { label: 'Container', to: '/components/Container' }] },
|
||||
{ label: 'Navigation', links: [{ label: 'Pills', to: '/components/Pills' }, { label: 'Tabs', to: '/components/Tabs' }, { label: 'VerticalNavigation', to: '/components/VerticalNavigation' }, { label: 'CommandPalette', to: '/components/CommandPalette' }] },
|
||||
{ label: 'Overlays', links: [{ label: 'ContextMenu', to: '/components/ContextMenu' }, { label: 'Modal', to: '/components/Modal' }, { label: 'Notification', to: '/components/Notification' }, { label: 'Popover', to: '/components/Popover' }, { label: 'Slideover', to: '/components/Slideover' }, { label: 'Tooltip', to: '/components/Tooltip' }] }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html.dark {
|
||||
@apply bg-black;
|
||||
}
|
||||
</style>
|
||||
|
||||
48
docs/app/router.options.ts
Normal file
48
docs/app/router.options.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
112
docs/components/Header.vue
Normal file
112
docs/components/Header.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<header class="sticky top-0 z-50 w-full backdrop-blur flex-none border-b border-gray-900/10 dark:border-gray-50/[0.06] bg-white/75 dark:bg-gray-900/75">
|
||||
<UContainer>
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<div class="flex items-center gap-3">
|
||||
<NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-gray-900 dark:text-white">
|
||||
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
|
||||
|
||||
nuxthq/ui
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center -mr-1.5">
|
||||
<div class="mr-1.5 hidden lg:block">
|
||||
<ThemeSelect />
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
class="lg:hidden"
|
||||
icon="i-heroicons-magnifying-glass-20-solid"
|
||||
@click="openDocsSearch"
|
||||
/>
|
||||
|
||||
<ClientOnly>
|
||||
<UButton
|
||||
:icon="isDark ? 'i-heroicons-moon' : 'i-heroicons-sun'"
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
aria-label="Theme"
|
||||
@click="isDark = !isDark"
|
||||
/>
|
||||
</ClientOnly>
|
||||
<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="i-heroicons-bars-3-20-solid"
|
||||
@click="isDialogOpen = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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-900/10 dark:border-gray-50/[0.06] bg-white/75 dark:bg-gray-900/75 backdrop-blur z-10">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<div class="flex items-center gap-3">
|
||||
<NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-gray-900 dark:text-white">
|
||||
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
|
||||
|
||||
nuxthq/ui
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="flex -mr-1.5">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
@click="isDialogOpen = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 sm:px-6 py-4 sm:py-6">
|
||||
<ThemeSelect class="mb-4 sm:mb-6 w-full" />
|
||||
|
||||
<DocsAsideLinks @click="isDialogOpen = false" />
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Dialog, DialogPanel, TransitionRoot } from '@headlessui/vue'
|
||||
|
||||
const { isSearchModalOpen } = useDocs()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const isDialogOpen = ref(false)
|
||||
|
||||
const isDark = computed({
|
||||
get () {
|
||||
return colorMode.value === 'dark'
|
||||
},
|
||||
set () {
|
||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||
}
|
||||
})
|
||||
|
||||
function openDocsSearch () {
|
||||
isDialogOpen.value = false
|
||||
|
||||
setTimeout(() => {
|
||||
isSearchModalOpen.value = true
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
5
docs/components/Logo.vue
Normal file
5
docs/components/Logo.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="currentColor" />
|
||||
</svg>
|
||||
</template>
|
||||
87
docs/components/ThemeSelect.vue
Normal file
87
docs/components/ThemeSelect.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="flex items-center shadow-sm">
|
||||
<USelectMenu
|
||||
v-model="primary"
|
||||
name="primary"
|
||||
class="w-full [&>div>button]:!rounded-r-none"
|
||||
appearance="gray"
|
||||
:ui="{ width: 'w-[194px]' }"
|
||||
:popper="{ placement: 'bottom-start' }"
|
||||
:options="primaryOptions"
|
||||
>
|
||||
<template #label>
|
||||
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${primary.hex}`}" />
|
||||
|
||||
{{ primary.text }}
|
||||
</template>
|
||||
|
||||
<template #option="{ option }">
|
||||
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
|
||||
|
||||
{{ option.text }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
|
||||
<USelectMenu
|
||||
v-model="gray"
|
||||
name="gray"
|
||||
class="w-full [&>div>button]:!rounded-l-none [&>div>button]:-ml-px"
|
||||
appearance="gray"
|
||||
:ui="{ width: 'w-[194px]' }"
|
||||
:popper="{ placement: 'bottom-end' }"
|
||||
:options="grayOptions"
|
||||
>
|
||||
<template #label>
|
||||
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${gray.hex}`}" />
|
||||
|
||||
{{ gray.text }}
|
||||
</template>
|
||||
|
||||
<template #option="{ option }">
|
||||
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
|
||||
|
||||
{{ option.text }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import colors from '#tailwind-config/theme/colors'
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const primaryCookie = useCookie('primary', { path: '/', default: () => appConfig.ui.primary })
|
||||
const grayCookie = useCookie('gray', { path: '/', default: () => appConfig.ui.gray })
|
||||
|
||||
watch(primaryCookie, (primary) => {
|
||||
appConfig.ui.primary = primary
|
||||
}, { immediate: true })
|
||||
|
||||
watch(grayCookie, (gray) => {
|
||||
appConfig.ui.gray = gray
|
||||
}, { immediate: true })
|
||||
|
||||
// Computed
|
||||
|
||||
const primaryOptions = computed(() => useWithout(appConfig.ui.colors, 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
|
||||
const primary = computed({
|
||||
get () {
|
||||
return primaryOptions.value.find(option => option.value === primaryCookie.value)
|
||||
},
|
||||
set (option) {
|
||||
primaryCookie.value = option.value
|
||||
}
|
||||
})
|
||||
|
||||
const grayOptions = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
|
||||
const gray = computed({
|
||||
get () {
|
||||
return grayOptions.value.find(option => option.value === grayCookie.value)
|
||||
},
|
||||
set (option) {
|
||||
grayCookie.value = option.value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
33
docs/components/content/Alert.vue
Normal file
33
docs/components/content/Alert.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<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 hover:text-gray-800 dark:hover:text-gray-200 text-sm leading-6 my-5 last:mb-0 font-normal group relative prose-code:bg-gray-200 dark:prose-code:bg-gray-800"
|
||||
:class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed' : '']"
|
||||
>
|
||||
<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>
|
||||
53
docs/components/content/CodeGroup.vue
Normal file
53
docs/components/content/CodeGroup.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<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>
|
||||
184
docs/components/content/ComponentCard.vue
Normal file
184
docs/components/content/ComponentCard.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="propsToSelect.length" class="relative flex border border-gray-200 dark:border-gray-700 rounded-t-md overflow-hidden not-prose">
|
||||
<div v-for="prop in propsToSelect" :key="prop.name" class="flex flex-col gap-0.5 justify-between py-1.5 font-medium bg-gray-50 dark:bg-gray-800 border-r border-r-gray-200 dark:border-r-gray-700">
|
||||
<label :for="prop.name" class="block text-xs px-3 font-medium text-gray-400 dark:text-gray-500 -my-px">{{ prop.label }}</label>
|
||||
<UCheckbox
|
||||
v-if="prop.type === 'boolean'"
|
||||
v-model="componentProps[prop.name]"
|
||||
:name="prop.name"
|
||||
appearance="none"
|
||||
class="justify-center"
|
||||
/>
|
||||
<USelectMenu
|
||||
v-else-if="prop.type === 'string' && prop.options.length"
|
||||
v-model="componentProps[prop.name]"
|
||||
:options="prop.options"
|
||||
:name="prop.name"
|
||||
:label="componentProps[prop.name]"
|
||||
appearance="none"
|
||||
class="inline-flex"
|
||||
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md' }"
|
||||
:ui-select="{ custom: '!py-0' }"
|
||||
:popper="{ strategy: 'fixed', placement: 'bottom-start' }"
|
||||
/>
|
||||
<UInput
|
||||
v-else
|
||||
:model-value="componentProps[prop.name]"
|
||||
:type="prop.type === 'number' ? 'number' : 'text'"
|
||||
:name="prop.name"
|
||||
appearance="none"
|
||||
autocomplete="off"
|
||||
:ui="{ custom: '!py-0' }"
|
||||
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex border border-b-0 border-gray-200 dark:border-gray-700 relative not-prose" :class="[{ 'p-4': padding }, propsToSelect.length ? 'border-t-0' : 'rounded-t-md', backgroundClass]">
|
||||
<component :is="name" v-model="vModel" v-bind="fullProps">
|
||||
<ContentSlot v-if="$slots.default" :use="$slots.default" />
|
||||
</component>
|
||||
</div>
|
||||
|
||||
<ContentRenderer :value="ast" class="[&>div>pre]:!rounded-t-none" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// @ts-expect-error
|
||||
import { transformContent } from '@nuxt/content/transformers'
|
||||
|
||||
const props = defineProps({
|
||||
slug: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
padding: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
props: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
code: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
baseProps: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
ui: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
excludedProps: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: 'bg-white dark:bg-gray-900'
|
||||
}
|
||||
})
|
||||
|
||||
const baseProps = reactive({ ...props.baseProps })
|
||||
const componentProps = reactive({ ...props.props })
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const route = useRoute()
|
||||
const slug = props.slug || route.params.slug[1]
|
||||
const camelName = useCamelCase(slug)
|
||||
const name = `U${useUpperFirst(camelName)}`
|
||||
|
||||
const meta = await fetchComponentMeta(name)
|
||||
|
||||
// Computed
|
||||
|
||||
const ui = computed(() => ({ ...appConfig.ui[camelName], ...props.ui }))
|
||||
|
||||
const fullProps = computed(() => ({ ...props.baseProps, ...componentProps }))
|
||||
const vModel = computed({
|
||||
get: () => baseProps.modelValue,
|
||||
set: (value) => {
|
||||
baseProps.modelValue = value
|
||||
}
|
||||
})
|
||||
|
||||
const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
|
||||
if (props.excludedProps.includes(key)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const prop = meta?.meta?.props?.find((prop: any) => prop.name === key)
|
||||
const dottedKey = useKebabCase(key).replaceAll('-', '.')
|
||||
const keys = useGet(ui.value, dottedKey, {})
|
||||
let options = typeof keys === 'object' && Object.keys(keys)
|
||||
if (key.toLowerCase().endsWith('color')) {
|
||||
options = appConfig.ui.colors
|
||||
}
|
||||
|
||||
return {
|
||||
type: prop?.type || 'string',
|
||||
name: key,
|
||||
label: key === 'modelValue' ? 'value' : useCamelCase(key),
|
||||
options
|
||||
}
|
||||
}).filter(Boolean))
|
||||
|
||||
const code = computed(() => {
|
||||
let code = `\`\`\`html
|
||||
<${name}`
|
||||
for (const [key, value] of Object.entries(componentProps)) {
|
||||
if (value === 'undefined' || value === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
const prop = meta?.meta?.props?.find((prop: any) => prop.name === key)
|
||||
|
||||
code += ` ${(prop?.type === 'boolean' && value !== true) || typeof value === 'object' ? ':' : ''}${key === 'modelValue' ? 'value' : useKebabCase(key)}${prop?.type === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}`
|
||||
}
|
||||
if (props.code) {
|
||||
const lineBreaks = (props.code.match(/\n/g) || []).length
|
||||
if (lineBreaks > 1) {
|
||||
code += `>
|
||||
${props.code}</${name}>`
|
||||
} else {
|
||||
code += `>${props.code}</${name}>`
|
||||
}
|
||||
} else {
|
||||
code += ' />'
|
||||
}
|
||||
code += `
|
||||
\`\`\`
|
||||
`
|
||||
return code
|
||||
})
|
||||
|
||||
function renderObject (obj: any) {
|
||||
if (Array.isArray(obj)) {
|
||||
return `[${obj.map(renderObject).join(', ')}]`
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
return `{ ${Object.entries(obj).map(([key, value]) => `${key}: ${renderObject(value)}`).join(', ')} }`
|
||||
}
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return `'${obj}'`
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify(componentProps)}`, () => transformContent('content:_markdown.md', code.value, {
|
||||
highlight: {
|
||||
theme: {
|
||||
light: 'material-lighter',
|
||||
dark: 'material-palenight'
|
||||
}
|
||||
}
|
||||
}), { watch: [code] })
|
||||
</script>
|
||||
22
docs/components/content/ComponentExample.vue
Normal file
22
docs/components/content/ComponentExample.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="[&>div>pre]:!rounded-t-none">
|
||||
<div class="flex border border-gray-200 dark:border-gray-700 relative not-prose rounded-t-md" :class="[{ 'p-4': padding, 'rounded-b-md': !$slots.code, 'border-b-0': !!$slots.code }, backgroundClass]">
|
||||
<ContentSlot v-if="$slots.default" :use="$slots.default" />
|
||||
</div>
|
||||
|
||||
<ContentSlot v-if="$slots.code" :use="$slots.code" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
padding: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: 'bg-white dark:bg-gray-900'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
36
docs/components/content/ComponentPreset.vue
Normal file
36
docs/components/content/ComponentPreset.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<ContentRenderer :value="ast" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// @ts-expect-error
|
||||
import { transformContent } from '@nuxt/content/transformers'
|
||||
|
||||
const props = defineProps({
|
||||
slug: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const route = useRoute()
|
||||
const slug = props.slug || route.params.slug[1]
|
||||
const camelName = useCamelCase(slug)
|
||||
const name = `U${useUpperFirst(camelName)}`
|
||||
|
||||
const preset = appConfig.ui[camelName]
|
||||
|
||||
const { data: ast } = await useAsyncData(`${name}-preset`, () => transformContent('content:_markdown.md', `
|
||||
\`\`\`json
|
||||
${JSON.stringify(preset, null, 2)}
|
||||
\`\`\`\
|
||||
`, {
|
||||
highlight: {
|
||||
theme: {
|
||||
light: 'material-lighter',
|
||||
dark: 'material-palenight'
|
||||
}
|
||||
}
|
||||
}))
|
||||
</script>
|
||||
55
docs/components/content/ComponentProps.vue
Normal file
55
docs/components/content/ComponentProps.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<table class="table-fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-[25%]">
|
||||
Prop
|
||||
</th>
|
||||
<th class="w-[50%]">
|
||||
Default
|
||||
</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="prop in metaProps" :key="prop.name">
|
||||
<td class="relative flex-shrink-0">
|
||||
<code>{{ prop.name }}</code><span v-if="prop.required" class="font-bold text-red-500 dark:text-red-400 absolute top-0 ml-1">*</span>
|
||||
</td>
|
||||
<td>
|
||||
<code v-if="prop.default">{{ prop.default }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<a v-if="prop.name === 'ui'" href="#preset">
|
||||
<code>{{ prop.type }}</code>
|
||||
</a>
|
||||
<code v-else class="break-all">
|
||||
{{ prop.type }}
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
slug: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const slug = props.slug || route.params.slug[1]
|
||||
const camelName = useCamelCase(slug)
|
||||
const name = `U${useUpperFirst(camelName)}`
|
||||
|
||||
const meta = await fetchComponentMeta(name)
|
||||
|
||||
const metaProps = computed(() => useSortBy(meta?.meta?.props || [], [
|
||||
prop => ['string', 'number', 'boolean', 'any'].indexOf(prop.type)
|
||||
]))
|
||||
</script>
|
||||
34
docs/components/content/ComponentSlots.vue
Normal file
34
docs/components/content/ComponentSlots.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Slot</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="slot in (meta.meta.slots as any[])" :key="slot.name">
|
||||
<td class="whitespace-nowrap">
|
||||
<code>{{ slot.name }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
slug: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const slug = props.slug || route.params.slug[1]
|
||||
const camelName = useCamelCase(slug)
|
||||
const name = `U${useUpperFirst(camelName)}`
|
||||
|
||||
const meta = await fetchComponentMeta(name)
|
||||
</script>
|
||||
21
docs/components/content/Placeholder.vue
Normal file
21
docs/components/content/Placeholder.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="relative overflow-hidden rounded border border-dashed border-gray-400 dark:border-gray-500 opacity-75">
|
||||
<svg class="absolute inset-0 h-full w-full stroke-gray-900/10 dark:stroke-white/10" fill="none">
|
||||
<defs>
|
||||
<pattern
|
||||
id="pattern-5c1e4f0e-62d5-498b-8ff0-cf77bb448c8e"
|
||||
x="0"
|
||||
y="0"
|
||||
width="10"
|
||||
height="10"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<path d="M-3 13 15-5M-5 5l18-18M-1 21 17 3" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect stroke="none" fill="url(#pattern-5c1e4f0e-62d5-498b-8ff0-cf77bb448c8e)" width="100%" height="100%" />
|
||||
</svg>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
20
docs/components/content/Shortcut.vue
Normal file
20
docs/components/content/Shortcut.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<kbd class="inline-flex items-center justify-center font-sans font-semibold px-1 h-5 min-w-[20px] text-[11px] rounded !my-0 align-text-top ring-1 ring-gray-300 dark:ring-gray-700">
|
||||
<ClientOnly>
|
||||
{{ shortcut }}
|
||||
</ClientOnly>
|
||||
</kbd>
|
||||
</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>
|
||||
13
docs/components/content/examples/CardExample.vue
Normal file
13
docs/components/content/examples/CardExample.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<Placeholder class="h-8" />
|
||||
</template>
|
||||
|
||||
<Placeholder class="h-32" />
|
||||
|
||||
<template #footer>
|
||||
<Placeholder class="h-8" />
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
const people = [
|
||||
{ id: 1, label: 'Wade Cooper' },
|
||||
{ id: 2, label: 'Arlene Mccoy' },
|
||||
{ id: 3, label: 'Devon Webb' },
|
||||
{ id: 4, label: 'Tom Cook' },
|
||||
{ id: 5, label: 'Tanya Fox' },
|
||||
{ id: 6, label: 'Hellen Schmidt' },
|
||||
{ id: 7, label: 'Caroline Schultz' },
|
||||
{ id: 8, label: 'Mason Heaney' },
|
||||
{ id: 9, label: 'Claudie Smitham' },
|
||||
{ id: 10, label: 'Emil Schaefer' }
|
||||
]
|
||||
|
||||
const selected = ref([people[3]])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCommandPalette
|
||||
v-model="selected"
|
||||
multiple
|
||||
nullable
|
||||
:groups="[{ key: 'people', commands: people }]"
|
||||
:fuse="{ resultLimit: 6 }"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
const router = useRouter()
|
||||
|
||||
const commandPaletteRef = ref()
|
||||
|
||||
const users = [
|
||||
{ id: 'benjamincanac', label: 'benjamincanac', href: 'https://github.com/benjamincanac', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' } },
|
||||
{ id: 'Atinux', label: 'Atinux', href: 'https://github.com/Atinux', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' } },
|
||||
{ id: 'smarroufin', label: 'smarroufin', href: 'https://github.com/smarroufin', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' } }
|
||||
]
|
||||
|
||||
const actions = [
|
||||
{ id: 'new-file', label: 'Add new file', icon: 'i-heroicons-document-plus', click: () => alert('New file') },
|
||||
{ id: 'new-folder', label: 'Add new folder', icon: 'i-heroicons-folder-plus', click: () => alert('New folder') },
|
||||
{ id: 'hashtag', label: 'Add hashtag', icon: 'i-heroicons-hashtag', click: () => alert('Add hashtag') },
|
||||
{ id: 'label', label: 'Add label', icon: 'i-heroicons-tag', click: () => alert('Add label') }
|
||||
]
|
||||
|
||||
const groups = computed(() => commandPaletteRef.value?.query
|
||||
? [{
|
||||
key: 'users',
|
||||
commands: users
|
||||
}]
|
||||
: [{
|
||||
key: 'recent',
|
||||
label: 'Recent searches',
|
||||
commands: users.slice(0, 1)
|
||||
}, {
|
||||
key: 'actions',
|
||||
commands: actions
|
||||
}])
|
||||
|
||||
function onSelect (option) {
|
||||
if (option.click) {
|
||||
option.click()
|
||||
} else if (option.to) {
|
||||
router.push(option.to)
|
||||
} else if (option.href) {
|
||||
window.open(option.href, '_blank')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCommandPalette ref="commandPaletteRef" :groups="groups" @update:model-value="onSelect" />
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
const open = ref(false)
|
||||
|
||||
const people = [
|
||||
{ id: 1, label: 'Wade Cooper' },
|
||||
{ id: 2, label: 'Arlene Mccoy' },
|
||||
{ id: 3, label: 'Devon Webb' },
|
||||
{ id: 4, label: 'Tom Cook' },
|
||||
{ id: 5, label: 'Tanya Fox' },
|
||||
{ id: 6, label: 'Hellen Schmidt' },
|
||||
{ id: 7, label: 'Caroline Schultz' },
|
||||
{ id: 8, label: 'Mason Heaney' },
|
||||
{ id: 9, label: 'Claudie Smitham' },
|
||||
{ id: 10, label: 'Emil Schaefer' }
|
||||
]
|
||||
|
||||
const selected = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UButton label="Open" @click="open = true" />
|
||||
|
||||
<UModal v-model="open">
|
||||
<UCommandPalette
|
||||
v-model="selected"
|
||||
multiple
|
||||
nullable
|
||||
:groups="[{ key: 'people', commands: people }]"
|
||||
/>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
5
docs/components/content/examples/ContainerExample.vue
Normal file
5
docs/components/content/examples/ContainerExample.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<UContainer>
|
||||
<Placeholder class="h-32" />
|
||||
</UContainer>
|
||||
</template>
|
||||
34
docs/components/content/examples/ContextMenuExample.vue
Normal file
34
docs/components/content/examples/ContextMenuExample.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
const { x, y } = useMouse()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
|
||||
|
||||
function openContextMenu () {
|
||||
const top = unref(y)
|
||||
const left = unref(x)
|
||||
|
||||
virtualElement.value.getBoundingClientRect = () => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
top,
|
||||
left
|
||||
})
|
||||
|
||||
isOpen.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full" @contextmenu.prevent="openContextMenu">
|
||||
<Placeholder class="h-20 w-full flex items-center justify-center">
|
||||
Right click here
|
||||
</Placeholder>
|
||||
|
||||
<UContextMenu v-model="isOpen" :virtual-element="virtualElement" width-class="w-48">
|
||||
<div class="p-4">
|
||||
Menu
|
||||
</div>
|
||||
</UContextMenu>
|
||||
</div>
|
||||
</template>
|
||||
31
docs/components/content/examples/DropdownExample.vue
Normal file
31
docs/components/content/examples/DropdownExample.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
const items = [
|
||||
[{
|
||||
label: 'Profile',
|
||||
avatar: {
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
}
|
||||
}], [{
|
||||
label: 'Edit',
|
||||
icon: 'i-heroicons-pencil-square-20-solid'
|
||||
}, {
|
||||
label: 'Duplicate',
|
||||
icon: 'i-heroicons-document-duplicate-20-solid'
|
||||
}], [{
|
||||
label: 'Archive',
|
||||
icon: 'i-heroicons-archive-box-20-solid'
|
||||
}, {
|
||||
label: 'Move',
|
||||
icon: 'i-heroicons-arrow-right-circle-20-solid'
|
||||
}], [{
|
||||
label: 'Delete',
|
||||
icon: 'i-heroicons-trash-20-solid'
|
||||
}]
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDropdown :items="items" :popper="{ placement: 'bottom-start' }">
|
||||
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
</UDropdown>
|
||||
</template>
|
||||
15
docs/components/content/examples/ModalExampleBasic.vue
Normal file
15
docs/components/content/examples/ModalExampleBasic.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UButton label="Open" @click="open = true" />
|
||||
|
||||
<UModal v-model="open">
|
||||
<div class="p-4">
|
||||
<Placeholder class="h-48" />
|
||||
</div>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
23
docs/components/content/examples/ModalExampleCard.vue
Normal file
23
docs/components/content/examples/ModalExampleCard.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UButton label="Open" @click="open = true" />
|
||||
|
||||
<UModal v-model="open">
|
||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<Placeholder class="h-8" />
|
||||
</template>
|
||||
|
||||
<Placeholder class="h-32" />
|
||||
|
||||
<template #footer>
|
||||
<Placeholder class="h-8" />
|
||||
</template>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
const toast = useToast()
|
||||
|
||||
const actions = ref([{
|
||||
label: 'Action 1',
|
||||
click: () => alert('Action 1 clicked!')
|
||||
}, {
|
||||
label: 'Action 2',
|
||||
click: () => alert('Action 2 clicked!')
|
||||
}])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Show toast" @click="toast.add({ title: 'With actions', actions })" />
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup>
|
||||
const toast = useToast()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Show toast" @click="toast.add({ title: 'Hello world!' })" />
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup>
|
||||
const toast = useToast()
|
||||
|
||||
function onCallback () {
|
||||
alert('Notification expired!')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Show toast" @click="toast.add({ title: 'Expires soon...', timeout: 1000, callback: onCallback })" />
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup>
|
||||
const toast = useToast()
|
||||
|
||||
function onClick () {
|
||||
alert('Clicked!')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Show toast" @click="toast.add({ title: 'Click me', click: onClick })" />
|
||||
</template>
|
||||
11
docs/components/content/examples/PopoverExample.vue
Normal file
11
docs/components/content/examples/PopoverExample.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<UPopover>
|
||||
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
|
||||
<template #panel>
|
||||
<div class="p-4">
|
||||
<Placeholder class="h-20 w-48" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu v-model="selected" :options="people" />
|
||||
</template>
|
||||
15
docs/components/content/examples/SelectMenuExampleButton.vue
Normal file
15
docs/components/content/examples/SelectMenuExampleButton.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[3])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
|
||||
<UButton>
|
||||
{{ selected }}
|
||||
|
||||
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
|
||||
</UButton>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu v-model="selected" :options="people" multiple>
|
||||
<template #label>
|
||||
<span v-if="selected.length" class="font-medium truncate">{{ selected.join(', ') }}</span>
|
||||
<span v-else class="block truncate text-gray-400 dark:text-gray-500">Select people</span>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup>
|
||||
const people = [{
|
||||
id: 'benjamincanac',
|
||||
label: 'benjamincanac',
|
||||
href: 'https://github.com/benjamincanac',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
|
||||
},
|
||||
{
|
||||
id: 'Atinux',
|
||||
label: 'Atinux',
|
||||
href: 'https://github.com/Atinux',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
|
||||
},
|
||||
{
|
||||
id: 'smarroufin',
|
||||
label: 'smarroufin',
|
||||
href: 'https://github.com/smarroufin',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' }
|
||||
},
|
||||
{
|
||||
id: 'nobody',
|
||||
label: 'Nobody',
|
||||
icon: 'i-heroicons-user-circle'
|
||||
}]
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu v-model="selected" :options="people">
|
||||
<template #label>
|
||||
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" />
|
||||
|
||||
{{ selected.label }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
15
docs/components/content/examples/SlideoverExample.vue
Normal file
15
docs/components/content/examples/SlideoverExample.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UButton label="Open" @click="open = true" />
|
||||
|
||||
<USlideover v-model="open">
|
||||
<div class="p-4 h-full">
|
||||
<Placeholder class="w-full h-full" />
|
||||
</div>
|
||||
</USlideover>
|
||||
</div>
|
||||
</template>
|
||||
5
docs/components/content/examples/TooltipExample.vue
Normal file
5
docs/components/content/examples/TooltipExample.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<UTooltip text="Tooltip">
|
||||
<UButton color="gray" label="Button" />
|
||||
</UTooltip>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
const links = [{
|
||||
label: 'Profile',
|
||||
avatar: {
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
}
|
||||
}, {
|
||||
label: 'Installation',
|
||||
icon: 'i-heroicons-home',
|
||||
to: '/getting-started/installation'
|
||||
}, {
|
||||
label: 'Vertical Navigation',
|
||||
icon: 'i-heroicons-chart-bar',
|
||||
to: '/navigation/vertical-navigation'
|
||||
}, {
|
||||
label: 'Command Palette',
|
||||
icon: 'i-heroicons-command-line',
|
||||
to: '/navigation/command-palette'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation :links="links" />
|
||||
</template>
|
||||
71
docs/components/content/prose/ProseCode.vue
Normal file
71
docs/components/content/prose/ProseCode.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<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="link"
|
||||
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>
|
||||
15
docs/components/content/prose/ProseH2.vue
Normal file
15
docs/components/content/prose/ProseH2.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
15
docs/components/content/prose/ProseH3.vue
Normal file
15
docs/components/content/prose/ProseH3.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup>
|
||||
const commandPaletteRef = ref()
|
||||
|
||||
const { navigation } = useContent()
|
||||
|
||||
const { data: files } = await useLazyAsyncData('search', () => queryContent().where({ _type: 'markdown' }).find(), { default: () => [] })
|
||||
|
||||
const groups = 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,
|
||||
icon: 'i-heroicons-document',
|
||||
title: file.navigation?.title || file.title,
|
||||
category: item.title,
|
||||
to: file._path
|
||||
}))
|
||||
})))
|
||||
|
||||
const close = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-x-mark', color: 'black', variant: 'ghost', size: 'lg', padded: false }) : null)
|
||||
const empty = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-magnifying-glass', queryLabel: 'No results' }) : ({ icon: '', label: 'No recent searches' }))
|
||||
|
||||
const ui = {
|
||||
wrapper: 'flex flex-col flex-1 min-h-0 bg-gray-50 dark:bg-gray-800',
|
||||
input: {
|
||||
wrapper: 'relative flex items-center mx-3 py-3',
|
||||
base: 'w-full rounded border-2 border-primary-500 placeholder-gray-400 dark:placeholder-gray-500 focus:border-primary-500 focus:outline-none focus:ring-0 h-14 text-lg bg-white dark:bg-gray-900',
|
||||
icon: 'pointer-events-none absolute left-3 h-6 w-6 text-primary-500 dark:text-primary-400'
|
||||
},
|
||||
group: {
|
||||
wrapper: 'p-3 relative',
|
||||
label: '-mx-3 px-3 -mt-4 mb-2 py-1 text-sm font-semibold text-primary-500 dark:text-primary-400 font-semibold sticky top-0 bg-gray-50 dark:bg-gray-800 z-10',
|
||||
container: 'space-y-1',
|
||||
command: {
|
||||
base: 'flex justify-between select-none items-center rounded px-2 py-4 gap-2 relative font-medium text-sm group shadow',
|
||||
active: 'bg-primary-500 dark:bg-primary-400 text-white',
|
||||
inactive: 'bg-white dark:bg-gray-900',
|
||||
label: 'flex flex-col min-w-0',
|
||||
suffix: 'text-xs',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 w-6 h-6',
|
||||
active: 'text-white',
|
||||
inactive: 'text-gray-400 dark:text-gray-500'
|
||||
}
|
||||
}
|
||||
},
|
||||
empty: {
|
||||
wrapper: 'flex flex-col items-center justify-center flex-1 py-9',
|
||||
label: 'text-sm text-center text-gray-500 dark:text-gray-400',
|
||||
queryLabel: 'text-lg text-center text-gray-900 dark:text-white',
|
||||
icon: 'w-12 h-12 mx-auto text-gray-400 dark:text-gray-500 mb-4'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCommandPalette
|
||||
ref="commandPaletteRef"
|
||||
:groups="groups"
|
||||
:ui="ui"
|
||||
:close="close"
|
||||
:empty="empty"
|
||||
command-attribute="title"
|
||||
:fuse="{
|
||||
fuseOptions: { keys: ['title', 'category'] },
|
||||
}"
|
||||
placeholder="Search docs"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup>
|
||||
const commandPaletteRef = ref()
|
||||
|
||||
const suggestions = [
|
||||
{ id: 'linear', label: 'Linear', icon: 'i-simple-icons-linear' },
|
||||
{ id: 'figma', label: 'Figma', icon: 'i-simple-icons-figma' },
|
||||
{ id: 'slack', label: 'Slack', icon: 'i-simple-icons-slack' },
|
||||
{ id: 'youtube', label: 'YouTube', icon: 'i-simple-icons-youtube' },
|
||||
{ id: 'github', label: 'GitHub', icon: 'i-simple-icons-github' }
|
||||
]
|
||||
|
||||
const commands = [
|
||||
{ id: 'clipboard-history', label: 'Clipboard History', icon: 'i-heroicons-clipboard', click: () => alert('New file') },
|
||||
{ id: 'import-extension', label: 'Import Extension', icon: 'i-heroicons-wrench-screwdriver', click: () => alert('New folder') },
|
||||
{ id: 'manage-extensions', label: 'Manage Extensions', icon: 'i-heroicons-wrench-screwdriver', click: () => alert('Add hashtag') }
|
||||
]
|
||||
|
||||
const groups = [{
|
||||
key: 'suggestions',
|
||||
label: 'Suggestions',
|
||||
inactive: 'Application',
|
||||
commands: suggestions
|
||||
}, {
|
||||
key: 'commands',
|
||||
label: 'Commands',
|
||||
inactive: 'Command',
|
||||
commands
|
||||
}]
|
||||
|
||||
const ui = {
|
||||
wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-200 dark:divide-gray-700 bg-gray-50 dark:bg-gray-800',
|
||||
container: 'relative flex-1 overflow-y-auto divide-y divide-gray-200 dark:divide-gray-700 scroll-py-2',
|
||||
input: {
|
||||
base: 'w-full h-14 px-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 text-gray-900 dark:text-white focus:ring-0'
|
||||
},
|
||||
group: {
|
||||
label: 'px-2 my-2 text-xs font-semibold text-gray-500 dark:text-gray-400',
|
||||
command: {
|
||||
base: 'flex justify-between select-none cursor-default items-center rounded-md px-2 py-2 gap-2 relative',
|
||||
active: 'bg-gray-200 dark:bg-gray-700/50 text-gray-900 dark:text-white',
|
||||
container: 'flex items-center gap-3 min-w-0',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 w-5 h-5',
|
||||
active: 'text-gray-900 dark:text-white',
|
||||
inactive: 'text-gray-400 dark:text-gray-500'
|
||||
},
|
||||
avatar: {
|
||||
size: '2xs'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCommandPalette ref="commandPaletteRef" :groups="groups" icon="" :ui="ui" placeholder="Search for apps and commands" />
|
||||
</template>
|
||||
33
docs/components/docs/DocsAside.vue
Normal file
33
docs/components/docs/DocsAside.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<aside
|
||||
class="hidden pb-8 overflow-y-auto lg:block lg:self-start lg:top-16 lg:max-h-[calc(100vh-64px)] lg:sticky lg:pr-8 lg:pl-[2px]"
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="sticky top-0 pointer-events-none">
|
||||
<div class="h-8 bg-white dark:bg-gray-900" />
|
||||
<div class="bg-white dark:bg-gray-900 relative pointer-events-auto">
|
||||
<UButton
|
||||
icon="i-heroicons-magnifying-glass-20-solid"
|
||||
class="w-full"
|
||||
color="gray"
|
||||
@click="isSearchModalOpen = true"
|
||||
>
|
||||
Search
|
||||
|
||||
<div class="hidden lg:flex items-center gap-1 ml-auto -my-1">
|
||||
<Shortcut value="meta" />
|
||||
<Shortcut value="K" />
|
||||
</div>
|
||||
</UButton>
|
||||
</div>
|
||||
<div class="h-8 bg-gradient-to-b from-white dark:from-gray-900" />
|
||||
</div>
|
||||
|
||||
<DocsAsideLinks />
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { isSearchModalOpen } = useDocs()
|
||||
</script>
|
||||
31
docs/components/docs/DocsAsideLinks.vue
Normal file
31
docs/components/docs/DocsAsideLinks.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="space-y-8">
|
||||
<div v-for="(group, index) in navigation" :key="index" class="space-y-3">
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-gray-200">
|
||||
<span class="truncate">{{ group.title }}</span>
|
||||
</div>
|
||||
|
||||
<UVerticalNavigation
|
||||
:links="mapContentLinks(group.children)"
|
||||
class="mt-1"
|
||||
:ui="{
|
||||
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
|
||||
spacing: 'pl-4',
|
||||
base: 'group text-sm block border-l -ml-px lg:leading-6',
|
||||
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
|
||||
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'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NavItem } from '@nuxt/content/dist/runtime/types'
|
||||
|
||||
const { navigation } = useContent() as { navigation: NavItem[] }
|
||||
|
||||
function mapContentLinks (links: NavItem[]) {
|
||||
return links?.map(link => ({ label: link.title, icon: link.icon, to: link._path })) || []
|
||||
}
|
||||
</script>
|
||||
37
docs/components/docs/DocsPageHeader.vue
Normal file
37
docs/components/docs/DocsPageHeader.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<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">
|
||||
{{ 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 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}/U${page.title}.vue`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="page.description" class="mt-4 text-lg">
|
||||
{{ page.description }}
|
||||
</p>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { page } = useContent()
|
||||
</script>
|
||||
11
docs/components/docs/DocsPrevNext.vue
Normal file
11
docs/components/docs/DocsPrevNext.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between">
|
||||
<UButton v-if="prev" :label="prev.navigation?.title || prev.title" :to="prev._path" icon="i-heroicons-arrow-small-left-20-solid" color="white" />
|
||||
<span v-else> </span>
|
||||
<UButton v-if="next" :label="next.navigation?.title || next.title" :to="next._path" trailing-icon="i-heroicons-arrow-small-right-20-solid" color="white" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { prev, next } = useContent()
|
||||
</script>
|
||||
172
docs/components/docs/DocsSearch.vue
Normal file
172
docs/components/docs/DocsSearch.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<UModal
|
||||
v-model="isSearchModalOpen"
|
||||
:ui="{
|
||||
spacing: '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"
|
||||
:fuse="{
|
||||
fuseOptions: { ignoreLocation: true, includeMatches: true, minMatchCharLength: 2, 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 { Command } from '../../../src/runtime/types'
|
||||
|
||||
const { navigation } = useContent()
|
||||
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>
|
||||
19
docs/components/docs/DocsToc.vue
Normal file
19
docs/components/docs/DocsToc.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div v-if="toc" 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">
|
||||
const { toc } = useContent()
|
||||
|
||||
const isTocOpen = ref(false)
|
||||
</script>
|
||||
49
docs/components/docs/DocsTocLinks.vue
Normal file
49
docs/components/docs/DocsTocLinks.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<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>
|
||||
19
docs/composables/useComponentMeta.ts
Normal file
19
docs/composables/useComponentMeta.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
const useComponentsMetaState = () => useState('components-meta', () => ({}))
|
||||
|
||||
export async function fetchComponentMeta (name: string) {
|
||||
const state = useComponentsMetaState()
|
||||
|
||||
if (state.value[name]?.then) {
|
||||
await state.value[name]
|
||||
return state.value[name]
|
||||
}
|
||||
if (state.value[name]) { return state.value[name] }
|
||||
|
||||
// Store promise to avoid multiple calls
|
||||
state.value[name] = $fetch(`/api/component-meta/${name}`).then((meta) => {
|
||||
state.value[name] = meta
|
||||
})
|
||||
|
||||
await state.value[name]
|
||||
return state.value[name]
|
||||
}
|
||||
11
docs/composables/useDocs.ts
Normal file
11
docs/composables/useDocs.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
|
||||
const _useDocs = () => {
|
||||
const isSearchModalOpen = ref(false)
|
||||
|
||||
return {
|
||||
isSearchModalOpen
|
||||
}
|
||||
}
|
||||
|
||||
export const useDocs = createSharedComposable(_useDocs)
|
||||
37
docs/composables/useScrollspy.ts
Normal file
37
docs/composables/useScrollspy.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
62
docs/content/1.getting-started/1.installation.md
Normal file
62
docs/content/1.getting-started/1.installation.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: 'nuxthq/ui'
|
||||
description: 'Components library as a Nuxt3 module using TailwindCSS based on TailwindUI.'
|
||||
navigation:
|
||||
title: Installation
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install `@nuxthq/ui` dependency to your project:
|
||||
|
||||
::code-group
|
||||
|
||||
```bash [yarn]
|
||||
yarn add -D @nuxthq/ui
|
||||
```
|
||||
|
||||
```bash [npm]
|
||||
npm install -D @nuxthq/ui
|
||||
```
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm i -D @nuxthq/ui
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
2. Add it to your `modules` section in your `nuxt.config`:
|
||||
|
||||
```ts [nuxt.config]
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxthq/ui']
|
||||
})
|
||||
```
|
||||
|
||||
::alert
|
||||
That's it! You can now use all the components and composables in your Nuxt app ✨
|
||||
::
|
||||
|
||||
## Options
|
||||
|
||||
| Key | Default | Description |
|
||||
| ------------------------ | ---------------------- | ------------------------------------------------ |
|
||||
| `prefix` | `u` | Define the prefix of the imported components. |
|
||||
| `global` | `false` | Expose components globally. |
|
||||
| `icons` | `['heroicons']` | Icon collections to load. |
|
||||
|
||||
## Edge channel
|
||||
|
||||
To use the latest updates pushed on the [`dev`](https://github.com/nuxtlabs/ui/tree/dev) branch, you can use `@nuxthq/ui-edge`.
|
||||
|
||||
Update your `package.json` to the following:
|
||||
|
||||
```json [package.json]
|
||||
{
|
||||
"devDependencies": {
|
||||
"@nuxthq/ui": "npm:@nuxthq/ui-edge@latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run `npm install` or `yarn install`.
|
||||
1
docs/content/1.getting-started/2.theming.md
Normal file
1
docs/content/1.getting-started/2.theming.md
Normal file
@@ -0,0 +1 @@
|
||||
## Overview
|
||||
7
docs/content/1.getting-started/3.shortcuts.md
Normal file
7
docs/content/1.getting-started/3.shortcuts.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## Overview
|
||||
|
||||
## Composables
|
||||
|
||||
## `defineShortcuts`
|
||||
|
||||
## `useShortcuts`
|
||||
152
docs/content/1.getting-started/4.migration.md
Normal file
152
docs/content/1.getting-started/4.migration.md
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
navigation: false
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
Classes to invert dark mode like `u-text-gray-900` have been removed.
|
||||
|
||||
- Components now have a `ui` prop to override the entire preset instead of individual props
|
||||
- Components prop `popperOptions` has been renamed to `popper`
|
||||
- `Alert`, `AlertDialog`, `Tabs` and `Pills` components have been removed
|
||||
|
||||
### `Avatar`
|
||||
|
||||
- `wrapperClass`, `backgroundClass`, `placeholderClass` and `roundedClass` props have been removed in favor of `ui`
|
||||
- `rounded` prop is now a class defaulting to `rounded-full` instead of a boolean prop, can be overriden through `ui.avatar.rounded`
|
||||
- `chip` prop is now `chipVariant`
|
||||
- `ui.avatar.size` and `ui.avatar.chip.size` `xxs` and `xxxs` have been renamed respectively to `2xs` and `3xs`
|
||||
|
||||
### `AvatarGroup`
|
||||
|
||||
- `ringClass` and `marginClass` props have been removed in favor of `ui`
|
||||
- `group` prop has been removed in favor of slots
|
||||
|
||||
### `Badge`
|
||||
|
||||
- `baseClass` prop has been removed in favor of `ui`
|
||||
- `rounded` prop is now a class defaulting to `rounded-md` instead of a boolean prop, can be overriden through `ui.badge.rounded`
|
||||
- `color` prop has been added to change the color scheme of the badge
|
||||
- `variant` prop is now the variant instead of the color
|
||||
- `font-medium` has been moved from `ui.badge.base` to `ui.badge.font`
|
||||
|
||||
### `Button`
|
||||
|
||||
- `customClass` prop have been removed
|
||||
- `baseClass`, `iconBaseClass` and `roundedClass` props have been removed in favor of `ui`
|
||||
- `leadingIconClass` and `trailingIconClass` props have been removed
|
||||
- `rounded` prop is now a class defaulting to `rounded-md` instead of a boolean prop, can be overriden through `ui.button.rounded`
|
||||
- `color` prop has been added to change the color scheme of the badge
|
||||
- `variant` prop is now the variant instead of the color
|
||||
- `labelCompact` and `compact` props have been removed entirely alongside preset `ui.button.compact` and `ui.button.icon.leading.compactSpacing` and `ui.button.icon.trailing.compactSpacing`
|
||||
- `padded` prop has been added to remove padding
|
||||
- `ui.button.size.xxs` has been renamed to `ui.button.size.2xs`
|
||||
- `ui.button.size.2xl` has been introduced
|
||||
- `ui.button.gap` has been introduced to replace margins defined in `ui.button.icon.leading.spacing` and `ui.button.icon.trailing.spacing`
|
||||
- `ui.button.icon.leading.spacing` and `ui.button.icon.trailing.spacing` that added negative margin to icons have been removed to keep consitency when surcharging a button through default slot (code has only been commented for now)
|
||||
- `font-medium` has been moved from `ui.button.base` to `ui.button.font`
|
||||
|
||||
### `ButtonGroup`
|
||||
|
||||
- New component
|
||||
|
||||
### `Dropdown`
|
||||
|
||||
- `wrapperClass`, `containerClass`, `widthClass`, `backgroundClass`, `shadowClass`, `roundedClass`, `ringClass`, `divideClass`, `baseClass`, `transitionClass`, `groupClass`, `itemBaseClass`, `itemActiveClass`, `itemInactiveClass`, `itemDisabledClass`, `itemIconClass`, `itemAvatarClass` and `itemShortcutsClass` props have been removed in favor of `ui`
|
||||
- preset has been updated to improve dark mode
|
||||
|
||||
### `Card`
|
||||
|
||||
- `baseClass`, `backgroundClass`, `borderColorClass`, `shadowClass`, `ringClass`, `roundedClass`, `bodyClass`, `bodyBackgroundClass`, `headerClass`, `headerBackgroundClass`, `footerClass`, `footerBackgroundClass` props have been removed in favor of `ui`
|
||||
- `rounded` prop is now a class defaulting to `rounded-lg` instead of a boolean prop, can be overriden through `ui.avatar.rounded`
|
||||
- `padded` prop has been removed, use `ui.rounded = 'sm:rounded-lg'` instead when false
|
||||
- `ui.card.border` has been removed in favor of `ui.card.divide`
|
||||
- `ui.card.header` & `ui.card.footer` are now `{ spacing: '', background: '' }`
|
||||
|
||||
### `Container`
|
||||
|
||||
- `constrainedClass` prop has been removed in favor of `ui`
|
||||
- `ui.container.base` and `ui.container.spacing` have been added
|
||||
- `padded` prop has been removed, use `ui.spacing = 'sm:px-6 lg:px-8'` instead when false
|
||||
- `constrained` prop has been removed, use `ui.constrained = ''` instead when false
|
||||
|
||||
### `Input`
|
||||
|
||||
- `wrapperClass`, `baseClass`, `iconBaseClass` and `customClass` props have been removed in favor of `ui`
|
||||
|
||||
### `FormGroup`
|
||||
|
||||
- Renamed to `InputGroup`
|
||||
- `wrapperClass`, `containerClass`, `labelClass`, `labelWrapperClass`, `descriptionClass`, `requiredClass` and `hintClass` props have been removed in favor of `ui`
|
||||
|
||||
### `Textarea`
|
||||
|
||||
- `wrapperClass`, `baseClass` and `customClass` props have been removed in favor of `ui`
|
||||
- `resize` is now false by default
|
||||
|
||||
### `Select`
|
||||
|
||||
- `wrapperClass`, `baseClass`, `iconBaseClass` and `customClass` props have been removed in favor of `ui`
|
||||
|
||||
### `SelectCustom`
|
||||
|
||||
- Renamed to `SelectMenu`
|
||||
- `placeholder` prop is now `null` by default
|
||||
- `nullable` prop has been removed
|
||||
- `textAttribute` has been renamed to `optionAttribute` and now defaults to `label`
|
||||
- `wrapperClass`, `baseClass`, `iconBaseClass`, `customClass`, `listBaseClass`, `listContainerClass`, `listWidthClass`, `listInputClass`, `listTransitionClass`, `listOptionBaseClass`, `listOptionContainerClass`, `listOptionActiveClass`, `listOptionInactiveClass`, `listOptionSelectedClass`, `listOptionUnselectedClass`, `listOptionDisabledClass`, `listOptionEmptyClass`, `listOptionIcon`, `listOptionIconBaseClass`, `listOptionIconActiveClass`, `listOptionIconInactiveClass` and `listOptionIconSizeClass` props have been removed in favor of `ui`
|
||||
- `ui.selectCustom.list` has been moved to the root of `ui.selectMenu`, the component now uses `ui.select` to render the default slot
|
||||
|
||||
### `Radio`
|
||||
|
||||
- `wrapperClass`, `baseClass`, `labelClass`, `requiredClass`, `helpClass` and `customClass` props have been removed in favor of `ui`
|
||||
|
||||
### `Checkbox`
|
||||
|
||||
- `wrapperClass`, `baseClass`, `labelClass`, `requiredClass`, `helpClass` and `customClass` props have been removed in favor of `ui`
|
||||
|
||||
### `Toggle`
|
||||
|
||||
- `baseClass`, `activeClass`, `inactiveClass`, `containerBaseClass`, `containerActiveClass`, `containerInactiveClass`, `iconBaseClass`, `iconActiveClass`, `iconInactiveClass`, `iconOnClass` and `iconOffClass` props have been removed in favor of `ui`
|
||||
|
||||
### `CommandPalette`
|
||||
|
||||
- `inputCloseIcon` and `emptyIcon` props have been removed in favor of `ui`
|
||||
- `inputIcon` prop has been renamed to `icon`
|
||||
- `inputPlaceholder` prop has been renamed to `placeholder`
|
||||
- `options` prop has been renamed to `fuse` to follow the `popper` and `ui` props convention
|
||||
|
||||
### `Modal`
|
||||
|
||||
- `wrapperClass`, `innerClass`, `containerClass`, `baseClass`, `backgroundClass`, `overlayBackgroundClass`, `overlayTransitionClass`, `shadowClass`, `ringClass`, `roundedClass`, `widthClass` and `transitionClass` props have been removed in favor of `ui`
|
||||
- `innerStyle` prop has been removed
|
||||
- `#header` and `#footer` slots have been removed
|
||||
|
||||
### `Slideover`
|
||||
|
||||
- `wrapperClass`, `baseClass`, `backgroundClass`, `overlayBackgroundClass`, `overlayTransitionClass`, `widthClass`, `headerClass` and `transitionClass` props have been removed in favor of `ui`
|
||||
- `#header` slot has been removed
|
||||
|
||||
### `Popover`
|
||||
|
||||
- `wrapperClass`, `containerClass`, `widthClass`, `baseClass`, `backgroundClass`, `shadowClass`, `roundedClass`, `ringClass` and `transitionClass` props have been removed in favor of `ui`
|
||||
|
||||
### `Tooltip`
|
||||
|
||||
- `wrapperClass`, `containerClass`, `baseClass`, `widthClass`, `backgroundClass`, `shadowClass`, `ringClass`, `roundedClass`, `shortcutsClass` and `transitionClass` props have been removed in favor of `ui`
|
||||
|
||||
### `ContextMenu`
|
||||
|
||||
- `wrapperClass`, `containerClass`, `widthClass`, `backgroundClass`, `shadowClass`, `roundedClass`, `ringClass`, `baseClass` and `transitionClass`
|
||||
|
||||
### `Notification`
|
||||
|
||||
- `backgroundClass`, `shadowClass`, `ringClass`, `roundedClass`, `transitionClass`, `customClass` and `iconBaseClass` props have been removed in favor of `ui`
|
||||
- `type` prop has been removed
|
||||
- `ui.notification.type` and `ui.notification.icon.color` have been removed
|
||||
- `ui.notification.close.icon.name` has been moved to `ui.notification.default.closeIcon`
|
||||
|
||||
### `useToast`
|
||||
|
||||
- `addNotification` and `removeNotification` have been renamed to `add` and `remove`
|
||||
- `success`, `info`, `warning` and `error` methods have been removed as `type` disappeared from `Notification`
|
||||
100
docs/content/2.elements/1.avatar.md
Normal file
100
docs/content/2.elements/1.avatar.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
alt: 'Avatar'
|
||||
---
|
||||
::
|
||||
|
||||
### Size
|
||||
|
||||
Use the `size` prop to change the size of the Avatar.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
size: 'md'
|
||||
baseProps:
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
alt: 'Avatar'
|
||||
---
|
||||
::
|
||||
|
||||
### Chip
|
||||
|
||||
Use the `chipColor`, `chipVariant` and `chipPosition` props to display a chip on the Avatar.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
chipColor: 'primary'
|
||||
chipVariant: 'solid'
|
||||
chipPosition: 'top-right'
|
||||
baseProps:
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
alt: 'Avatar'
|
||||
---
|
||||
::
|
||||
|
||||
### Placeholder
|
||||
|
||||
If there is an error loading the `src` of the avatar or `src` is null a background placeholder will be displayed, customizable in `ui.avatar.background`.
|
||||
|
||||
If there's an `alt` prop initials will be displayed on top of the background, customizable in `ui.avatar.placeholder`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
alt: 'Benjamin Canac'
|
||||
---
|
||||
::
|
||||
|
||||
### Group
|
||||
|
||||
To stack avatars as a group, use the `AvatarGroup` component.
|
||||
|
||||
- To limit the amount of avatars to show, use the `max` prop. It'll truncate the avatars and show a "+X" label (where X is the remaining avatars)
|
||||
- To size all the avatars equally, pass the `size` prop
|
||||
- To adjust the spacing or the ring between avatars, customize with `ui.avatarGroup.margin` or `ui.avatarGroup.ring`
|
||||
|
||||
::component-card{slug="AvatarGroup"}
|
||||
---
|
||||
props:
|
||||
size: 'md'
|
||||
max: 2
|
||||
ui:
|
||||
size:
|
||||
'3xs': ''
|
||||
'2xs': ''
|
||||
xs: ''
|
||||
sm: ''
|
||||
md: ''
|
||||
lg: ''
|
||||
xl: ''
|
||||
'2xl': ''
|
||||
'3xl': ''
|
||||
code: |
|
||||
<UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" alt="benjamincanac" />
|
||||
<UAvatar src="https://avatars.githubusercontent.com/u/904724?v=4" alt="Atinux" />
|
||||
<UAvatar src="https://avatars.githubusercontent.com/u/7547335?v=4" alt="smarroufin" />
|
||||
---
|
||||
|
||||
#default
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" alt="Avatar"}
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/904724?v=4" alt="Avatar"}
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/7547335?v=4" alt="Avatar"}
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
59
docs/content/2.elements/2.badge.md
Normal file
59
docs/content/2.elements/2.badge.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Use the default slot to set the text of the Badge.
|
||||
|
||||
::component-card
|
||||
---
|
||||
code: Badge
|
||||
---
|
||||
|
||||
Badge
|
||||
::
|
||||
|
||||
You can also use the `label` prop:
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
label: Badge
|
||||
---
|
||||
::
|
||||
|
||||
### Style
|
||||
|
||||
Use the `color` and `variant` props to change the visual style of the Badge.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
color: 'primary'
|
||||
variant: 'solid'
|
||||
---
|
||||
|
||||
Badge
|
||||
::
|
||||
|
||||
### Size
|
||||
|
||||
Use the `size` prop to change the size of the Badge.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
size: 'md'
|
||||
---
|
||||
|
||||
Badge
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
280
docs/content/2.elements/3.button.md
Normal file
280
docs/content/2.elements/3.button.md
Normal file
@@ -0,0 +1,280 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Use the default slot to set the text of the Button.
|
||||
|
||||
::component-card
|
||||
---
|
||||
code: Button
|
||||
---
|
||||
|
||||
Button
|
||||
::
|
||||
|
||||
You can also use the `label` prop.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
label: Button
|
||||
---
|
||||
::
|
||||
|
||||
### Style
|
||||
|
||||
Use the `color` and `variant` props to change the visual style of the Button.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
color: 'primary'
|
||||
variant: 'solid'
|
||||
---
|
||||
|
||||
Button
|
||||
::
|
||||
|
||||
Besides all the colors from the `ui.colors` object, you can also use the `white` and `gray` and `black` colors with their pre-defined variants.
|
||||
|
||||
#### White
|
||||
|
||||
::component-card
|
||||
---
|
||||
backgroundClass: 'bg-gray-50 dark:bg-gray-800'
|
||||
props:
|
||||
color: 'white'
|
||||
variant: 'solid'
|
||||
ui:
|
||||
variant:
|
||||
solid: 1
|
||||
ghost: 1
|
||||
excludedProps:
|
||||
- color
|
||||
---
|
||||
|
||||
Button
|
||||
::
|
||||
|
||||
#### Gray
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
color: 'gray'
|
||||
variant: 'solid'
|
||||
ui:
|
||||
variant:
|
||||
solid: 1
|
||||
ghost: 1
|
||||
link: 1
|
||||
excludedProps:
|
||||
- color
|
||||
---
|
||||
|
||||
Button
|
||||
::
|
||||
|
||||
#### Black
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
color: 'black'
|
||||
variant: 'solid'
|
||||
ui:
|
||||
variant:
|
||||
solid: 1
|
||||
link: 1
|
||||
excludedProps:
|
||||
- color
|
||||
---
|
||||
|
||||
Button
|
||||
::
|
||||
|
||||
### Size
|
||||
|
||||
Use the `size` prop to change the size of the Button.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
size: 'sm'
|
||||
---
|
||||
|
||||
Button
|
||||
::
|
||||
|
||||
### Icon
|
||||
|
||||
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
|
||||
|
||||
Use the `leading` and `trailing` props to set the icon position or the `leadingIcon` and `trailingIcon` props to set a different icon for each position.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
icon: 'i-heroicons-pencil-square'
|
||||
size: 'sm'
|
||||
color: 'primary'
|
||||
variant: 'solid'
|
||||
label: Button
|
||||
trailing: false
|
||||
excludedProps:
|
||||
- icon
|
||||
---
|
||||
::
|
||||
|
||||
The `label` as prop or slot is optional so you can use the Button as an icon-only button.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
icon: 'i-heroicons-pencil-square'
|
||||
size: 'sm'
|
||||
color: 'primary'
|
||||
square: true
|
||||
variant: 'solid'
|
||||
excludedProps:
|
||||
- icon
|
||||
- square
|
||||
---
|
||||
::
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` prop to disable the Button.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
disabled: true
|
||||
---
|
||||
|
||||
Button
|
||||
::
|
||||
|
||||
### Loading
|
||||
|
||||
Use the `loading` prop to show a loading icon and disable the Button.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
loading: true
|
||||
---
|
||||
|
||||
Button
|
||||
::
|
||||
|
||||
### Block
|
||||
|
||||
Use the `block` prop to make the Button fill the width of its container.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
block: true
|
||||
---
|
||||
|
||||
Button
|
||||
::
|
||||
|
||||
### Link
|
||||
|
||||
Use the `to` prop to make the Button a link.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
to: 'https://volta.net'
|
||||
target: '_blank'
|
||||
---
|
||||
|
||||
Button
|
||||
::
|
||||
|
||||
### Padded
|
||||
|
||||
Use the `padded` prop to remove the padding of the Button.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
padded: false
|
||||
baseProps:
|
||||
color: 'gray'
|
||||
variant: 'link'
|
||||
icon: 'i-heroicons-x-mark-20-solid'
|
||||
---
|
||||
::
|
||||
|
||||
### Square
|
||||
|
||||
Use the `square` prop to force the Button to have the same padding horizontally and vertically.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
square: true
|
||||
baseProps:
|
||||
label: 'Button'
|
||||
color: 'gray'
|
||||
variant: 'solid'
|
||||
---
|
||||
::
|
||||
|
||||
### Truncate
|
||||
|
||||
Use the `truncate` prop to truncate the label of the Button.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
truncate: true
|
||||
class: 'w-20'
|
||||
label: 'Button with a long text'
|
||||
excludedProps:
|
||||
- class
|
||||
---
|
||||
::
|
||||
|
||||
### Group
|
||||
|
||||
To stack buttons as a group, use the `ButtonGroup` component.
|
||||
|
||||
- To size all the buttons equally, pass the `size` prop
|
||||
- To adjust the rounded or the shadow around buttons, customize with `ui.buttonGroup.rounded` or `ui.buttonGroup.shadow`
|
||||
|
||||
::component-card{slug="ButtonGroup"}
|
||||
---
|
||||
props:
|
||||
size: 'sm'
|
||||
ui:
|
||||
size:
|
||||
xxs: ''
|
||||
xs: ''
|
||||
sm: ''
|
||||
md: ''
|
||||
lg: ''
|
||||
xl: ''
|
||||
code: |
|
||||
<UButton label="Action" color="white" />
|
||||
<UButton icon="i-heroicons-chevron-down-20-solid" color="gray" />
|
||||
---
|
||||
|
||||
#default
|
||||
:u-button{label="Action" color="white"}
|
||||
:u-button{icon="i-heroicons-chevron-down-20-solid" color="gray"}
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
56
docs/content/2.elements/4.dropdown.md
Normal file
56
docs/content/2.elements/4.dropdown.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
github: true
|
||||
headlessui:
|
||||
label: 'Menu'
|
||||
to: 'https://headlessui.com/vue/menu'
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:dropdown-example
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const items = [
|
||||
[{
|
||||
label: 'Profile',
|
||||
avatar: {
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
}
|
||||
}], [{
|
||||
label: 'Edit',
|
||||
icon: 'i-heroicons-pencil-square-20-solid'
|
||||
}, {
|
||||
label: 'Duplicate',
|
||||
icon: 'i-heroicons-document-duplicate-20-solid'
|
||||
}], [{
|
||||
label: 'Archive',
|
||||
icon: 'i-heroicons-archive-box-20-solid'
|
||||
}, {
|
||||
label: 'Move',
|
||||
icon: 'i-heroicons-arrow-right-circle-20-solid'
|
||||
}], [{
|
||||
label: 'Delete',
|
||||
icon: 'i-heroicons-trash-20-solid'
|
||||
}]
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDropdown :items="items" :popper="{ placement: 'bottom-start' }">
|
||||
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
</UDropdown>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
91
docs/content/3.forms/1.input.md
Normal file
91
docs/content/3.forms/1.input.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
name: 'input'
|
||||
---
|
||||
::
|
||||
|
||||
### Size
|
||||
|
||||
Use the `size` prop to change the size of the Input.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
name: 'input'
|
||||
props:
|
||||
size: 'sm'
|
||||
---
|
||||
::
|
||||
|
||||
### Icon
|
||||
|
||||
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
|
||||
|
||||
Use the `leading` and `trailing` props to set the icon position or the `leadingIcon` and `trailingIcon` props to set a different icon for each position.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
name: 'input'
|
||||
props:
|
||||
icon: 'i-heroicons-magnifying-glass-20-solid'
|
||||
appearance: 'white'
|
||||
size: 'sm'
|
||||
trailing: false
|
||||
placeholder: 'Search...'
|
||||
excludedProps:
|
||||
- icon
|
||||
- placeholder
|
||||
---
|
||||
::
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` prop to disable the Input.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
name: 'input'
|
||||
props:
|
||||
placeholder: 'Search...'
|
||||
appearance: 'white'
|
||||
disabled: true
|
||||
excludedProps:
|
||||
- placeholder
|
||||
---
|
||||
::
|
||||
|
||||
### Loading
|
||||
|
||||
Use the `loading` prop to show a loading icon and disable the Input.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
name: 'input'
|
||||
placeholder: 'Search'
|
||||
props:
|
||||
loading: true
|
||||
icon: 'i-heroicons-magnifying-glass-20-solid'
|
||||
excludedProps:
|
||||
- icon
|
||||
---
|
||||
::
|
||||
|
||||
### Group
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
20
docs/content/3.forms/2.textarea.md
Normal file
20
docs/content/3.forms/2.textarea.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
name: 'textarea'
|
||||
---
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
25
docs/content/3.forms/3.select.md
Normal file
25
docs/content/3.forms/3.select.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
name: 'select'
|
||||
modelValue: 'Canada'
|
||||
options:
|
||||
- 'United States'
|
||||
- 'Canada'
|
||||
- 'Mexico'
|
||||
---
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
185
docs/content/3.forms/4.select-menu.md
Normal file
185
docs/content/3.forms/4.select-menu.md
Normal file
@@ -0,0 +1,185 @@
|
||||
---
|
||||
github: true
|
||||
headlessui:
|
||||
label: 'Listbox'
|
||||
to: 'https://headlessui.com/vue/listbox'
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:select-menu-example-basic{class="max-w-[12rem] w-full"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu v-model="selected" :options="people" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
You can use multiple values but you have to override the `#label` slot and handle the display yourself.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:select-menu-example-multiple{class="max-w-[12rem] w-full"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu v-model="selected" :options="people" multiple>
|
||||
<template #label>
|
||||
<span v-if="selected.length" class="font-medium truncate">{{ selected.join(', ') }}</span>
|
||||
<span v-else class="block truncate text-gray-400 dark:text-gray-500">Select people</span>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
You can also override the default slot entirely.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:select-menu-example-button{class="max-w-[12rem] w-full"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[3])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
|
||||
<UButton>
|
||||
{{ selected }}
|
||||
|
||||
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
|
||||
</UButton>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:select-menu-example-objects{class="max-w-[12rem] w-full"}
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const people = [{
|
||||
id: 'benjamincanac',
|
||||
label: 'benjamincanac',
|
||||
href: 'https://github.com/benjamincanac',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
|
||||
},
|
||||
{
|
||||
id: 'Atinux',
|
||||
label: 'Atinux',
|
||||
href: 'https://github.com/Atinux',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
|
||||
},
|
||||
{
|
||||
id: 'smarroufin',
|
||||
label: 'smarroufin',
|
||||
href: 'https://github.com/smarroufin',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' }
|
||||
},
|
||||
{
|
||||
id: 'nobody',
|
||||
label: 'Nobody',
|
||||
icon: 'i-heroicons-user-circle'
|
||||
}]
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu v-model="selected" :options="people">
|
||||
<template #label>
|
||||
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" />
|
||||
|
||||
{{ selected.label }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### Icon
|
||||
|
||||
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'max-w-[12rem] w-full'
|
||||
placeholder: 'Select a person'
|
||||
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
props:
|
||||
icon: 'i-heroicons-magnifying-glass-20-solid'
|
||||
excludedProps:
|
||||
- icon
|
||||
---
|
||||
::
|
||||
|
||||
### Search
|
||||
|
||||
Use the `searchable` prop to enable search.
|
||||
|
||||
This will use HeadlessUI [Combobox](https://headlessui.com/vue/combobox) component instead of [Listbox](https://headlessui.com/vue/listbox).
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'max-w-[12rem] w-full'
|
||||
placeholder: 'Select a person'
|
||||
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
props:
|
||||
searchable: true
|
||||
---
|
||||
::
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` prop to disable the SelectMenu.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'max-w-[12rem] w-full'
|
||||
placeholder: 'Select a person'
|
||||
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
props:
|
||||
disabled: true
|
||||
---
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
16
docs/content/3.forms/5.checkbox.md
Normal file
16
docs/content/3.forms/5.checkbox.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-card
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
16
docs/content/3.forms/6.radio.md
Normal file
16
docs/content/3.forms/6.radio.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-card
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
19
docs/content/3.forms/7.toggle.md
Normal file
19
docs/content/3.forms/7.toggle.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
github: true
|
||||
headlessui:
|
||||
label: 'Switch'
|
||||
to: 'https://headlessui.com/vue/switch'
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-card
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
46
docs/content/4.navigation/1.vertical-navigation.md
Normal file
46
docs/content/4.navigation/1.vertical-navigation.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:vertical-navigation-example
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const links = [{
|
||||
label: 'Profile',
|
||||
avatar: {
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
}
|
||||
}, {
|
||||
label: 'Installation',
|
||||
icon: 'i-heroicons-home',
|
||||
to: '/getting-started/installation'
|
||||
}, {
|
||||
label: 'Vertical Navigation',
|
||||
icon: 'i-heroicons-chart-bar',
|
||||
to: '/navigation/vertical-navigation'
|
||||
}, {
|
||||
label: 'Command Palette',
|
||||
icon: 'i-heroicons-command-line',
|
||||
to: '/navigation/command-palette'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation :links="links" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
273
docs/content/4.navigation/2.command-palette.md
Normal file
273
docs/content/4.navigation/2.command-palette.md
Normal file
@@ -0,0 +1,273 @@
|
||||
---
|
||||
github: true
|
||||
headlessui:
|
||||
label: 'Combobox'
|
||||
to: 'https://headlessui.com/vue/combobox'
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Use a `v-model` to display a searchable and selectable list of commands.
|
||||
|
||||
::component-example
|
||||
---
|
||||
padding: false
|
||||
---
|
||||
|
||||
#default
|
||||
:command-palette-example-basic{class="h-[257px]"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const people = [
|
||||
{ id: 1, label: 'Wade Cooper' },
|
||||
{ id: 2, label: 'Arlene Mccoy' },
|
||||
{ id: 3, label: 'Devon Webb' },
|
||||
{ id: 4, label: 'Tom Cook' },
|
||||
{ id: 5, label: 'Tanya Fox' },
|
||||
{ id: 6, label: 'Hellen Schmidt' },
|
||||
{ id: 7, label: 'Caroline Schultz' },
|
||||
{ id: 8, label: 'Mason Heaney' },
|
||||
{ id: 9, label: 'Claudie Smitham' },
|
||||
{ id: 10, label: 'Emil Schaefer' }
|
||||
]
|
||||
|
||||
const selected = ref([people[3]])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCommandPalette
|
||||
v-model="selected"
|
||||
multiple
|
||||
nullable
|
||||
:groups="[{ key: 'people', commands: people }]"
|
||||
:fuse="{ resultLimit: 6 }"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
You can put a `CommandPalette` anywhere you want but it's most commonly used inside of a modal.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:command-palette-example-modal
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const open = ref(false)
|
||||
|
||||
const people = [
|
||||
{ id: 1, label: 'Wade Cooper' },
|
||||
{ id: 2, label: 'Arlene Mccoy' },
|
||||
{ id: 3, label: 'Devon Webb' },
|
||||
{ id: 4, label: 'Tom Cook' },
|
||||
{ id: 5, label: 'Tanya Fox' },
|
||||
{ id: 6, label: 'Hellen Schmidt' },
|
||||
{ id: 7, label: 'Caroline Schultz' },
|
||||
{ id: 8, label: 'Mason Heaney' },
|
||||
{ id: 9, label: 'Claudie Smitham' },
|
||||
{ id: 10, label: 'Emil Schaefer' }
|
||||
]
|
||||
|
||||
const selected = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UButton label="Open" @click="open = true" />
|
||||
|
||||
<UModal v-model="open">
|
||||
<UCommandPalette
|
||||
v-model="selected"
|
||||
multiple
|
||||
nullable
|
||||
:groups="[{ key: 'people', commands: people }]"
|
||||
/>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
You can pass multiple groups of commands to the component. Each group will be separated by a divider and will display a label.
|
||||
|
||||
Without a `v-model`, you can also listen on `@update:model-value` to navigate to a link or do something else when a command is clicked.
|
||||
|
||||
::component-example
|
||||
---
|
||||
padding: false
|
||||
---
|
||||
|
||||
#default
|
||||
:command-palette-example-groups{class="h-[274px]"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const router = useRouter()
|
||||
|
||||
const commandPaletteRef = ref()
|
||||
|
||||
const users = [
|
||||
{ id: 'benjamincanac', label: 'benjamincanac', href: 'https://github.com/benjamincanac', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' } },
|
||||
{ id: 'Atinux', label: 'Atinux', href: 'https://github.com/Atinux', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' } },
|
||||
{ id: 'smarroufin', label: 'smarroufin', href: 'https://github.com/smarroufin', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' } }
|
||||
]
|
||||
|
||||
const actions = [
|
||||
{ id: 'new-file', label: 'Add new file', icon: 'i-heroicons-document-plus', click: () => alert('New file') },
|
||||
{ id: 'new-folder', label: 'Add new folder', icon: 'i-heroicons-folder-plus', click: () => alert('New folder') },
|
||||
{ id: 'hashtag', label: 'Add hashtag', icon: 'i-heroicons-hashtag', click: () => alert('Add hashtag') },
|
||||
{ id: 'label', label: 'Add label', icon: 'i-heroicons-tag', click: () => alert('Add label') }
|
||||
]
|
||||
|
||||
const groups = computed(() => commandPaletteRef.value?.query
|
||||
? [{
|
||||
key: 'users',
|
||||
commands: users
|
||||
}]
|
||||
: [{
|
||||
key: 'recent',
|
||||
label: 'Recent searches',
|
||||
commands: users.slice(0, 1)
|
||||
}, {
|
||||
key: 'actions',
|
||||
commands: actions
|
||||
}])
|
||||
|
||||
function onSelect (option) {
|
||||
if (option.click) {
|
||||
option.click()
|
||||
} else if (option.to) {
|
||||
router.push(option.to)
|
||||
} else if (option.href) {
|
||||
window.open(option.href, '_blank')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCommandPalette ref="commandPaletteRef" :groups="groups" @update:model-value="onSelect" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### Icon
|
||||
|
||||
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
padding: false
|
||||
baseProps:
|
||||
empty: null
|
||||
props:
|
||||
icon: 'i-heroicons-command-line'
|
||||
excludedProps:
|
||||
- icon
|
||||
---
|
||||
::
|
||||
|
||||
### Placeholder
|
||||
|
||||
Use the `placeholder` prop to change the input placeholder
|
||||
|
||||
::component-card
|
||||
---
|
||||
padding: false
|
||||
baseProps:
|
||||
empty: null
|
||||
props:
|
||||
placeholder: 'Type a command...'
|
||||
excludedProps:
|
||||
- icon
|
||||
---
|
||||
::
|
||||
|
||||
### Close
|
||||
|
||||
Use the `close` prop to display a close button on the right side of the input.
|
||||
|
||||
You can pass all the props of the [Button](/elements/button) component to customize it through the `close` prop or globally through `ui.commandPalette.default.close`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
padding: false
|
||||
baseProps:
|
||||
empty: null
|
||||
props:
|
||||
close:
|
||||
icon: 'i-heroicons-x-mark-20-solid'
|
||||
color: 'gray'
|
||||
variant: 'link'
|
||||
padded: false
|
||||
excludedProps:
|
||||
- close
|
||||
---
|
||||
::
|
||||
|
||||
### Empty
|
||||
|
||||
Use the `empty` prop to display a message when there are no results.
|
||||
|
||||
You can pass an `object` through the `empty` prop or globally through `ui.commandPalette.default.empty`. Here is the default:
|
||||
|
||||
::component-card
|
||||
---
|
||||
padding: false
|
||||
baseProps:
|
||||
placeholder: 'Type something to see the empty label change'
|
||||
props:
|
||||
empty:
|
||||
icon: 'i-heroicons-magnifying-glass-20-solid'
|
||||
label: "We couldn't find any items."
|
||||
queryLabel: "We couldn't find any items with that term. Please try again."
|
||||
excludedProps:
|
||||
- empty
|
||||
---
|
||||
::
|
||||
|
||||
## Themes
|
||||
|
||||
Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do.
|
||||
|
||||
### Algolia
|
||||
|
||||
::component-example
|
||||
---
|
||||
padding: false
|
||||
---
|
||||
|
||||
#default
|
||||
:command-palette-theme-algolia{class="max-h-[480px] rounded-md"}
|
||||
::
|
||||
|
||||
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/docs/rework/docs/components/content/themes/CommandPaletteThemeAlgolia.vue#L23"}
|
||||
Take a look at the component!
|
||||
::
|
||||
|
||||
### Raycast
|
||||
|
||||
::component-example
|
||||
---
|
||||
padding: false
|
||||
---
|
||||
|
||||
#default
|
||||
:command-palette-theme-raycast{class="max-h-[480px] rounded-md"}
|
||||
::
|
||||
|
||||
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/docs/rework/docs/components/content/themes/CommandPaletteThemeRaycast.vue#L30"}
|
||||
Take a look at the component!
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
72
docs/content/5.overlays/1.modal.md
Normal file
72
docs/content/5.overlays/1.modal.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
github: true
|
||||
headlessui:
|
||||
label: 'Dialog'
|
||||
to: 'https://headlessui.com/vue/dialog'
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:modal-example-basic
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UButton label="Open" @click="open = true" />
|
||||
|
||||
<UModal v-model="open">
|
||||
<!-- Content -->
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
You can put a [Card](/layout/card) component inside your Modal to handle forms and take advantage of `header` and `footer` slots:
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:modal-example-card
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UButton label="Open" @click="open = true" />
|
||||
|
||||
<UModal v-model="open">
|
||||
<UCard :ui="{ divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<!-- Content -->
|
||||
</template>
|
||||
|
||||
<!-- Content -->
|
||||
|
||||
<template #footer>
|
||||
<!-- Content -->
|
||||
</template>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
37
docs/content/5.overlays/2.slideover.md
Normal file
37
docs/content/5.overlays/2.slideover.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
github: true
|
||||
headlessui:
|
||||
label: 'Dialog'
|
||||
to: 'https://headlessui.com/vue/dialog'
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:slideover-example
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UButton label="Open" @click="open = true" />
|
||||
|
||||
<USlideover v-model="open">
|
||||
<!-- Content -->
|
||||
</USlideover>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
33
docs/content/5.overlays/3.popover.md
Normal file
33
docs/content/5.overlays/3.popover.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
github: true
|
||||
headlessui:
|
||||
label: 'Popover'
|
||||
to: 'https://headlessui.com/vue/popover'
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:popover-example
|
||||
#code
|
||||
```vue
|
||||
<template>
|
||||
<UPopover>
|
||||
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
|
||||
<template #panel>
|
||||
<!-- Content -->
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
26
docs/content/5.overlays/4.tooltip.md
Normal file
26
docs/content/5.overlays/4.tooltip.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:tooltip-example
|
||||
#code
|
||||
```vue
|
||||
<template>
|
||||
<UTooltip text="Tooltip">
|
||||
<UButton color="gray" label="Button" />
|
||||
</UTooltip>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
49
docs/content/5.overlays/5.context-menu.md
Normal file
49
docs/content/5.overlays/5.context-menu.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:context-menu-example
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const { x, y } = useMouse()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
|
||||
|
||||
function openContextMenu () {
|
||||
const top = unref(y)
|
||||
const left = unref(x)
|
||||
|
||||
virtualElement.value.getBoundingClientRect = () => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
top,
|
||||
left
|
||||
})
|
||||
|
||||
isOpen.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @contextmenu.prevent="openContextMenu">
|
||||
<UContextMenu v-model="isOpen" :virtual-element="virtualElement" width-class="w-48">
|
||||
<!-- Content -->
|
||||
</UContextMenu>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
249
docs/content/5.overlays/6.notification.md
Normal file
249
docs/content/5.overlays/6.notification.md
Normal file
@@ -0,0 +1,249 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
First of all, add the `Notifications` component to your app, preferably inside `app.vue`.
|
||||
|
||||
This component will render by default the notifications at the bottom right of the screen. You can configure its behaviour in the `app.config.ts` through `ui.notifications`.
|
||||
|
||||
```vue [app.vue]
|
||||
<template>
|
||||
<div>
|
||||
<UContainer>
|
||||
<NuxtPage />
|
||||
</UContainer>
|
||||
|
||||
<UNotifications />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Then, you can use the `useToast` composable to add notifications to your app:
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:notification-example-basic
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const toast = useToast()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Show toast" @click="toast.add({ title: 'Hello world!' })" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### Description
|
||||
|
||||
You can add a `description` in addition of the `title`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
id: 2
|
||||
timeout: 0
|
||||
props:
|
||||
title: 'Notification'
|
||||
description: 'This is a notification.'
|
||||
---
|
||||
::
|
||||
|
||||
### Icon
|
||||
|
||||
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
id: 3
|
||||
timeout: 0
|
||||
title: 'Notification'
|
||||
props:
|
||||
icon: 'i-heroicons-check-circle'
|
||||
description: 'This is a notification.'
|
||||
excludedProps:
|
||||
- icon
|
||||
---
|
||||
::
|
||||
|
||||
### Avatar
|
||||
|
||||
Use the [avatar](/elements/avatar) prop as an `object` and configure it with any of its props.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
id: 4
|
||||
timeout: 0
|
||||
title: 'Notification'
|
||||
description: 'This is a notification.'
|
||||
props:
|
||||
avatar:
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
excludedProps:
|
||||
- avatar
|
||||
---
|
||||
::
|
||||
|
||||
### Timeout
|
||||
|
||||
Use the `timeout` prop to configure how long the Notification will remain. Set it to `0` to disable the timeout.
|
||||
|
||||
You will see a progress bar at the bottom of the Notification which will indicate the remaining time. When hovering the Notification, the progress bar will be paused.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
id: 5
|
||||
title: 'Notification'
|
||||
description: 'This is a notification.'
|
||||
props:
|
||||
timeout: 10000
|
||||
---
|
||||
::
|
||||
|
||||
### Click
|
||||
|
||||
Use the `click` prop to execute a function when the Notification is clicked.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:notification-example-click
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const toast = useToast()
|
||||
|
||||
function onClick () {
|
||||
alert('Clicked!')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Show toast" @click="toast.add({ title: 'Click me', click: onClick })" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### Callback
|
||||
|
||||
Use the `callback` prop to execute a function when the Notification expires.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:notification-example-callback
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const toast = useToast()
|
||||
|
||||
function onCallback () {
|
||||
alert('Notification expired!')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Show toast" @click="toast.add({ title: 'Expires soon...', timeout: 1000, callback: onCallback })" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### Close
|
||||
|
||||
Use the `close` prop to hide or customize the close button on the Notification.
|
||||
|
||||
You can pass all the props of the [Button](/elements/button) component to customize it through the `close` prop or globally through `ui.notifications.default.close`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
id: 6
|
||||
title: 'Notification'
|
||||
timeout: 0
|
||||
props:
|
||||
close:
|
||||
icon: 'i-heroicons-archive-box-x-mark'
|
||||
color: 'primary'
|
||||
variant: 'outline'
|
||||
padded: true
|
||||
size: '2xs'
|
||||
ui:
|
||||
rounded: 'rounded-full'
|
||||
excludedProps:
|
||||
- close
|
||||
---
|
||||
::
|
||||
|
||||
### Actions
|
||||
|
||||
Use the `actions` prop to add actions to the Notification.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:notification-example-actions
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const toast = useToast()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Show toast" @click="toast.add({ title: 'Click me', click: () => alert('Clicked!') })" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
Like for `close`, you can pass all the props of the [Button](/elements/button) component inside the action or globally through `ui.notifications.default.action`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
id: 6
|
||||
title: 'Notification'
|
||||
timeout: 0
|
||||
props:
|
||||
actions:
|
||||
- variant: 'ghost'
|
||||
color: 'gray'
|
||||
label: Action 1
|
||||
- variant: 'solid'
|
||||
color: 'gray'
|
||||
label: Action 2
|
||||
excludedProps:
|
||||
- actions
|
||||
---
|
||||
::
|
||||
|
||||
Actions will render differently whether you have a `description` set.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
id: 6
|
||||
title: 'Notification'
|
||||
description: 'This is a notification.'
|
||||
timeout: 0
|
||||
props:
|
||||
actions:
|
||||
- variant: 'solid'
|
||||
color: 'primary'
|
||||
label: Action 1
|
||||
- variant: 'outline'
|
||||
color: 'primary'
|
||||
label: Action 2
|
||||
excludedProps:
|
||||
- actions
|
||||
---
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
35
docs/content/6.layout/1.card.md
Normal file
35
docs/content/6.layout/1.card.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:card-example{class="w-full"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<template>
|
||||
<UCard>
|
||||
<template #header />
|
||||
|
||||
Body
|
||||
|
||||
<template #footer />
|
||||
</UCard>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Slots
|
||||
|
||||
:component-slots
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
25
docs/content/6.layout/2.container.md
Normal file
25
docs/content/6.layout/2.container.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
github: true
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:container-example{class="w-full"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<template>
|
||||
<UContainer>Content</UContainer>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
19
docs/layouts/default.vue
Normal file
19
docs/layouts/default.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="relative grid lg:grid-cols-10 lg:gap-8">
|
||||
<DocsAside class="lg:col-span-2" />
|
||||
|
||||
<div class="relative lg:col-span-6 pt-8 pb-16">
|
||||
<DocsPageHeader />
|
||||
|
||||
<div class="prose prose-primary dark:prose-invert max-w-none">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-200 dark:border-gray-800 my-12">
|
||||
|
||||
<DocsPrevNext />
|
||||
</div>
|
||||
|
||||
<DocsToc class="lg:col-span-2 order-first lg:order-last" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,39 +1,38 @@
|
||||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
import nuxtUI from '../src/module'
|
||||
import ui from '../src/module'
|
||||
|
||||
// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
|
||||
export default defineNuxtConfig({
|
||||
app: {
|
||||
head: {
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
modules: [
|
||||
// @ts-ignore
|
||||
nuxtUI
|
||||
ui,
|
||||
'@vueuse/nuxt',
|
||||
'@nuxt/content',
|
||||
'@nuxtjs/plausible',
|
||||
'nuxt-lodash',
|
||||
'nuxt-component-meta'
|
||||
],
|
||||
components: {
|
||||
global: true
|
||||
content: {
|
||||
documentDriven: true,
|
||||
highlight: {
|
||||
theme: {
|
||||
light: 'material-lighter',
|
||||
dark: 'material-palenight'
|
||||
},
|
||||
preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'diff', 'shell', 'markdown', 'yaml', 'bash', 'ini']
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'blue',
|
||||
gray: 'zinc'
|
||||
global: true,
|
||||
icons: ['heroicons', 'simple-icons']
|
||||
},
|
||||
preset: {
|
||||
typescript: {
|
||||
strict: false,
|
||||
includeWorkspace: true
|
||||
},
|
||||
icons: ['heroicons', 'mdi'],
|
||||
tailwindcss: {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter var', ...defaultTheme.fontFamily.sans]
|
||||
}
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
$production: {
|
||||
routeRules: {
|
||||
'/api/_content/**': { isr: true, static: true },
|
||||
'/api/component-meta/**': { isr: true, static: true }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,509 +0,0 @@
|
||||
<!-- eslint-disable vue/no-template-shadow -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<template>
|
||||
<UCard v-if="component" class="relative flex flex-col" body-class="px-4 py-5 sm:p-6 relative" footer-class="flex flex-col flex-1 overflow-hidden">
|
||||
<div class="flex justify-center">
|
||||
<component :is="`U${defaultProps[params.component].component.name}`" v-if="defaultProps[params.component] && defaultProps[params.component].component" v-bind="defaultProps[params.component].component.props" />
|
||||
|
||||
<component :is="is" v-bind="{ ...boundProps, ...eventProps }">
|
||||
<template v-for="[key, slot] of Object.entries(defaultProps[params.component]?.slots || {}) || []" #[key]>
|
||||
<template v-if="Array.isArray(slot)">
|
||||
<div :key="key">
|
||||
<component
|
||||
:is="slot.component ? `U${slot.component.name}` : slot.tag"
|
||||
v-for="(slot, index) of slot"
|
||||
:key="index"
|
||||
:class="slot.class"
|
||||
v-bind="slot.component?.props || defaultProps[slot.component]"
|
||||
v-html="slot.html"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<component :is="`U${slot.component.name}`" v-if="slot.component" :key="`${key}-component`" v-bind="slot.component?.props || defaultProps[slot.component]" />
|
||||
<component :is="slot.tag" v-else :key="`${key}-tag`" :class="slot.class" v-html="slot.html" />
|
||||
</template>
|
||||
</template>
|
||||
</component>
|
||||
</div>
|
||||
|
||||
<template v-if="props.length" #footer>
|
||||
<div class="border-b u-border-gray-200">
|
||||
<pre class="text-sm leading-6 u-text-gray-900 flex-1 relative flex ligatures-none overflow-x-hidden px-4 sm:px-6 py-5 sm:py-6">
|
||||
<code class="flex-none min-w-full whitespace-pre-wrap break-all">{{ code }}</code>
|
||||
|
||||
<UButton
|
||||
class="absolute top-0 right-0"
|
||||
:icon="copied ? 'i-heroicons-clipboard-document-check' : 'i-heroicons-clipboard-document'"
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
:custom-class="copied ? '!text-green-500' : ''"
|
||||
@click="onCopy"
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 px-4 py-5 sm:p-6 space-y-3">
|
||||
<UFormGroup
|
||||
v-for="prop of props"
|
||||
:key="prop.key"
|
||||
class="capitalize"
|
||||
:name="prop.key"
|
||||
:label="prop.key"
|
||||
>
|
||||
<UToggle
|
||||
v-if="prop.type === 'Boolean'"
|
||||
v-model="prop.value"
|
||||
:name="prop.key"
|
||||
/>
|
||||
<USelect
|
||||
v-else-if="prop.values"
|
||||
v-model="prop.value"
|
||||
:name="prop.key"
|
||||
placeholder="Choose one..."
|
||||
:options="prop.values"
|
||||
size="sm"
|
||||
/>
|
||||
<UInput
|
||||
v-else-if="prop.type === 'String'"
|
||||
v-model="prop.value"
|
||||
:name="prop.key"
|
||||
size="sm"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<UInput
|
||||
v-else-if="prop.type === 'Number'"
|
||||
v-model="prop.value"
|
||||
type="number"
|
||||
:name="prop.key"
|
||||
size="sm"
|
||||
/>
|
||||
<UTextarea
|
||||
v-else
|
||||
:model-value="prop.value && JSON.stringify(prop.value)"
|
||||
:name="prop.key"
|
||||
size="sm"
|
||||
:rows="8"
|
||||
autoresize
|
||||
@update:model-value="value => prop.value = JSON.parse(value)"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
const { params } = useRoute()
|
||||
|
||||
const is = `U${params.component}`
|
||||
|
||||
const component = nuxtApp.vueApp.component(is)
|
||||
|
||||
const people = [
|
||||
{ id: 1, label: 'Durward Reynolds', disabled: false },
|
||||
{ id: 2, label: 'Kenton Towne', disabled: false },
|
||||
{ id: 3, label: 'Therese Wunsch', disabled: false },
|
||||
{ id: 4, label: 'Benedict Kessler', disabled: true },
|
||||
{ id: 5, label: 'Katelyn Rohan', disabled: false }
|
||||
]
|
||||
|
||||
const selectCustom = ref(people[0])
|
||||
const commandPalette = ref(people[0])
|
||||
const alertDialog = ref(false)
|
||||
const toggle = ref(false)
|
||||
const modal = ref(false)
|
||||
const slideover = ref(false)
|
||||
|
||||
const x = ref(0)
|
||||
const y = ref(0)
|
||||
const isContextMenuOpen = ref(false)
|
||||
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', ({ clientX, clientY }) => {
|
||||
x.value = clientX
|
||||
y.value = clientY
|
||||
})
|
||||
})
|
||||
|
||||
function openContextMenu () {
|
||||
const top = unref(y)
|
||||
const left = unref(x)
|
||||
|
||||
virtualElement.value.getBoundingClientRect = () => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
top,
|
||||
left
|
||||
})
|
||||
isContextMenuOpen.value = true
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
Button: {
|
||||
label: 'Button text'
|
||||
},
|
||||
Badge: {
|
||||
label: 'Badge'
|
||||
},
|
||||
Alert: {
|
||||
title: 'A new software update is available. See what’s new in version 2.0.4.'
|
||||
},
|
||||
AlertDialog: {
|
||||
icon: 'i-heroicons-exclamation-triangle',
|
||||
title: 'Deactivate account',
|
||||
description: 'Are you sure you want to deactivate your account? All of your data will be permanently removed from our servers forever. This action cannot be undone.',
|
||||
modelValue: alertDialog,
|
||||
'onUpdate:modelValue': (v) => { alertDialog.value = v },
|
||||
component: {
|
||||
name: 'Button',
|
||||
props: {
|
||||
variant: 'secondary',
|
||||
label: 'Open modal',
|
||||
onClick: () => { alertDialog.value = true }
|
||||
}
|
||||
}
|
||||
},
|
||||
Avatar: {
|
||||
src: 'https://picsum.photos/200/300'
|
||||
},
|
||||
AvatarGroup: {
|
||||
group: ['https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', 'https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.25&w=256&h=256&q=80', 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80']
|
||||
},
|
||||
Dropdown: {
|
||||
items: [
|
||||
[{
|
||||
label: 'Edit',
|
||||
icon: 'i-heroicons-pencil-square-20-solid'
|
||||
}, {
|
||||
label: 'Duplicate',
|
||||
icon: 'i-heroicons-document-duplicate-20-solid'
|
||||
}],
|
||||
[{
|
||||
label: 'Archive',
|
||||
icon: 'i-heroicons-archive-box-20-solid'
|
||||
}, {
|
||||
label: 'Move',
|
||||
icon: 'i-heroicons-arrow-right-circle-20-solid'
|
||||
}],
|
||||
[{
|
||||
label: 'Delete',
|
||||
icon: 'i-heroicons-trash-20-solid'
|
||||
}]
|
||||
]
|
||||
},
|
||||
VerticalNavigation: {
|
||||
links: [
|
||||
{
|
||||
label: 'Home',
|
||||
icon: 'i-heroicons-home',
|
||||
to: '/'
|
||||
},
|
||||
{
|
||||
label: 'Examples',
|
||||
icon: 'i-heroicons-book-open',
|
||||
to: '/examples'
|
||||
},
|
||||
{
|
||||
label: 'Migration',
|
||||
icon: 'i-heroicons-arrow-path',
|
||||
to: '/migration'
|
||||
},
|
||||
{
|
||||
label: 'External link',
|
||||
icon: 'i-heroicons-link',
|
||||
to: 'https://google.fr',
|
||||
target: '_blank'
|
||||
}
|
||||
]
|
||||
},
|
||||
CommandPalette: {
|
||||
modelValue: commandPalette,
|
||||
'onUpdate:modelValue': (v) => { commandPalette.value = v },
|
||||
groups: [{
|
||||
key: 'people',
|
||||
label: 'People',
|
||||
commands: people
|
||||
}]
|
||||
},
|
||||
Icon: {
|
||||
name: 'i-heroicons-bell'
|
||||
},
|
||||
Input: {
|
||||
name: 'input',
|
||||
placeholder: 'Enter text'
|
||||
},
|
||||
FormGroup: {
|
||||
name: 'input',
|
||||
label: 'Input group',
|
||||
slots: {
|
||||
default: {
|
||||
component: {
|
||||
name: 'Input',
|
||||
props: {
|
||||
name: 'input',
|
||||
placeholder: 'Works with every form element'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Toggle: {
|
||||
modelValue: toggle,
|
||||
'onUpdate:modelValue': (v) => { toggle.value = v }
|
||||
},
|
||||
Checkbox: {
|
||||
name: 'checkbox'
|
||||
},
|
||||
Radio: {
|
||||
name: 'radio'
|
||||
},
|
||||
Select: {
|
||||
name: 'select',
|
||||
options: ['English', 'Spanish', 'French', 'German', 'Chinese']
|
||||
},
|
||||
SelectCustom: {
|
||||
modelValue: selectCustom,
|
||||
'onUpdate:modelValue': (v) => { selectCustom.value = v },
|
||||
textAttribute: 'label',
|
||||
options: people
|
||||
},
|
||||
Textarea: {
|
||||
name: 'textarea'
|
||||
},
|
||||
Tooltip: {
|
||||
text: 'Tooltip text'
|
||||
},
|
||||
Notification: {
|
||||
id: '1',
|
||||
title: 'Notification title',
|
||||
callback: 'console.log(\'Timer expired\')'
|
||||
},
|
||||
ContextMenu: {
|
||||
modelValue: isContextMenuOpen,
|
||||
'onUpdate:modelValue': (v) => { isContextMenuOpen.value = v },
|
||||
virtualElement,
|
||||
component: {
|
||||
name: 'Card',
|
||||
props: {
|
||||
variant: 'secondary',
|
||||
label: 'Open context menu',
|
||||
onClick: () => { isContextMenuOpen.value = false },
|
||||
onContextmenu: (e) => {
|
||||
e?.preventDefault()
|
||||
openContextMenu()
|
||||
},
|
||||
class: 'relative w-[300px] h-[100px]'
|
||||
}
|
||||
},
|
||||
slots: {
|
||||
default: {
|
||||
tag: 'div',
|
||||
html: 'Context menu content',
|
||||
class: 'rounded border u-border-gray-200 p-2'
|
||||
}
|
||||
}
|
||||
},
|
||||
Modal: {
|
||||
modelValue: modal,
|
||||
'onUpdate:modelValue': (v) => { modal.value = v },
|
||||
component: {
|
||||
name: 'Button',
|
||||
props: {
|
||||
variant: 'secondary',
|
||||
label: 'Open modal',
|
||||
onClick: () => { modal.value = true }
|
||||
}
|
||||
},
|
||||
slots: {
|
||||
default: {
|
||||
tag: 'div',
|
||||
html: 'Modal content'
|
||||
},
|
||||
footer: {
|
||||
component: {
|
||||
name: 'Button',
|
||||
props: {
|
||||
label: 'Close',
|
||||
onClick: () => { modal.value = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Slideover: {
|
||||
modelValue: slideover,
|
||||
'onUpdate:modelValue': (v) => { slideover.value = v },
|
||||
component: {
|
||||
name: 'Button',
|
||||
props: {
|
||||
variant: 'secondary',
|
||||
label: 'Open slideover',
|
||||
onClick: () => { slideover.value = true }
|
||||
}
|
||||
},
|
||||
slots: {
|
||||
default: {
|
||||
tag: 'div',
|
||||
html: 'Slideover content'
|
||||
}
|
||||
}
|
||||
},
|
||||
Popover: {
|
||||
slots: {
|
||||
panel: {
|
||||
tag: 'div',
|
||||
class: 'u-bg-gray-100 rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 p-6',
|
||||
html: 'Popover content'
|
||||
}
|
||||
}
|
||||
},
|
||||
Tabs: {
|
||||
links: [{
|
||||
label: 'Usage',
|
||||
to: '/',
|
||||
exact: true
|
||||
}, {
|
||||
label: 'Examples',
|
||||
to: '/examples'
|
||||
}, {
|
||||
label: 'Migration',
|
||||
to: '/migration'
|
||||
}, {
|
||||
label: 'Tabs',
|
||||
to: '/components/Tabs'
|
||||
}]
|
||||
},
|
||||
Pills: {
|
||||
links: [{
|
||||
label: 'Usage',
|
||||
to: '/',
|
||||
exact: true
|
||||
}, {
|
||||
label: 'Examples',
|
||||
to: '/examples'
|
||||
}, {
|
||||
label: 'Migration',
|
||||
to: '/migration'
|
||||
}, {
|
||||
label: 'Pills',
|
||||
to: '/components/Pills'
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
const componentDefaultProps = defaultProps[params.component] || {}
|
||||
const { props: componentProps } = await component.__asyncLoader()
|
||||
|
||||
function lowercaseFirstLetter (string) {
|
||||
return string.charAt(0).toLowerCase() + string.slice(1)
|
||||
}
|
||||
|
||||
const refProps = Object.entries(componentProps).map(([key, prop]) => {
|
||||
const defaultValue = componentDefaultProps[key]
|
||||
const propDefault = (typeof prop.default === 'function' ? prop.default() : prop.default)
|
||||
let value = defaultValue !== undefined ? defaultValue : propDefault
|
||||
let type = prop.type
|
||||
if (Array.isArray(type)) {
|
||||
type = type[0].name
|
||||
} else {
|
||||
type = type.name
|
||||
}
|
||||
|
||||
let values
|
||||
if (prop.validator) {
|
||||
const arrayRegex = prop.validator.toString().match(/\[.*\]/g, '')
|
||||
if (arrayRegex) {
|
||||
values = JSON.parse(arrayRegex[0].replace(/'/g, '"')).filter(Boolean)
|
||||
} else {
|
||||
const $uiProp = $ui[lowercaseFirstLetter(params.component)][key]
|
||||
if ($uiProp) {
|
||||
values = Object.keys($uiProp).filter(Boolean)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
if (type === 'String' && typeof value === 'string') {
|
||||
value = value.replace(/^'(.*)'$/, '$1')
|
||||
} else if (type === 'Array') {
|
||||
value = JSON.stringify(value)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
type,
|
||||
value,
|
||||
values,
|
||||
default: propDefault
|
||||
}
|
||||
})
|
||||
|
||||
const eventProps = Object.entries(componentDefaultProps)
|
||||
.filter(([key]) => !refProps.find(prop => prop.key === key))
|
||||
.filter(([key]) => !['slots'].includes(key))
|
||||
.reduce((acc, [key, value]) => {
|
||||
acc[key] = value
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const props = ref(refProps)
|
||||
const boundProps = computed(() => {
|
||||
const bound = {}
|
||||
for (const prop of props.value) {
|
||||
let value = prop.value
|
||||
if (value === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
if (prop.type === 'Array') {
|
||||
value = JSON.parse(value)
|
||||
} else if (prop.type === 'Number') {
|
||||
value = Number(value)
|
||||
} else if (prop.type === 'Function') {
|
||||
// eslint-disable-next-line no-new-func
|
||||
value = Function(value)
|
||||
}
|
||||
|
||||
bound[prop.key] = value
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return bound
|
||||
})
|
||||
|
||||
function toKebabCase (str) {
|
||||
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
||||
}
|
||||
|
||||
const { copy, copied } = useClipboard({ copiedDuring: 2000 })
|
||||
const onCopy = () => {
|
||||
copy(code.value)
|
||||
}
|
||||
|
||||
const code = computed(() => {
|
||||
let code = `<U${params.component}`
|
||||
for (const prop of props.value) {
|
||||
if (prop.value === null) {
|
||||
continue
|
||||
}
|
||||
if (prop.value === prop.default) {
|
||||
continue
|
||||
}
|
||||
|
||||
const key = toKebabCase(prop.key)
|
||||
code += `\n ${prop.type === 'Boolean' ? ':' : ''}${key === 'model-value' ? 'v-model' : key}="${prop.value}"`
|
||||
}
|
||||
code += '\n/>'
|
||||
return code
|
||||
})
|
||||
</script>
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="pb-10 border-b u-border-gray-200 mb-10">
|
||||
<div>
|
||||
<h1 class="inline-block text-3xl font-extrabold u-text-gray-900 tracking-tight">
|
||||
Dark mode
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p class="mt-1 text-lg u-text-gray-500">
|
||||
Dark mode implementation with <a href="https://color-mode.nuxtjs.org/" target="_blank" class="underline">Color Mode</a> module.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="font-bold text-2xl u-text-gray-900">
|
||||
Usage
|
||||
</h2>
|
||||
|
||||
<p>TailwindCSS takes advantage of the `dark` class on the html tag:</p>
|
||||
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm">
|
||||
{{ code4 }}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>The `@nuxtjs/color-mode` module is now installed by default, you can easily implement a toggle button:</p>
|
||||
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm">
|
||||
{{ code1 }}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<h2 class="font-bold text-2xl u-text-gray-900">
|
||||
Shortcuts
|
||||
</h2>
|
||||
|
||||
<p>A number of shortcuts are available to make your life with colors easier.</p>
|
||||
|
||||
<p>For each color utilities: `bg`, `text`, `border`, `ring` and `divide`, shortcuts for `white`, `black` and `gray` colors are generated (based on your prefix `u` by default) that handles the dark mode automatically:</p>
|
||||
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm">
|
||||
{{ code3 }}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>For example `u-bg-gray-100` is a shortcut for `bg-gray-100 dark:bg-gray-800`. Take a look at the <a href="https://github.com/nuxtlabs/ui/blob/dev/src/index.ts#L61" target="_blank" class="underline">shortcuts definitions</a>.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const code1 = `
|
||||
const colorMode = useColorMode()
|
||||
const toggleDark = () => {
|
||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||
}`
|
||||
const code3 = `
|
||||
<div class="u-bg-gray-100 border u-border-gray-200 u-text-gray-700"></div>
|
||||
`
|
||||
const code4 = `
|
||||
<div class="bg-white dark:bg-black"></div>
|
||||
`
|
||||
</script>
|
||||
@@ -1,367 +0,0 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="pb-10 border-b u-border-gray-200 mb-10">
|
||||
<div>
|
||||
<h1 class="inline-block text-3xl font-extrabold u-text-gray-900 tracking-tight">
|
||||
Examples
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p class="mt-1 text-lg u-text-gray-500">
|
||||
Examples of real-life usage of components.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Avatar:
|
||||
</div>
|
||||
|
||||
<UAvatar src="https://picsum.photos/200/300" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Button:
|
||||
</div>
|
||||
|
||||
<UButton variant="primary" icon="i-heroicons-envelope">
|
||||
Button text
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Modal:
|
||||
</div>
|
||||
|
||||
<UButton @click="toggleModalIsOpen()">
|
||||
Toggle modal!
|
||||
</UButton>
|
||||
|
||||
<UModal v-model="isModalOpen" @submit.prevent="onSubmit">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<UIcon name="i-heroicons-exclamation" class="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium u-text-gray-900">
|
||||
Deactivate account
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm u-text-gray-500">
|
||||
Are you sure you want to deactivate your account? All of your data will be permanently removed from our servers forever. This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm" @click="open = false">
|
||||
Deactivate
|
||||
</button>
|
||||
<button ref="cancelButtonRef" type="button" class="mt-3 w-full inline-flex justify-center rounded-md border u-border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium u-text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm" @click="open = false">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</UModal>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Dropdown:
|
||||
</div>
|
||||
|
||||
<UDropdown v-slot="{ open }" :items="dropdownItems" placement="bottom-start">
|
||||
<UButton variant="white" :icon="open ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'" trailing icon-class="transition">
|
||||
Open menu!
|
||||
</UButton>
|
||||
</UDropdown>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Dropdown with avatar:
|
||||
</div>
|
||||
|
||||
<UDropdown :items="customDropdownItems" placement="bottom-end">
|
||||
<button class="flex">
|
||||
<UAvatar src="https://picsum.photos/200/300" />
|
||||
</button>
|
||||
</UDropdown>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Popover:
|
||||
</div>
|
||||
|
||||
<UPopover wrapper-class="inline-block relative">
|
||||
<template #default="{ open }">
|
||||
<UButton variant="secondary" :icon="open ? 'i-heroicons-chevron-up-20-solid-20' : 'i-heroicons-chevron-down-20-solid'" trailing icon-class="transition">
|
||||
Open popover!
|
||||
</UButton>
|
||||
</template>
|
||||
|
||||
<template #panel>
|
||||
<div class="p-2">
|
||||
Panel
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Tooltip:
|
||||
</div>
|
||||
|
||||
<UTooltip text="Hello tooltip!" :shortcuts="['⌘', 'G']">
|
||||
<UIcon name="i-heroicons-information-circle" class="w-6 h-6 u-text-gray-900 cursor-pointer" />
|
||||
</UTooltip>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Notifications:
|
||||
</div>
|
||||
<UButton icon="i-heroicons-bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Copy text to clipboard:
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<UInput v-model="textToCopy" name="textToCopy" />
|
||||
<UButton icon="i-heroicons-document-duplicate-solid" variant="primary" label="Copy text" @click="onCopyTextClick" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Context menu:
|
||||
</div>
|
||||
|
||||
<UCard class="relative" body-class="h-64" @click="isContextMenuOpen = false" @contextmenu.prevent="openContextMenu">
|
||||
<UContextMenu v-model="isContextMenuOpen" :virtual-element="virtualElement" width-class="w-48">
|
||||
<div class="p-2">
|
||||
Menu
|
||||
</div>
|
||||
</UContextMenu>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Command palette:
|
||||
</div>
|
||||
|
||||
<UCard body-class="">
|
||||
<UCommandPalette
|
||||
:placeholder="false"
|
||||
:options="{
|
||||
fuseOptions: {
|
||||
includeMatches: true
|
||||
}
|
||||
}"
|
||||
:groups="commandPaletteGroups"
|
||||
command-attribute="name"
|
||||
/>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Card:
|
||||
</div>
|
||||
|
||||
<UCard body-class="flex">
|
||||
<div class="flex-1 px-4 py-5 sm:p-6 space-y-3">
|
||||
<UFormGroup label="Email" name="email" required>
|
||||
<UInput v-model="form.email" type="email" name="email" required icon="i-heroicons-mail" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Description" name="description">
|
||||
<UTextarea v-model="form.description" type="description" name="description" autoresize />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Person" name="person" required>
|
||||
<USelect
|
||||
v-model="form.personId"
|
||||
name="person"
|
||||
:options="people"
|
||||
placeholder="Select a person"
|
||||
text-attribute="name"
|
||||
value-attribute="id"
|
||||
icon="i-heroicons-user"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="People" name="people" required>
|
||||
<USelectCustom v-model="form.person" name="people" :options="people" text-attribute="name" searchable />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Toggle" name="toggle">
|
||||
<UToggle v-model="form.toggle" name="toggle" icon-off="i-heroicons-x-mark-20-solid" icon-on="i-heroicons-check-20-solid" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Notifications" label-class="text-base font-medium u-text-gray-900" description="How do you prefer to receive notifications?">
|
||||
<div class="space-y-4 mt-3">
|
||||
<URadio v-model="form.notification" value="email" label="Email" help="Email" />
|
||||
<URadio v-model="form.notification" value="phone" label="Phone (SMS)" help="Phone (SMS)" />
|
||||
<URadio v-model="form.notification" value="push" label="Push notification" help="Push notification" />
|
||||
</div>
|
||||
</UFormGroup>
|
||||
|
||||
<UCard body-class="px-4 py-5 space-y-5">
|
||||
<UCheckbox v-model="form.notifications" name="comments" value="comments" label="Comments" help="Get notified when someones posts a comment on a posting." />
|
||||
<UCheckbox v-model="form.notifications" name="candidates" value="candidates" label="Candidates" help="Get notified when a candidate applies for a job." />
|
||||
<UCheckbox v-model="form.notifications" name="offers" value="offers" label="Offers" help="Get notified when a candidate accepts or rejects an offer." />
|
||||
</UCard>
|
||||
|
||||
<div>
|
||||
<UCheckbox v-model="form.terms" label="I agree to the terms and conditions" name="terms" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<UButton type="submit" label="Submit" class="ml-auto" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/3 px-4 py-5 sm:p-6 border-l u-border-gray-200 u-bg-gray-50">
|
||||
<pre class="whitespace-pre-wrap break-all">
|
||||
{{ form }}
|
||||
</pre>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const isModalOpen = ref(false)
|
||||
const textToCopy = ref('Copied text')
|
||||
|
||||
const people = ref([
|
||||
{ id: 1, name: 'Durward Reynolds', disabled: false },
|
||||
{ id: 2, name: 'Kenton Towne', disabled: false },
|
||||
{ id: 3, name: 'Therese Wunsch', disabled: false },
|
||||
{ id: 4, name: 'Benedict Kessler', disabled: true },
|
||||
{ id: 5, name: 'Katelyn Rohan', disabled: false }
|
||||
])
|
||||
|
||||
const form = reactive({
|
||||
email: '',
|
||||
description: '',
|
||||
toggle: false,
|
||||
notification: 'email',
|
||||
notifications: [],
|
||||
terms: false,
|
||||
personId: null,
|
||||
person: ref(people.value[0]),
|
||||
persons: ref([people.value[0]])
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const clipboard = useCopyToClipboard()
|
||||
|
||||
const x = ref(0)
|
||||
const y = ref(0)
|
||||
const isContextMenuOpen = ref(false)
|
||||
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
|
||||
|
||||
const commandPaletteGroups = computed(() => ([{
|
||||
key: 'people',
|
||||
commands: people.value
|
||||
}, {
|
||||
key: 'search',
|
||||
label: q => q && `Search results for "${q}"...`,
|
||||
search: async (q) => {
|
||||
if (!q) { return [] }
|
||||
return await $fetch(`https://jsonplaceholder.typicode.com/users?q=${q}`)
|
||||
}
|
||||
}]))
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', ({ clientX, clientY }) => {
|
||||
x.value = clientX
|
||||
y.value = clientY
|
||||
})
|
||||
})
|
||||
|
||||
function openContextMenu () {
|
||||
const top = unref(y)
|
||||
const left = unref(x)
|
||||
|
||||
virtualElement.value.getBoundingClientRect = () => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
top,
|
||||
left
|
||||
})
|
||||
isContextMenuOpen.value = true
|
||||
}
|
||||
|
||||
function toggleModalIsOpen () {
|
||||
isModalOpen.value = !isModalOpen.value
|
||||
}
|
||||
|
||||
function onClick () {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('click')
|
||||
}
|
||||
|
||||
function onSubmit () {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('submit')
|
||||
}
|
||||
|
||||
const dropdownItems = [
|
||||
[{
|
||||
label: 'Edit',
|
||||
icon: 'i-heroicons-pencil-square-20-solid',
|
||||
click: (e) => {
|
||||
e.preventDefault()
|
||||
onClick()
|
||||
}
|
||||
}, {
|
||||
label: 'Duplicate',
|
||||
icon: 'i-heroicons-document-duplicate-20-solid'
|
||||
}],
|
||||
[{
|
||||
label: 'Archive',
|
||||
icon: 'i-heroicons-archive-box-20-solid'
|
||||
}, {
|
||||
label: 'Move',
|
||||
icon: 'i-heroicons-arrow-right-circle-20-solid',
|
||||
to: 'https://www.google.fr',
|
||||
target: '_blank'
|
||||
}],
|
||||
[{
|
||||
label: 'Delete',
|
||||
icon: 'i-heroicons-trash-20-solid'
|
||||
}]
|
||||
]
|
||||
|
||||
const customDropdownItems = [
|
||||
[{
|
||||
label: 'benjamincanac',
|
||||
avatar: { src: 'https://picsum.photos/200/300' },
|
||||
href: 'https://google.fr',
|
||||
target: '_blank'
|
||||
}],
|
||||
[{
|
||||
label: 'About',
|
||||
icon: 'i-heroicons-plus-20-solid',
|
||||
to: '/about'
|
||||
}]
|
||||
]
|
||||
|
||||
const onNotificationClick = () => {
|
||||
toast.error({ title: 'Error', description: 'This is an error message' })
|
||||
}
|
||||
|
||||
const onCopyTextClick = () => {
|
||||
clipboard.copy(textToCopy.value, { title: 'Text copied successfully!' })
|
||||
}
|
||||
</script>
|
||||
@@ -1,117 +1,7 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="pb-10 border-b u-border-gray-200 mb-10">
|
||||
<div>
|
||||
<h1 class="inline-block text-3xl font-extrabold u-text-gray-900 tracking-tight">
|
||||
Documentation
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p class="mt-1 text-lg u-text-gray-500">
|
||||
Components library as a Nuxt3 module using <a href="https://tailwindcss.com" target="_blank" class="underline">TailwindCSS</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="font-bold text-2xl u-text-gray-900">
|
||||
Installation
|
||||
</h2>
|
||||
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm" v-html="code1" />
|
||||
</pre>
|
||||
|
||||
<p>Then, register the module in your `nuxt.config.js`:</p>
|
||||
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm" v-html="code2" />
|
||||
</pre>
|
||||
|
||||
<p>If you want latest updates, please use `@nuxthq/ui-edge` in your `package.json`:</p>
|
||||
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm" v-html="code3" />
|
||||
</pre>
|
||||
|
||||
<h2 class="font-bold text-2xl u-text-gray-900">
|
||||
Options
|
||||
</h2>
|
||||
|
||||
<p>- `preset`</p>
|
||||
|
||||
<p>Choose preset. Defaults to `tailwindui`. An object can also be used to override some parts of the default preset.</p>
|
||||
|
||||
<p>- `prefix`</p>
|
||||
|
||||
<p>Define the prefix of the imported components. Defaults to `u`.</p>
|
||||
|
||||
<p class="font-medium">
|
||||
Example:
|
||||
</p>
|
||||
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm" v-html="code4" />
|
||||
</pre>
|
||||
|
||||
<p>- `colors.primary`</p>
|
||||
|
||||
<p>Define the primary variant. Defaults to `indigo`. You can specify your own object of colors like here:</p>
|
||||
|
||||
<p class="font-medium">
|
||||
Example:
|
||||
</p>
|
||||
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm" v-html="code5" />
|
||||
</pre>
|
||||
|
||||
<p>- `colors.gray`</p>
|
||||
|
||||
<p>Define the gray variant. Defaults to `zinc`. You can like the `primary` color specify your own object. https://tailwindcss.com/docs/customizing-colors#default-color-palette</p>
|
||||
|
||||
<p>- `tailwindcss.theme`. Defaults to `{}`.</p>
|
||||
|
||||
<p>Define TailwindCSS theme: https://tailwindcss.com/docs/theme.</p>
|
||||
</div>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const code1 = `
|
||||
yarn add --dev @nuxthq/ui`
|
||||
|
||||
const code2 = `
|
||||
import { defineNuxtConfig } from 'nuxt'
|
||||
|
||||
defineNuxtConfig({
|
||||
buildModules: [
|
||||
'@nuxthq/ui'
|
||||
]
|
||||
})`
|
||||
|
||||
const code3 = `
|
||||
{
|
||||
"devDependencies": {
|
||||
"@nuxthq/ui": "npm:@nuxthq/ui-edge@latest"
|
||||
}
|
||||
}`
|
||||
|
||||
const code4 = `
|
||||
import { defineNuxtConfig } from 'nuxt'
|
||||
|
||||
defineNuxtConfig({
|
||||
ui: {
|
||||
prefix: 'tw'
|
||||
}
|
||||
})`
|
||||
|
||||
const code5 = `
|
||||
import { defineNuxtConfig } from 'nuxt'
|
||||
|
||||
defineNuxtConfig({
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'blue'
|
||||
}
|
||||
}
|
||||
})`
|
||||
<script setup lang="ts">
|
||||
await navigateTo('/getting-started/installation')
|
||||
</script>
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="pb-10 border-b u-border-gray-200 mb-10">
|
||||
<div>
|
||||
<h1 class="inline-block text-3xl font-extrabold u-text-gray-900 tracking-tight">
|
||||
Migration
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p class="mt-1 text-lg u-text-gray-500">
|
||||
Check out the components that have been migrated to Vue3 coming from `@nuxthq/volta-ui`.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UCard body-class>
|
||||
<table class="min-w-full divide-y u-divide-gray-200">
|
||||
<thead class="u-bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
|
||||
Component
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
|
||||
Nuxt3 ready
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
|
||||
Composition API
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
|
||||
Preset system
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
|
||||
Typescript
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(component, index) of components" :key="index" :class="index % 2 === 0 ? 'u-bg-white' : 'u-bg-gray-50'">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium u-text-gray-900">
|
||||
<NuxtLink :to="component.to" class="hover:underline">
|
||||
{{ component.label }}
|
||||
</NuxtLink>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm u-text-gray-500">
|
||||
<span v-if="component.nuxt3 || component.capi">✅</span>
|
||||
<span v-else>❌</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm u-text-gray-500">
|
||||
<span v-if="component.capi">✅</span>
|
||||
<span v-else>❌</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm u-text-gray-500">
|
||||
<span v-if="component.preset">✅</span>
|
||||
<span v-else>❌</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm u-text-gray-500">
|
||||
<span v-if="component.typescript">✅</span>
|
||||
<span v-else>❌</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const components = [
|
||||
{
|
||||
label: 'Avatar',
|
||||
to: '/components/Avatar',
|
||||
nuxt3: true,
|
||||
preset: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'AvatarGroup',
|
||||
to: '/components/AvatarGroup',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Badge',
|
||||
to: '/components/Badge',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Button',
|
||||
to: '/components/Button',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Dropdown',
|
||||
to: '/components/Dropdown',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Link',
|
||||
to: '/components/Link',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Toggle',
|
||||
to: '/components/Toggle',
|
||||
nuxt3: true,
|
||||
preset: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Alert',
|
||||
to: '/components/Alert',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'AlertDialog',
|
||||
to: '/components/AlertDialog',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Input',
|
||||
to: '/components/Input',
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'FormGroup',
|
||||
to: '/components/FormGroup',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Checkbox',
|
||||
to: '/components/Checkbox',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Radio',
|
||||
to: '/components/Radio',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Select',
|
||||
to: '/components/Select',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'SelectCustom',
|
||||
to: '/components/SelectCustom',
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Textarea',
|
||||
to: '/components/Textarea',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Card',
|
||||
to: '/components/Card',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Container',
|
||||
to: '/components/Container',
|
||||
nuxt3: true,
|
||||
preset: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'CommandPalette',
|
||||
to: '/components/CommandPalette',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: false,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Tabs',
|
||||
to: '/components/Tabs',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Pills',
|
||||
to: '/components/Pills',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'VerticalNavigation',
|
||||
to: '/components/VerticalNavigation',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Modal',
|
||||
to: '/components/Modal',
|
||||
nuxt3: true,
|
||||
preset: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Notification',
|
||||
to: '/components/Notification',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Notifications',
|
||||
to: '/components/Notifications',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Popover',
|
||||
to: '/components/Popover',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Slideover',
|
||||
to: '/components/Slideover',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Tooltip',
|
||||
to: '/components/Tooltip',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
}
|
||||
]
|
||||
</script>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
3
docs/public/icon-dark.svg
Normal file
3
docs/public/icon-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
docs/public/icon-light.svg
Normal file
3
docs/public/icon-light.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="#0C0C0D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
187
docs/tailwind.config.ts
Normal file
187
docs/tailwind.config.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
|
||||
export default <Partial<Config>> {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
'docs/content/**/*.md'
|
||||
],
|
||||
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-words'
|
||||
},
|
||||
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))'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
docs/tsconfig.json
Normal file
3
docs/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
38
package.json
38
package.json
@@ -14,11 +14,14 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18 <19"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "nuxt-module-build",
|
||||
"prepack": "yarn build",
|
||||
"dev": "nuxi dev docs",
|
||||
"build:docs": "nuxi generate docs",
|
||||
"build:docs": "nuxi build docs",
|
||||
"lint": "eslint --ext .ts,.js,.vue .",
|
||||
"typecheck": "nuxi typecheck",
|
||||
"prepare": "nuxi prepare docs",
|
||||
@@ -28,31 +31,36 @@
|
||||
"@egoist/tailwindcss-icons": "^1.0.7",
|
||||
"@headlessui/vue": "1.7.10",
|
||||
"@iconify-json/heroicons": "^1.1.10",
|
||||
"@nuxt/kit": "^3.3.3",
|
||||
"@nuxt/kit": "^3.4.3",
|
||||
"@nuxtjs/color-mode": "^3.2.0",
|
||||
"@nuxtjs/tailwindcss": "^6.6.5",
|
||||
"@nuxtjs/tailwindcss": "^6.6.7",
|
||||
"@popperjs/core": "^2.11.7",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/forms": "^0.0.0-insiders.615a228",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"@vueuse/integrations": "^9.13.0",
|
||||
"@vueuse/math": "^9.13.0",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/integrations": "^10.1.2",
|
||||
"@vueuse/math": "^10.1.2",
|
||||
"defu": "^6.1.2",
|
||||
"fuse.js": "^6.6.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"tailwindcss": "^3.3.1"
|
||||
"tailwindcss": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/mdi": "^1.1.50",
|
||||
"@nuxt/module-builder": "^0.2.1",
|
||||
"@iconify-json/simple-icons": "^1.1.51",
|
||||
"@nuxt/content": "^2.6.0",
|
||||
"@nuxt/module-builder": "^0.3.1",
|
||||
"@nuxtjs/eslint-config-typescript": "^12.0.0",
|
||||
"@nuxtjs/plausible": "^0.2.0",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/node": "^18.15.11",
|
||||
"eslint": "^8.37.0",
|
||||
"nuxt": "^3.3.3",
|
||||
"@types/node": "^18.16.3",
|
||||
"@vueuse/nuxt": "^10.1.2",
|
||||
"eslint": "^8.39.0",
|
||||
"nuxt": "^3.4.3",
|
||||
"nuxt-component-meta": "^0.5.1",
|
||||
"nuxt-lodash": "^2.4.1",
|
||||
"standard-version": "^9.5.0",
|
||||
"unbuild": "^1.2.0",
|
||||
"vue-tsc": "^1.2.0"
|
||||
"unbuild": "^1.2.1",
|
||||
"vue-tsc": "^1.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
6
src/env.d.ts
vendored
6
src/env.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
import type { DefaultPreset } from './runtime/presets/default'
|
||||
|
||||
declare module '#build/ui' {
|
||||
declare const preset: DefaultPreset
|
||||
export default preset
|
||||
}
|
||||
197
src/module.ts
197
src/module.ts
@@ -1,11 +1,10 @@
|
||||
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, addTemplate, createResolver } from '@nuxt/kit'
|
||||
import { defu } from 'defu'
|
||||
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, createResolver, addPlugin } from '@nuxt/kit'
|
||||
import colors from 'tailwindcss/colors.js'
|
||||
import type { Config } from 'tailwindcss'
|
||||
import { iconsPlugin, getIconCollections } from '@egoist/tailwindcss-icons'
|
||||
import { name, version } from '../package.json'
|
||||
import { colorsAsRegex, excludeColors } from './runtime/utils/colors'
|
||||
import defaultPreset from './runtime/presets/default'
|
||||
import preset from './runtime/app.config'
|
||||
import type { DeepPartial } from './runtime/types'
|
||||
|
||||
// @ts-ignore
|
||||
delete colors.lightBlue
|
||||
@@ -18,44 +17,28 @@ delete colors.coolGray
|
||||
// @ts-ignore
|
||||
delete colors.blueGray
|
||||
|
||||
interface ColorsOptions {
|
||||
/**
|
||||
* @default 'indigo'
|
||||
*/
|
||||
declare module 'nuxt/schema' {
|
||||
interface AppConfigInput {
|
||||
ui?: {
|
||||
primary?: string
|
||||
|
||||
/**
|
||||
* @default 'gray'
|
||||
*/
|
||||
gray?: string
|
||||
colors?: string[]
|
||||
} & DeepPartial<typeof preset.ui>
|
||||
}
|
||||
}
|
||||
|
||||
export interface ModuleOptions {
|
||||
preset?: object
|
||||
|
||||
/**
|
||||
* @default 'u'
|
||||
*/
|
||||
prefix?: string
|
||||
|
||||
colors?: ColorsOptions
|
||||
/**
|
||||
* @default false
|
||||
*/
|
||||
global?: boolean
|
||||
|
||||
icons: string[]
|
||||
|
||||
tailwindcss?: Partial<Config>
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
preset: {},
|
||||
prefix: 'u',
|
||||
colors: {
|
||||
primary: 'indigo',
|
||||
gray: 'gray'
|
||||
},
|
||||
icons: ['heroicons'],
|
||||
tailwindcss: {
|
||||
theme: {}
|
||||
}
|
||||
}
|
||||
|
||||
export default defineNuxtModule<ModuleOptions>({
|
||||
@@ -67,10 +50,11 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
nuxt: '^3.0.0-rc.8'
|
||||
}
|
||||
},
|
||||
defaults,
|
||||
defaults: {
|
||||
prefix: 'u',
|
||||
icons: ['heroicons']
|
||||
},
|
||||
async setup (options, nuxt) {
|
||||
const { preset = {}, prefix, colors: { primary = 'indigo', gray = 'gray' } = {}, tailwindcss: { theme = {} } = {} } = options
|
||||
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
|
||||
// Transpile runtime
|
||||
@@ -78,6 +62,12 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
nuxt.options.build.transpile.push(runtimeDir)
|
||||
nuxt.options.build.transpile.push('@popperjs/core', '@headlessui/vue')
|
||||
|
||||
nuxt.options.css.push(resolve(runtimeDir, 'ui.css'))
|
||||
|
||||
nuxt.hook('app:resolve', (app) => {
|
||||
app.configs.push(resolve(runtimeDir, 'app.config.ts'))
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
nuxt.hook('tailwindcss:config', function (tailwindConfig: TailwindConfig) {
|
||||
const globalColors = {
|
||||
@@ -85,55 +75,100 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
...tailwindConfig.theme.extend?.colors
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
tailwindConfig.theme.extend.colors = tailwindConfig.theme.extend.colors || {}
|
||||
// @ts-ignore
|
||||
globalColors.primary = tailwindConfig.theme.extend.colors.primary = globalColors[primary] || colors[primary]
|
||||
// @ts-ignore
|
||||
globalColors.gray = tailwindConfig.theme.extend.colors.gray = globalColors[gray] || colors[gray]
|
||||
globalColors.primary = tailwindConfig.theme.extend.colors.primary = {
|
||||
50: 'rgb(var(--color-primary-50) / <alpha-value>)',
|
||||
100: 'rgb(var(--color-primary-100) / <alpha-value>)',
|
||||
200: 'rgb(var(--color-primary-200) / <alpha-value>)',
|
||||
300: 'rgb(var(--color-primary-300) / <alpha-value>)',
|
||||
400: 'rgb(var(--color-primary-400) / <alpha-value>)',
|
||||
500: 'rgb(var(--color-primary-500) / <alpha-value>)',
|
||||
600: 'rgb(var(--color-primary-600) / <alpha-value>)',
|
||||
700: 'rgb(var(--color-primary-700) / <alpha-value>)',
|
||||
800: 'rgb(var(--color-primary-800) / <alpha-value>)',
|
||||
900: 'rgb(var(--color-primary-900) / <alpha-value>)',
|
||||
950: 'rgb(var(--color-primary-950) / <alpha-value>)'
|
||||
}
|
||||
|
||||
if (globalColors.gray) {
|
||||
globalColors.cool = tailwindConfig.theme.extend.colors.cool = colors.gray
|
||||
}
|
||||
|
||||
globalColors.gray = tailwindConfig.theme.extend.colors.gray = {
|
||||
50: 'rgb(var(--color-gray-50) / <alpha-value>)',
|
||||
100: 'rgb(var(--color-gray-100) / <alpha-value>)',
|
||||
200: 'rgb(var(--color-gray-200) / <alpha-value>)',
|
||||
300: 'rgb(var(--color-gray-300) / <alpha-value>)',
|
||||
400: 'rgb(var(--color-gray-400) / <alpha-value>)',
|
||||
500: 'rgb(var(--color-gray-500) / <alpha-value>)',
|
||||
600: 'rgb(var(--color-gray-600) / <alpha-value>)',
|
||||
700: 'rgb(var(--color-gray-700) / <alpha-value>)',
|
||||
800: 'rgb(var(--color-gray-800) / <alpha-value>)',
|
||||
900: 'rgb(var(--color-gray-900) / <alpha-value>)',
|
||||
950: 'rgb(var(--color-gray-950) / <alpha-value>)'
|
||||
}
|
||||
|
||||
const variantColors = excludeColors(globalColors)
|
||||
const safeColorsAsRegex = colorsAsRegex(variantColors)
|
||||
|
||||
nuxt.options.appConfig.ui = {
|
||||
...nuxt.options.appConfig.ui,
|
||||
primary: 'sky',
|
||||
gray: 'cool',
|
||||
colors: variantColors
|
||||
}
|
||||
|
||||
tailwindConfig.safelist = tailwindConfig.safelist || []
|
||||
tailwindConfig.safelist.push(...[{
|
||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`)
|
||||
},
|
||||
{
|
||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(100|600|700)`),
|
||||
variants: ['hover', 'disabled', 'dark']
|
||||
},
|
||||
{
|
||||
pattern: new RegExp(`text-(${safeColorsAsRegex})-(100|800)`),
|
||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|400|500)`)
|
||||
}, {
|
||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-500`),
|
||||
variants: ['disabled']
|
||||
}, {
|
||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(400|950)`),
|
||||
variants: ['dark']
|
||||
},
|
||||
{
|
||||
pattern: new RegExp(`ring-(${safeColorsAsRegex})-(500)`),
|
||||
variants: ['focus']
|
||||
}, {
|
||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(500|900|950)`),
|
||||
variants: ['dark:hover']
|
||||
}, {
|
||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`),
|
||||
variants: ['dark:disabled']
|
||||
}, {
|
||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|100|600)`),
|
||||
variants: ['hover']
|
||||
}, {
|
||||
pattern: new RegExp(`outline-(${safeColorsAsRegex})-500`),
|
||||
variants: ['focus-visible']
|
||||
}, {
|
||||
pattern: new RegExp(`outline-(${safeColorsAsRegex})-400`),
|
||||
variants: ['dark:focus-visible']
|
||||
}, {
|
||||
pattern: new RegExp(`ring-(${safeColorsAsRegex})-500`),
|
||||
variants: ['focus-visible']
|
||||
}, {
|
||||
pattern: new RegExp(`ring-(${safeColorsAsRegex})-400`),
|
||||
variants: ['dark', 'dark:focus-visible']
|
||||
}, {
|
||||
pattern: new RegExp(`text-(${safeColorsAsRegex})-400`),
|
||||
variants: ['dark']
|
||||
}, {
|
||||
pattern: new RegExp(`text-(${safeColorsAsRegex})-600`),
|
||||
variants: ['hover']
|
||||
}, {
|
||||
pattern: new RegExp(`text-(${safeColorsAsRegex})-500`),
|
||||
variants: ['dark:hover']
|
||||
}])
|
||||
|
||||
tailwindConfig.plugins = tailwindConfig.plugins || []
|
||||
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
|
||||
|
||||
const ui: object = defu(preset, defaultPreset(variantColors))
|
||||
|
||||
addTemplate({
|
||||
filename: 'ui.mjs',
|
||||
getContents: () => `export default ${JSON.stringify(ui)}`
|
||||
})
|
||||
addTemplate({
|
||||
filename: 'ui.d.ts',
|
||||
write: true,
|
||||
getContents: () => 'declare const d: any; export default d;'
|
||||
})
|
||||
})
|
||||
|
||||
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
|
||||
await installModule('@nuxtjs/tailwindcss', {
|
||||
viewer: false,
|
||||
exposeConfig: true,
|
||||
config: {
|
||||
darkMode: 'class',
|
||||
theme,
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
@@ -141,47 +176,43 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
],
|
||||
content: [
|
||||
resolve(runtimeDir, 'components/**/*.{vue,js,ts}'),
|
||||
resolve(runtimeDir, 'presets/*.{mjs,js,ts}')
|
||||
],
|
||||
safelist: [
|
||||
'dark',
|
||||
{
|
||||
pattern: /rounded-(sm|md|lg|xl|2xl|3xl)/,
|
||||
variants: ['sm']
|
||||
}
|
||||
resolve(runtimeDir, '*.{mjs,js,ts}')
|
||||
]
|
||||
},
|
||||
cssPath: resolve(runtimeDir, 'tailwind.css')
|
||||
}
|
||||
})
|
||||
|
||||
addPlugin({
|
||||
src: resolve(runtimeDir, 'plugins', 'colors')
|
||||
})
|
||||
|
||||
addComponentsDir({
|
||||
path: resolve(runtimeDir, 'components', 'elements'),
|
||||
prefix,
|
||||
watch: false
|
||||
})
|
||||
addComponentsDir({
|
||||
path: resolve(runtimeDir, 'components', 'feedback'),
|
||||
prefix,
|
||||
prefix: options.prefix,
|
||||
global: options.global,
|
||||
watch: false
|
||||
})
|
||||
addComponentsDir({
|
||||
path: resolve(runtimeDir, 'components', 'forms'),
|
||||
prefix,
|
||||
prefix: options.prefix,
|
||||
global: options.global,
|
||||
watch: false
|
||||
})
|
||||
addComponentsDir({
|
||||
path: resolve(runtimeDir, 'components', 'layout'),
|
||||
prefix,
|
||||
prefix: options.prefix,
|
||||
global: options.global,
|
||||
watch: false
|
||||
})
|
||||
addComponentsDir({
|
||||
path: resolve(runtimeDir, 'components', 'navigation'),
|
||||
prefix,
|
||||
prefix: options.prefix,
|
||||
global: options.global,
|
||||
watch: false
|
||||
})
|
||||
addComponentsDir({
|
||||
path: resolve(runtimeDir, 'components', 'overlays'),
|
||||
prefix,
|
||||
prefix: options.prefix,
|
||||
global: options.global,
|
||||
watch: false
|
||||
})
|
||||
|
||||
|
||||
708
src/runtime/app.config.ts
Normal file
708
src/runtime/app.config.ts
Normal file
@@ -0,0 +1,708 @@
|
||||
// Elements
|
||||
|
||||
const avatar = {
|
||||
wrapper: 'relative inline-flex items-center justify-center',
|
||||
background: 'bg-gray-100 dark:bg-gray-800',
|
||||
rounded: 'rounded-full',
|
||||
placeholder: 'text-xs font-medium leading-none text-gray-900 dark:text-white truncate',
|
||||
size: {
|
||||
'3xs': 'h-4 w-4 text-xs',
|
||||
'2xs': 'h-5 w-5 text-xs',
|
||||
xs: 'h-6 w-6 text-xs',
|
||||
sm: 'h-8 w-8 text-sm',
|
||||
md: 'h-10 w-10 text-md',
|
||||
lg: 'h-12 w-12 text-lg',
|
||||
xl: 'h-14 w-14 text-xl',
|
||||
'2xl': 'h-16 w-16 text-2xl',
|
||||
'3xl': 'h-20 w-20 text-3xl'
|
||||
},
|
||||
chip: {
|
||||
base: 'absolute block rounded-full ring-2 ring-white dark:ring-gray-900',
|
||||
position: {
|
||||
'top-right': 'top-0 right-0',
|
||||
'bottom-right': 'bottom-0 right-0',
|
||||
'top-left': 'top-0 left-0',
|
||||
'bottom-left': 'bottom-0 left-0'
|
||||
},
|
||||
variant: {
|
||||
solid: 'bg-{color}-400'
|
||||
},
|
||||
size: {
|
||||
'3xs': 'h-1 w-1',
|
||||
'2xs': 'h-1 w-1',
|
||||
xs: 'h-1.5 w-1.5',
|
||||
sm: 'h-2 w-2',
|
||||
md: 'h-2.5 w-2.5',
|
||||
lg: 'h-3 w-3',
|
||||
xl: 'h-3.5 w-3.5',
|
||||
'2xl': 'h-3.5 w-3.5',
|
||||
'3xl': 'h-4 w-4'
|
||||
}
|
||||
},
|
||||
default: {
|
||||
size: 'md',
|
||||
chipVariant: 'solid',
|
||||
chipPosition: 'top-right'
|
||||
}
|
||||
}
|
||||
|
||||
const avatarGroup = {
|
||||
wrapper: 'flex flex-row-reverse',
|
||||
ring: 'ring-2 ring-white dark:ring-gray-900',
|
||||
margin: '-mr-1.5 first:mr-0'
|
||||
}
|
||||
|
||||
const badge = {
|
||||
base: 'inline-flex items-center',
|
||||
rounded: 'rounded-md',
|
||||
font: 'font-medium',
|
||||
size: {
|
||||
sm: 'text-xs px-1.5 py-0.5',
|
||||
md: 'text-xs px-2 py-1',
|
||||
lg: 'text-xs px-2.5 py-1.5'
|
||||
},
|
||||
variant: {
|
||||
solid: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-10 dark:ring-opacity-20'
|
||||
},
|
||||
default: {
|
||||
size: 'md',
|
||||
variant: 'solid',
|
||||
color: 'primary'
|
||||
}
|
||||
}
|
||||
|
||||
const button = {
|
||||
base: 'focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75',
|
||||
font: 'font-medium',
|
||||
rounded: 'rounded-md',
|
||||
size: {
|
||||
'2xs': 'text-xs',
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
xl: 'text-base'
|
||||
},
|
||||
gap: {
|
||||
'2xs': 'gap-x-1',
|
||||
xs: 'gap-x-1.5',
|
||||
sm: 'gap-x-2',
|
||||
md: 'gap-x-2',
|
||||
lg: 'gap-x-2',
|
||||
xl: 'gap-x-2'
|
||||
},
|
||||
spacing: {
|
||||
'2xs': 'px-2 py-1',
|
||||
xs: 'px-2.5 py-1.5',
|
||||
sm: 'px-3 py-1.5',
|
||||
md: 'px-3 py-2',
|
||||
lg: 'px-4 py-2',
|
||||
xl: 'px-4 py-3'
|
||||
},
|
||||
square: {
|
||||
'2xs': 'p-[5px]',
|
||||
xs: 'p-1.5',
|
||||
sm: 'p-2',
|
||||
md: 'p-2',
|
||||
lg: 'p-2.5',
|
||||
xl: 'p-3'
|
||||
},
|
||||
color: {
|
||||
white: {
|
||||
solid: 'shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-900 dark:text-white bg-white hover:bg-gray-50 disabled:bg-white dark:bg-gray-900 dark:hover:bg-gray-800/50 dark:disabled:bg-gray-900 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
|
||||
ghost: 'text-gray-900 dark:text-white hover:bg-white dark:hover:bg-gray-900 focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
|
||||
},
|
||||
gray: {
|
||||
solid: 'shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-700 dark:text-gray-200 bg-gray-50 hover:bg-gray-100 disabled:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700/50 dark:disabled:bg-gray-800 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
|
||||
// TODO: For Volta
|
||||
// 'outline-ghost': 'text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:ring-1 ring-inset ring-gray-300 dark:ring-gray-700',
|
||||
ghost: 'text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
|
||||
link: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 underline-offset-4 hover:underline focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
|
||||
},
|
||||
black: {
|
||||
solid: 'shadow-sm text-white dark:text-gray-900 bg-gray-900 hover:bg-gray-800 disabled:bg-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:disabled:bg-white focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
|
||||
link: 'text-gray-900 dark:text-white underline-offset-4 hover:underline focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
|
||||
}
|
||||
},
|
||||
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'
|
||||
},
|
||||
icon: {
|
||||
base: 'flex-shrink-0',
|
||||
size: {
|
||||
'2xs': 'h-3.5 w-3.5',
|
||||
xs: 'h-4 w-4',
|
||||
sm: 'h-4 w-4',
|
||||
md: 'h-5 w-5',
|
||||
lg: 'h-5 w-5',
|
||||
xl: 'h-6 w-6'
|
||||
}
|
||||
},
|
||||
default: {
|
||||
size: 'sm',
|
||||
variant: 'solid',
|
||||
color: 'primary',
|
||||
loadingIcon: 'i-heroicons-arrow-path-20-solid'
|
||||
}
|
||||
}
|
||||
|
||||
const buttonGroup = {
|
||||
wrapper: 'inline-flex',
|
||||
rounded: 'rounded-md',
|
||||
shadow: 'shadow-sm'
|
||||
}
|
||||
|
||||
const dropdown = {
|
||||
wrapper: 'relative inline-flex text-left',
|
||||
container: 'z-20',
|
||||
width: 'w-48',
|
||||
background: 'bg-white dark:bg-gray-800',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||
base: 'focus:outline-none',
|
||||
divide: 'divide-y divide-gray-200 dark:divide-gray-700',
|
||||
spacing: 'p-1',
|
||||
item: {
|
||||
base: 'group flex items-center gap-2 px-2 py-1.5 text-sm w-full rounded-md',
|
||||
active: 'bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-white',
|
||||
inactive: 'text-gray-700 dark:text-gray-200',
|
||||
disabled: 'cursor-not-allowed opacity-50',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 h-4 w-4',
|
||||
active: 'text-gray-500 dark:text-gray-400',
|
||||
inactive: 'text-gray-400 dark:text-gray-500'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
size: '3xs'
|
||||
},
|
||||
shortcuts: 'hidden md:inline-flex flex-shrink-0 text-xs font-semibold text-gray-500 dark:text-gray-400 ml-auto'
|
||||
},
|
||||
transition: {
|
||||
enterActiveClass: 'transition duration-100 ease-out',
|
||||
enterFromClass: 'transform scale-95 opacity-0',
|
||||
enterToClass: 'transform scale-100 opacity-100',
|
||||
leaveActiveClass: 'transition duration-75 ease-out',
|
||||
leaveFromClass: 'transform scale-100 opacity-100',
|
||||
leaveToClass: 'transform scale-95 opacity-0'
|
||||
},
|
||||
popper: {
|
||||
placement: 'bottom-end',
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
// Forms
|
||||
|
||||
const input = {
|
||||
wrapper: 'relative',
|
||||
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none',
|
||||
custom: '',
|
||||
size: {
|
||||
'2xs': 'text-xs',
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
xl: 'text-base'
|
||||
},
|
||||
gap: {
|
||||
'2xs': 'gap-x-1',
|
||||
xs: 'gap-x-1.5',
|
||||
sm: 'gap-x-2',
|
||||
md: 'gap-x-2',
|
||||
lg: 'gap-x-2',
|
||||
xl: 'gap-x-2'
|
||||
},
|
||||
spacing: {
|
||||
'2xs': 'px-2 py-1',
|
||||
xs: 'px-2.5 py-1.5',
|
||||
sm: 'px-3 py-1.5',
|
||||
md: 'px-3 py-2',
|
||||
lg: 'px-4 py-2',
|
||||
xl: 'px-4 py-3'
|
||||
},
|
||||
leading: {
|
||||
spacing: {
|
||||
'2xs': 'pl-[26px]',
|
||||
xs: 'pl-8',
|
||||
sm: 'pl-9',
|
||||
md: 'pl-10',
|
||||
lg: 'pl-11',
|
||||
xl: 'pl-12'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
spacing: {
|
||||
'2xs': 'pr-[26px]',
|
||||
xs: 'pr-8',
|
||||
sm: 'pr-9',
|
||||
md: 'pr-10',
|
||||
lg: 'pr-11',
|
||||
xl: 'pr-12'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
white: 'border-0 bg-white dark:bg-gray-900 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500',
|
||||
gray: 'border-0 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500',
|
||||
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none'
|
||||
},
|
||||
icon: {
|
||||
base: 'text-gray-400 dark:text-gray-500',
|
||||
size: {
|
||||
'2xs': 'h-3.5 w-3.5',
|
||||
xs: 'h-4 w-4',
|
||||
sm: 'h-4 w-4',
|
||||
md: 'h-5 w-5',
|
||||
lg: 'h-5 w-5',
|
||||
xl: 'h-6 w-6'
|
||||
},
|
||||
leading: {
|
||||
wrapper: 'absolute inset-y-0 left-0 flex items-center pointer-events-none',
|
||||
spacing: {
|
||||
'2xs': 'pl-2',
|
||||
xs: 'pl-2.5',
|
||||
sm: 'pl-3',
|
||||
md: 'pl-3',
|
||||
lg: 'pl-4',
|
||||
xl: 'pl-4'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
wrapper: 'absolute inset-y-0 right-0 flex items-center pointer-events-none',
|
||||
spacing: {
|
||||
'2xs': 'pr-2',
|
||||
xs: 'pr-2.5',
|
||||
sm: 'pr-3',
|
||||
md: 'pr-3',
|
||||
lg: 'pr-4',
|
||||
xl: 'pr-4'
|
||||
}
|
||||
}
|
||||
},
|
||||
default: {
|
||||
size: 'sm',
|
||||
appearance: 'white',
|
||||
loadingIcon: 'i-heroicons-arrow-path-20-solid'
|
||||
}
|
||||
}
|
||||
|
||||
const inputGroup = {
|
||||
wrapper: '',
|
||||
label: 'block text-sm font-medium text-gray-700 dark:text-gray-200',
|
||||
labelWrapper: 'flex content-center justify-between',
|
||||
container: 'mt-1 relative',
|
||||
required: 'text-red-400',
|
||||
description: 'text-sm leading-5 text-gray-500 dark:text-gray-400',
|
||||
hint: 'text-sm leading-5 text-gray-500 dark:text-gray-400',
|
||||
help: 'mt-2 text-sm text-gray-500 dark:text-gray-400'
|
||||
}
|
||||
|
||||
const textarea = {
|
||||
...input
|
||||
}
|
||||
|
||||
const select = {
|
||||
...input
|
||||
}
|
||||
|
||||
const selectMenu = {
|
||||
wrapper: 'relative',
|
||||
container: 'z-20',
|
||||
width: 'w-full',
|
||||
height: 'max-h-60',
|
||||
base: 'relative focus:outline-none overflow-y-auto scroll-py-1',
|
||||
background: 'bg-white dark:bg-gray-800',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
spacing: 'p-1',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||
input: 'block w-[calc(100%+0.5rem)] focus:ring-transparent text-sm px-3 py-1.5 u-text-gray-700 bg-white dark:bg-gray-800 border-0 border-b border-gray-200 dark:border-gray-700 focus:border-inherit sticky -top-1 -mt-1 mb-1 -mx-1 z-10 placeholder-gray-400 dark:placeholder-gray-500',
|
||||
option: {
|
||||
base: 'cursor-default select-none relative px-2 py-1.5 rounded-md text-sm text-gray-900 dark:text-white flex items-center justify-between gap-1',
|
||||
container: 'flex items-center gap-2',
|
||||
active: 'bg-gray-100 dark:bg-gray-900',
|
||||
inactive: '',
|
||||
disabled: 'cursor-not-allowed opacity-50',
|
||||
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 h-4 w-4',
|
||||
active: 'text-gray-900 dark:text-white',
|
||||
inactive: 'text-gray-400 dark:text-gray-500'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
size: '3xs'
|
||||
},
|
||||
chip: {
|
||||
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
|
||||
},
|
||||
selected: {
|
||||
wrapper: 'absolute inset-y-0 right-0 flex items-center pr-2',
|
||||
icon: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0'
|
||||
}
|
||||
},
|
||||
transition: {
|
||||
leaveActiveClass: 'transition ease-in duration-100',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0'
|
||||
},
|
||||
popper: {
|
||||
placement: 'bottom-end'
|
||||
},
|
||||
default: {
|
||||
selectedIcon: 'i-heroicons-check-20-solid'
|
||||
}
|
||||
}
|
||||
|
||||
const radio = {
|
||||
wrapper: 'relative flex items-start',
|
||||
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus:ring-2 focus:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent focus:ring-primary-500 dark:focus:ring-primary-400 focus:ring-offset-white dark:focus:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
label: 'font-medium text-gray-700 dark:text-gray-200',
|
||||
required: 'text-red-400',
|
||||
help: 'text-gray-500 dark:text-gray-400'
|
||||
}
|
||||
|
||||
const checkbox = {
|
||||
...radio,
|
||||
base: radio.base + ' rounded'
|
||||
}
|
||||
|
||||
const toggle = {
|
||||
base: 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:focus:ring-primary-400 focus:ring-offset-white dark:focus:ring-offset-gray-900',
|
||||
active: 'bg-primary-500 dark:bg-primary-400',
|
||||
inactive: 'bg-gray-200 dark:bg-gray-700',
|
||||
container: {
|
||||
base: 'pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
|
||||
active: 'translate-x-5',
|
||||
inactive: 'translate-x-0'
|
||||
},
|
||||
icon: {
|
||||
base: 'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity',
|
||||
active: 'opacity-100 ease-in duration-200',
|
||||
inactive: 'opacity-0 ease-out duration-100',
|
||||
on: 'h-3 w-3 text-primary-500 dark:text-primary-400',
|
||||
off: 'h-3 w-3 text-gray-400 dark:text-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
// Layout
|
||||
|
||||
const card = {
|
||||
base: 'overflow-hidden',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
divide: 'divide-y divide-gray-200 dark:divide-gray-700',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||
rounded: 'rounded-lg',
|
||||
shadow: 'shadow',
|
||||
body: {
|
||||
base: '',
|
||||
background: '',
|
||||
spacing: 'px-4 py-5 sm:p-6'
|
||||
},
|
||||
header: {
|
||||
base: '',
|
||||
background: '',
|
||||
spacing: 'px-4 py-5 sm:px-6'
|
||||
},
|
||||
footer: {
|
||||
base: '',
|
||||
background: '',
|
||||
spacing: 'px-4 py-4 sm:px-6'
|
||||
}
|
||||
}
|
||||
|
||||
const container = {
|
||||
base: 'mx-auto',
|
||||
spacing: 'px-4 sm:px-6 lg:px-8',
|
||||
constrained: 'max-w-7xl'
|
||||
}
|
||||
|
||||
// Navigation
|
||||
|
||||
const verticalNavigation = {
|
||||
wrapper: 'relative z-0',
|
||||
base: 'group flex items-center gap-2 text-sm font-medium rounded-md w-full relative focus:outline-none after:absolute after:inset-px after:z-[-1] after:rounded-md disabled:cursor-not-allowed disabled:opacity-75',
|
||||
spacing: 'px-3 py-1.5',
|
||||
active: 'u-text-gray-900 after:bg-gray-100 dark:after:bg-gray-800',
|
||||
inactive: 'u-text-gray-500 hover:u-text-gray-900 hover:after:bg-gray-50 dark:hover:after:bg-gray-800/50 focus-visible:after:bg-gray-50 dark:focus-visible:after:bg-gray-800/50',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 w-4 h-4',
|
||||
active: 'u-text-gray-700',
|
||||
inactive: 'u-text-gray-400 group-hover:u-text-gray-700'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
size: '3xs'
|
||||
},
|
||||
badge: {
|
||||
base: 'ml-auto inline-block py-0.5 px-2 text-xs rounded-md -mr-1 -my-0.5',
|
||||
active: 'bg-white dark:bg-gray-900',
|
||||
inactive: 'u-bg-gray-100 u-text-gray-600 group-hover:bg-white dark:group-hover:bg-gray-900'
|
||||
}
|
||||
}
|
||||
|
||||
const commandPalette = {
|
||||
wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800',
|
||||
container: 'relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2',
|
||||
input: {
|
||||
wrapper: 'relative flex items-center',
|
||||
base: 'w-full h-12 px-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 text-gray-900 dark:text-white focus:ring-0 sm:text-sm',
|
||||
spacing: 'pl-10',
|
||||
icon: 'pointer-events-none absolute left-4 h-4 w-4 text-gray-400 dark:text-gray-500',
|
||||
close: 'absolute right-4'
|
||||
},
|
||||
empty: {
|
||||
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
|
||||
label: 'text-sm text-center text-gray-900 dark:text-white',
|
||||
queryLabel: 'text-sm text-center text-gray-900 dark:text-white',
|
||||
icon: 'w-6 h-6 mx-auto text-gray-400 dark:text-gray-500 mb-4'
|
||||
},
|
||||
group: {
|
||||
wrapper: 'p-2',
|
||||
label: 'px-2 my-2 text-xs font-semibold text-gray-900 dark:text-white',
|
||||
container: 'text-sm text-gray-700 dark:text-gray-200',
|
||||
command: {
|
||||
base: 'flex justify-between select-none items-center rounded-md px-2 py-1.5 gap-2 relative',
|
||||
active: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white',
|
||||
inactive: '',
|
||||
label: 'flex items-center gap-1.5 min-w-0',
|
||||
prefix: 'text-gray-400 dark:text-gray-500',
|
||||
suffix: 'text-gray-400 dark:text-gray-500',
|
||||
container: 'flex items-center gap-2 min-w-0',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 w-4 h-4',
|
||||
active: 'text-gray-900 dark:text-white',
|
||||
inactive: 'text-gray-400 dark:text-gray-500'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
size: '3xs'
|
||||
},
|
||||
chip: {
|
||||
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
|
||||
},
|
||||
disabled: 'opacity-50',
|
||||
selected: {
|
||||
icon: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0'
|
||||
},
|
||||
shortcuts: 'hidden md:inline-flex flex-shrink-0 text-xs font-semibold text-gray-500 dark:text-gray-400'
|
||||
},
|
||||
active: 'flex-shrink-0 text-gray-500 dark:text-gray-400',
|
||||
inactive: 'flex-shrink-0 text-gray-500 dark:text-gray-400'
|
||||
},
|
||||
default: {
|
||||
icon: 'i-heroicons-magnifying-glass-20-solid',
|
||||
empty: {
|
||||
icon: 'i-heroicons-magnifying-glass-20-solid',
|
||||
label: 'We couldn\'t find any items.',
|
||||
queryLabel: 'We couldn\'t find any items with that term. Please try again.'
|
||||
},
|
||||
close: null,
|
||||
selectedIcon: 'i-heroicons-check-20-solid'
|
||||
}
|
||||
}
|
||||
|
||||
// Overlays
|
||||
|
||||
const modal = {
|
||||
wrapper: 'relative z-50',
|
||||
inner: 'fixed inset-0 overflow-y-auto',
|
||||
container: 'flex min-h-full items-end sm:items-center justify-center text-center',
|
||||
spacing: 'p-4 sm:p-0',
|
||||
base: 'relative text-left overflow-hidden sm:my-8 w-full flex flex-col',
|
||||
overlay: {
|
||||
base: 'fixed inset-0 transition-opacity',
|
||||
background: 'bg-gray-500/75 dark:bg-gray-600/75',
|
||||
transition: {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0'
|
||||
}
|
||||
},
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
ring: '',
|
||||
rounded: 'rounded-lg',
|
||||
shadow: 'shadow-xl',
|
||||
width: 'sm:max-w-lg',
|
||||
height: '',
|
||||
transition: {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95',
|
||||
enterTo: 'opacity-100 translate-y-0 sm:scale-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100 translate-y-0 sm:scale-100',
|
||||
leaveTo: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
||||
}
|
||||
}
|
||||
|
||||
const slideover = {
|
||||
wrapper: 'fixed inset-0 flex z-50',
|
||||
overlay: {
|
||||
base: 'fixed inset-0 transition-opacity',
|
||||
background: 'bg-gray-500/75 dark:bg-gray-600/75',
|
||||
transition: {
|
||||
enter: 'ease-in-out duration-500',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in-out duration-500',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0'
|
||||
}
|
||||
},
|
||||
base: 'relative flex-1 flex flex-col w-full focus:outline-none',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
ring: '',
|
||||
rounded: '',
|
||||
shadow: 'shadow-xl',
|
||||
width: 'w-screen max-w-md',
|
||||
transition: {
|
||||
enter: 'transform transition ease-in-out duration-500 sm:duration-700',
|
||||
leave: 'transform transition ease-in-out duration-500 sm:duration-700'
|
||||
}
|
||||
}
|
||||
|
||||
const tooltip = {
|
||||
wrapper: 'relative inline-flex',
|
||||
container: 'z-20',
|
||||
width: 'max-w-xs',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
shadow: 'shadow',
|
||||
rounded: 'rounded',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
|
||||
base: 'invisible lg:visible h-6 px-2 py-1 text-xs font-normal truncate',
|
||||
shortcuts: 'hidden md:inline-flex items-center justify-end flex-shrink-0 gap-0.5 ml-1',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popper: {
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
const popover = {
|
||||
wrapper: 'relative',
|
||||
container: 'z-20',
|
||||
width: '',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
|
||||
base: 'overflow-hidden focus:outline-none',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popper: {
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
const contextMenu = {
|
||||
wrapper: 'relative',
|
||||
container: 'z-20',
|
||||
width: '',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
|
||||
base: 'overflow-hidden focus:outline-none',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popper: {
|
||||
placement: 'bottom-start',
|
||||
scroll: false
|
||||
}
|
||||
}
|
||||
|
||||
const notification = {
|
||||
wrapper: 'w-full pointer-events-auto',
|
||||
container: 'relative overflow-hidden',
|
||||
title: 'text-sm font-medium text-gray-900 dark:text-white',
|
||||
description: 'mt-1 text-sm leading-5 text-gray-500 dark:text-gray-400',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-lg',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
|
||||
icon: 'flex-shrink-0 w-5 h-5 text-gray-900 dark:text-white',
|
||||
avatar: 'flex-shrink-0 pt-0.5',
|
||||
progress: 'absolute bottom-0 left-0 right-0 h-1 bg-primary-500 dark:bg-primary-400',
|
||||
transition: {
|
||||
enterActiveClass: 'transform ease-out duration-300 transition',
|
||||
enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2',
|
||||
enterToClass: 'translate-y-0 opacity-100 sm:translate-x-0',
|
||||
leaveActiveClass: 'transition ease-in duration-100',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0'
|
||||
},
|
||||
default: {
|
||||
close: {
|
||||
icon: 'i-heroicons-x-mark-20-solid',
|
||||
color: 'gray',
|
||||
variant: 'link',
|
||||
padded: false
|
||||
},
|
||||
action: {
|
||||
size: 'xs',
|
||||
color: 'white'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notifications = {
|
||||
wrapper: 'fixed bottom-0 right-0 flex flex-col justify-end w-full z-[55] sm:w-96',
|
||||
container: 'px-4 sm:px-6 py-6 space-y-3 overflow-y-auto'
|
||||
}
|
||||
|
||||
export default {
|
||||
ui: {
|
||||
avatar,
|
||||
avatarGroup,
|
||||
badge,
|
||||
button,
|
||||
buttonGroup,
|
||||
dropdown,
|
||||
input,
|
||||
inputGroup,
|
||||
textarea,
|
||||
select,
|
||||
selectMenu,
|
||||
checkbox,
|
||||
radio,
|
||||
toggle,
|
||||
card,
|
||||
container,
|
||||
verticalNavigation,
|
||||
commandPalette,
|
||||
modal,
|
||||
slideover,
|
||||
popover,
|
||||
tooltip,
|
||||
contextMenu,
|
||||
notification,
|
||||
notifications
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,27 @@
|
||||
<template>
|
||||
<span :class="wrapperClass">
|
||||
<img v-if="url && !error" :class="avatarClass" :src="url" :alt="alt" :onerror="() => onError()">
|
||||
<span v-else-if="text || placeholder" :class="placeholderClass">{{ text || placeholder }}</span>
|
||||
<span v-else-if="text || placeholder" :class="ui.placeholder">{{ text || placeholder }}</span>
|
||||
|
||||
<span v-if="chip" :class="chipClass" />
|
||||
<span v-if="chipColor" :class="chipClass" />
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, watch } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
src: {
|
||||
type: [String, Boolean],
|
||||
default: null
|
||||
@@ -28,69 +36,65 @@ const props = defineProps({
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
default: () => appConfig.ui.avatar.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.size).includes(value)
|
||||
return Object.keys(appConfig.ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
chip: {
|
||||
chipColor: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.chip.variant).includes(value)
|
||||
return appConfig.ui.colors.includes(value)
|
||||
}
|
||||
},
|
||||
chipVariant: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.avatar.default.chipVariant,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.chip.variant).includes(value)
|
||||
}
|
||||
},
|
||||
chipPosition: {
|
||||
type: String,
|
||||
default: 'top-right',
|
||||
default: () => appConfig.ui.avatar.default.chipPosition,
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.chip.position).includes(value)
|
||||
return Object.keys(appConfig.ui.avatar.chip.position).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.wrapper
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.background
|
||||
},
|
||||
placeholderClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.placeholder
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.rounded
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.avatar>>,
|
||||
default: () => appConfig.ui.avatar
|
||||
}
|
||||
})
|
||||
},
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.avatar>>(() => defu({}, props.ui, appConfig.ui.avatar))
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
return classNames(
|
||||
props.wrapperClass,
|
||||
props.backgroundClass,
|
||||
$ui.avatar.size[props.size],
|
||||
props.rounded ? 'rounded-full' : props.roundedClass
|
||||
ui.value.wrapper,
|
||||
ui.value.background,
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const avatarClass = computed(() => {
|
||||
return classNames(
|
||||
$ui.avatar.size[props.size],
|
||||
props.rounded ? 'rounded-full' : props.roundedClass
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const chipClass = computed(() => {
|
||||
return classNames(
|
||||
$ui.avatar.chip.base,
|
||||
$ui.avatar.chip.variant[props.chip],
|
||||
$ui.avatar.chip.position[props.chipPosition],
|
||||
$ui.avatar.chip.size[props.size]
|
||||
ui.value.chip.base,
|
||||
ui.value.chip.size[props.size],
|
||||
ui.value.chip.position[props.chipPosition],
|
||||
ui.value.chip.variant[props.chipVariant]?.replaceAll('{color}', props.chipColor)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -116,8 +120,16 @@ watch(() => props.src, () => {
|
||||
function onError () {
|
||||
error.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UAvatar' }
|
||||
return {
|
||||
wrapperClass,
|
||||
avatarClass,
|
||||
chipClass,
|
||||
url,
|
||||
placeholder,
|
||||
error,
|
||||
onError
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
80
src/runtime/components/elements/AvatarGroup.ts
Normal file
80
src/runtime/components/elements/AvatarGroup.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { h, computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import Avatar from './Avatar.vue'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.avatarGroup>>,
|
||||
default: () => appConfig.ui.avatarGroup
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defu({}, props.ui, appConfig.ui.avatarGroup))
|
||||
|
||||
const children = computed(() => {
|
||||
let children = slots.default?.()
|
||||
// @ts-ignore-next
|
||||
if (children.length && children[0].type.name === 'ContentSlot') {
|
||||
// @ts-ignore-next
|
||||
children = children[0].ctx.slots.default?.()
|
||||
}
|
||||
return children
|
||||
})
|
||||
|
||||
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max)
|
||||
|
||||
const clones = computed(() => children.value.map((node, index) => {
|
||||
if (!props.max || (max.value && index < max.value)) {
|
||||
if (props.size) {
|
||||
node.props.size = props.size
|
||||
}
|
||||
|
||||
node.props.class = node.props.class || ''
|
||||
node.props.class += ` ${classNames(
|
||||
ui.value.ring,
|
||||
ui.value.margin
|
||||
)}`
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
if (max.value !== undefined && index === max.value) {
|
||||
return h(Avatar, {
|
||||
size: props.size,
|
||||
text: `+${children.value.length - max.value}`,
|
||||
class: classNames(
|
||||
ui.value.ring,
|
||||
ui.value.margin
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
}).filter(Boolean).reverse())
|
||||
|
||||
return () => h('div', { class: ui.value.wrapper }, clones.value)
|
||||
}
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-row-reverse">
|
||||
<Avatar
|
||||
v-if="remainingGroupSize > 0"
|
||||
:size="size"
|
||||
:text="`+${remainingGroupSize}`"
|
||||
:class="avatarClass"
|
||||
/>
|
||||
<Avatar
|
||||
v-for="(avatar, index) of displayedGroup"
|
||||
:key="index"
|
||||
v-bind="avatar"
|
||||
:size="size"
|
||||
:class="avatarClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import Avatar from './Avatar.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
group: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatarGroup.ring
|
||||
},
|
||||
marginClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatarGroup.margin
|
||||
}
|
||||
})
|
||||
|
||||
const avatars = computed(() => {
|
||||
return props.group.map((avatar) => {
|
||||
return typeof avatar === 'string' ? { src: avatar } : avatar
|
||||
})
|
||||
})
|
||||
|
||||
const displayedGroup = computed(() => {
|
||||
if (!props.max) { return [...avatars.value].reverse() }
|
||||
|
||||
return avatars.value.slice(0, props.max).reverse()
|
||||
})
|
||||
|
||||
const remainingGroupSize = computed(() => {
|
||||
if (!props.max) { return 0 }
|
||||
|
||||
return avatars.value.length - props.max
|
||||
})
|
||||
|
||||
const avatarClass = computed(() => {
|
||||
return classNames(
|
||||
props.ringClass,
|
||||
props.marginClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UAvatarGroup' }
|
||||
</script>
|
||||
@@ -4,50 +4,69 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
default: () => appConfig.ui.badge.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.badge.size).includes(value)
|
||||
return Object.keys(appConfig.ui.badge.size).includes(value)
|
||||
}
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.badge.default.color,
|
||||
validator (value: string) {
|
||||
return appConfig.ui.colors.includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
default: () => appConfig.ui.badge.default.variant,
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.badge.variant).includes(value)
|
||||
return Object.keys(appConfig.ui.badge.variant).includes(value)
|
||||
}
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.badge.base
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.badge>>,
|
||||
default: () => appConfig.ui.badge
|
||||
}
|
||||
})
|
||||
},
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defu({}, props.ui, appConfig.ui.badge))
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.badge.size[props.size],
|
||||
$ui.badge.variant[props.variant],
|
||||
props.rounded ? 'rounded-full' : 'rounded-md'
|
||||
ui.value.base,
|
||||
ui.value.font,
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size],
|
||||
ui.value.variant[props.variant]?.replaceAll('{color}', props.color)
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UBadge' }
|
||||
return {
|
||||
badgeClass
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -8,25 +8,34 @@
|
||||
>
|
||||
<Icon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
|
||||
<slot>
|
||||
<span :class="[truncate ? 'text-left break-all line-clamp-1' : '', compact ? 'hidden sm:block' : '']">
|
||||
<span :class="[labelCompact && 'hidden sm:block']">{{ label }}</span>
|
||||
<span v-if="labelCompact" class="sm:hidden">{{ labelCompact }}</span>
|
||||
<span v-if="label" :class="[truncate ? 'text-left break-all line-clamp-1' : '']">
|
||||
{{ label }}
|
||||
</span>
|
||||
</slot>
|
||||
<Icon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, useSlots } from 'vue'
|
||||
<script lang="ts">
|
||||
import { ref, computed, defineComponent, useSlots } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized, RouteLocationRaw } from 'vue-router'
|
||||
import NuxtLink from '#app/components/nuxt-link'
|
||||
import { defu } from 'defu'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
import { NuxtLink } from '#components'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Icon
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
@@ -39,10 +48,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
labelCompact: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -51,24 +56,39 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
padded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
default: () => appConfig.ui.button.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.button.size).includes(value)
|
||||
return Object.keys(appConfig.ui.button.size).includes(value)
|
||||
}
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.button.default.color,
|
||||
validator (value: string) {
|
||||
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.button.color)].includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
default: () => appConfig.ui.button.default.variant,
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.button.variant).includes(value)
|
||||
return Object.keys(appConfig.ui.button.variant).includes(value)
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.button.default.loadingIcon
|
||||
},
|
||||
leadingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
@@ -77,10 +97,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => $ui.button.icon.loading
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -101,34 +117,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.rounded
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.icon.base
|
||||
},
|
||||
leadingIconClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
trailingIconClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
square: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -137,11 +125,16 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.button>>,
|
||||
default: () => appConfig.ui.button
|
||||
}
|
||||
})
|
||||
},
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.button>>(() => defu({}, props.ui, appConfig.ui.button))
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
@@ -174,14 +167,17 @@ const isTrailing = computed(() => {
|
||||
const isSquare = computed(() => props.square || (!slots.default && !props.label))
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.button.size[props.size],
|
||||
$ui.button[isSquare.value ? 'square' : (props.compact ? 'compact' : 'spacing')][props.size],
|
||||
$ui.button.variant[props.variant],
|
||||
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center',
|
||||
props.rounded ? 'rounded-full' : props.roundedClass,
|
||||
props.customClass
|
||||
ui.value.base,
|
||||
ui.value.font,
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size],
|
||||
ui.value.gap[props.size],
|
||||
props.padded && ui.value[isSquare.value ? 'square' : 'spacing'][props.size],
|
||||
variant?.replaceAll('{color}', props.color),
|
||||
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -203,25 +199,33 @@ const trailingIconName = computed(() => {
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.button.icon.size[props.size],
|
||||
(!!slots.default || !!props.label?.length) && $ui.button.icon.leading[props.compact ? 'compactSpacing' : 'spacing'][props.size],
|
||||
props.leadingIconClass,
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[props.size],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.button.icon.size[props.size],
|
||||
(!!slots.default || !!props.label?.length) && $ui.button.icon.trailing[props.compact ? 'compactSpacing' : 'spacing'][props.size],
|
||||
props.trailingIconClass,
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[props.size],
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UButton' }
|
||||
return {
|
||||
button,
|
||||
buttonIs,
|
||||
buttonProps,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
isSquare,
|
||||
buttonClass,
|
||||
leadingIconName,
|
||||
trailingIconName,
|
||||
leadingIconClass,
|
||||
trailingIconClass
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user