mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 12:39:35 +01:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
189bd4cd3e | ||
|
|
871d3b3a85 | ||
|
|
248b0a68c6 | ||
|
|
396aae7563 | ||
|
|
dc1979cae1 | ||
|
|
d51ad93f40 | ||
|
|
c59595f2c6 | ||
|
|
a3aba1abad | ||
|
|
c37a927b4e | ||
|
|
05ea5d2d78 | ||
|
|
963d81324c | ||
|
|
927b63fa2e | ||
|
|
cefe5a76e0 | ||
|
|
a9300db91e | ||
|
|
8e1aa2f1b6 | ||
|
|
5221294f78 | ||
|
|
1bc055935e | ||
|
|
e25be118b7 | ||
|
|
93aebe6fc6 | ||
|
|
2b3dc8d065 | ||
|
|
5cf3bcf32d | ||
|
|
3b183ac9cd | ||
|
|
3400e17d17 | ||
|
|
4cd38ecc5a | ||
|
|
8380607a85 | ||
|
|
4561816b50 | ||
|
|
e6d1106b83 | ||
|
|
94f1c4e6a0 | ||
|
|
09d0ea27ab | ||
|
|
66ab95a2be | ||
|
|
2cd620899f | ||
|
|
0300be8539 | ||
|
|
5bd5dc2bca | ||
|
|
572b7a5984 | ||
|
|
ab2abae48a | ||
|
|
8298b62f21 | ||
|
|
10890e6704 | ||
|
|
3af39cacf7 | ||
|
|
58e3958390 | ||
|
|
3dd0492f91 | ||
|
|
9a73c5fb64 | ||
|
|
e7cfca2aa7 | ||
|
|
dc77cf292b | ||
|
|
05503e564c | ||
|
|
0420a17c1d | ||
|
|
a9578f8c50 | ||
|
|
d9ae1ee5b0 | ||
|
|
9e5f265f42 | ||
|
|
041f9e17de | ||
|
|
df1e4a40ca | ||
|
|
f005cbb95e | ||
|
|
d2a8a07a21 | ||
|
|
b0440f81ce | ||
|
|
4f4a659ccc | ||
|
|
beffde1849 | ||
|
|
959c968420 | ||
|
|
7cccbcfef8 | ||
|
|
df455db3ca | ||
|
|
9fc786eda0 | ||
|
|
92a9ac0c85 | ||
|
|
5a9910c2a3 | ||
|
|
a0f485c49d | ||
|
|
e92f341224 | ||
|
|
c4e0e5a685 | ||
|
|
72ee359b73 | ||
|
|
8a2b2604be | ||
|
|
d94c1b5b15 | ||
|
|
b0486140e2 | ||
|
|
b7d9c08a1c | ||
|
|
dbcb02d0ea | ||
|
|
208acca1e9 | ||
|
|
82e152be02 | ||
|
|
403899f11a | ||
|
|
914d156103 | ||
|
|
0f06b7c3fe | ||
|
|
2c454b528a | ||
|
|
b28ae68945 | ||
|
|
1171724791 | ||
|
|
ad63c72d37 | ||
|
|
d7f74d1868 | ||
|
|
a0ffdce36c | ||
|
|
a31e7dfa28 | ||
|
|
c91ea60c84 |
@@ -2,15 +2,42 @@ module.exports = {
|
||||
root: true,
|
||||
extends: ['@nuxt/eslint-config'],
|
||||
rules: {
|
||||
'semi': ['error', 'never'],
|
||||
'quotes': ['error', 'single'],
|
||||
// General
|
||||
semi: ['error', 'never'],
|
||||
quotes: ['error', 'single'],
|
||||
'comma-dangle': ['error', 'never'],
|
||||
'comma-spacing': ['error', { before: false, after: true }],
|
||||
'keyword-spacing': ['error', { before: true, after: true }],
|
||||
'space-before-function-paren': ['error', 'always'],
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'arrow-spacing': ['error', { before: true, after: true }],
|
||||
'key-spacing': ['error', { beforeColon: false, afterColon: true, mode: 'strict' }],
|
||||
'space-before-blocks': ['error', 'always'],
|
||||
'space-infix-ops': ['error', { int32Hint: false }],
|
||||
|
||||
// Typescript
|
||||
'@typescript-eslint/type-annotation-spacing': 'error',
|
||||
|
||||
// Vuejs
|
||||
'vue/multi-word-component-names': 0,
|
||||
'vue/max-attributes-per-line': ['error', {
|
||||
singleline: {
|
||||
max: 5
|
||||
'vue/html-indent': ['error', 2],
|
||||
'vue/script-indent': ['error', 2, { baseIndent: 0 }],
|
||||
'vue/keyword-spacing': ['error', { before: true, after: true }],
|
||||
'vue/object-curly-spacing': ['error', 'always'],
|
||||
'vue/key-spacing': ['error', { beforeColon: false, afterColon: true, mode: 'strict' }],
|
||||
'vue/arrow-spacing': ['error', { before: true, after: true }],
|
||||
'vue/array-bracket-spacing': ['error', 'never'],
|
||||
'vue/block-spacing': ['error', 'always'],
|
||||
'vue/brace-style': ['error', 'stroustrup', { allowSingleLine: true }],
|
||||
'vue/space-infix-ops': ['error', { int32Hint: false }],
|
||||
'vue/max-attributes-per-line': [
|
||||
'error',
|
||||
{
|
||||
singleline: {
|
||||
max: 5
|
||||
}
|
||||
}
|
||||
}]
|
||||
],
|
||||
'vue/padding-line-between-blocks': ['error', 'always']
|
||||
}
|
||||
}
|
||||
|
||||
6
.github/workflows/ci-dev.yml
vendored
6
.github/workflows/ci-dev.yml
vendored
@@ -52,12 +52,12 @@ jobs:
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Release Edge
|
||||
if: github.event_name == 'push'
|
||||
run: ./scripts/release-edge.sh
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -52,12 +52,12 @@ jobs:
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Version Check
|
||||
id: check
|
||||
uses: EndBug/version-check@v2
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ dist
|
||||
.history
|
||||
.vercel
|
||||
.idea
|
||||
.env
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -1,5 +1,48 @@
|
||||
# Changelog
|
||||
|
||||
## [2.7.0](https://github.com/nuxtlabs/ui/compare/v2.6.0...v2.7.0) (2023-08-01)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **Link:** rename from `LinkCustom` and add `exact-query` / `exact-hash` props
|
||||
* **Badge:** add colors and variants (solid has changed)
|
||||
* **SelectMenu:** invert `ui` and `ui-select` props (#432)
|
||||
|
||||
### Features
|
||||
|
||||
* **Alert:** new component ([#449](https://github.com/nuxtlabs/ui/issues/449)) ([ab2abae](https://github.com/nuxtlabs/ui/commit/ab2abae48a03200d273b4f0cb3ce300fffcee64b))
|
||||
* **Badge:** add colors and variants (solid has changed) ([05503e5](https://github.com/nuxtlabs/ui/commit/05503e564c3e8dfaee2e27e25b3409edb4c555a1))
|
||||
* **Badge:** rename `outline` to `subtle` + add `soft` variants ([5bd5dc2](https://github.com/nuxtlabs/ui/commit/5bd5dc2bcaeb59d4b0a1aea802bd3e2b2160ad53))
|
||||
* **CommandPalette:** bind active and selected to scoped slot ([#441](https://github.com/nuxtlabs/ui/issues/441)) ([b0440f8](https://github.com/nuxtlabs/ui/commit/b0440f81ce2960704ed7386ec069e52ecd7bb787))
|
||||
* **FormGroup:** add `size` prop and theme options ([#391](https://github.com/nuxtlabs/ui/issues/391)) ([d2a8a07](https://github.com/nuxtlabs/ui/commit/d2a8a07a21a4943144bd990fdbfe645ea308ae7b))
|
||||
* **Form:** new component ([#439](https://github.com/nuxtlabs/ui/issues/439)) ([a3aba1a](https://github.com/nuxtlabs/ui/commit/a3aba1abadd569b69f15697bcc5908b49e0a7f8a))
|
||||
* **Link:** rename from `LinkCustom` and add `exact-query` / `exact-hash` props ([cefe5a7](https://github.com/nuxtlabs/ui/commit/cefe5a76e0a4820f648d23734228540e3010b233))
|
||||
* **Notification:** support html with `title` and `description` slots ([#431](https://github.com/nuxtlabs/ui/issues/431)) ([df455db](https://github.com/nuxtlabs/ui/commit/df455db3caeb689ac1f24f224606183d4f5135ea))
|
||||
* **Range:** increase narrowed surface ([#459](https://github.com/nuxtlabs/ui/issues/459)) ([3b183ac](https://github.com/nuxtlabs/ui/commit/3b183ac9cde86cf3ab6fbdc5dd124d66deec865d))
|
||||
* **SelectMenu:** add `value-attribute` prop ([#429](https://github.com/nuxtlabs/ui/issues/429)) ([959c968](https://github.com/nuxtlabs/ui/commit/959c968420945fc0a143edb909c1289123fd62cb))
|
||||
* **Tabs:** new component ([#450](https://github.com/nuxtlabs/ui/issues/450)) ([8298b62](https://github.com/nuxtlabs/ui/commit/8298b62f216712fc156ef1a114d6cff3a573efdb))
|
||||
* **ui:** apply primary bg on `::selection` ([09d0ea2](https://github.com/nuxtlabs/ui/commit/09d0ea27ab36b0655106f0cf000f2c13c63b92bd))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **FormGroup:** `required` star display ([3dd0492](https://github.com/nuxtlabs/ui/commit/3dd0492f91422c8248691ac9d3f5de6d37278721))
|
||||
* **FormGroup:** err when no prop defined ([93aebe6](https://github.com/nuxtlabs/ui/commit/93aebe6fc614bc2a78109f632297c3843f7a785a))
|
||||
* **FormGroup:** missing imports ([#470](https://github.com/nuxtlabs/ui/issues/470)) ([dc1979c](https://github.com/nuxtlabs/ui/commit/dc1979cae1478cf864aab5ea573cc87d070185ce))
|
||||
* **FormGroup:** set `size` default to null ([c59595f](https://github.com/nuxtlabs/ui/commit/c59595f2c6cf414bf04bb5995ba029a59ef8035b))
|
||||
* **Form:** return state on validate ([#472](https://github.com/nuxtlabs/ui/issues/472)) ([248b0a6](https://github.com/nuxtlabs/ui/commit/248b0a68c675255a586d0ff61b0561f2f47b2682))
|
||||
* **LinkCustom:** `exact` prop wasn't working ([82e152b](https://github.com/nuxtlabs/ui/commit/82e152be02ca7b8fc5d6e27ffd81ff3f0d8a8f80)), closes [#417](https://github.com/nuxtlabs/ui/issues/417)
|
||||
* **LinkCustom:** improve prop binding and prevent error with externals ([914d156](https://github.com/nuxtlabs/ui/commit/914d156103d5ebca6b14ea705ed329508bf66d74))
|
||||
* **Link:** handle `disabled` prop ([396aae7](https://github.com/nuxtlabs/ui/commit/396aae75638da88a736179f71324374d74a89d70)), closes [#473](https://github.com/nuxtlabs/ui/issues/473)
|
||||
* **module:** ensure `red` color is safelisted for form elements ([208acca](https://github.com/nuxtlabs/ui/commit/208acca1e9269494310ff000104b21e999b66cf8)), closes [#423](https://github.com/nuxtlabs/ui/issues/423) [#373](https://github.com/nuxtlabs/ui/issues/373)
|
||||
* **module:** omit colors defined as strings ([927b63f](https://github.com/nuxtlabs/ui/commit/927b63fa2e33cc5ee303554c0c43c9e89156b7c8))
|
||||
* **module:** safelist all colors for `toast.add` ([2cd6208](https://github.com/nuxtlabs/ui/commit/2cd620899f3e997357f6274cc7a0bfc79a8277b6)), closes [#375](https://github.com/nuxtlabs/ui/issues/375) [#440](https://github.com/nuxtlabs/ui/issues/440)
|
||||
* **module:** smart safelisting for components in snake case ([e25be11](https://github.com/nuxtlabs/ui/commit/e25be118b7fe4bfd4ec26be9aacfb0d87ee94d81)), closes [#461](https://github.com/nuxtlabs/ui/issues/461)
|
||||
* **Popover:** hover mode ([#453](https://github.com/nuxtlabs/ui/issues/453)) ([10890e6](https://github.com/nuxtlabs/ui/commit/10890e6704e9884a7e86b37d0dc72e8f9c5177e7))
|
||||
* **SelectMenu:** invert `ui` and `ui-select` props ([#432](https://github.com/nuxtlabs/ui/issues/432)) ([7cccbcf](https://github.com/nuxtlabs/ui/commit/7cccbcfef899a64d63c8d33639a2d0da155c46cb))
|
||||
* **Table:** hide data when loading state is active ([#460](https://github.com/nuxtlabs/ui/issues/460)) ([2b3dc8d](https://github.com/nuxtlabs/ui/commit/2b3dc8d065c35671434975a07af4b2182a793b58))
|
||||
|
||||
## [2.6.0](https://github.com/nuxtlabs/ui/compare/v2.5.0...v2.6.0) (2023-07-18)
|
||||
|
||||
|
||||
|
||||
1
docs/.env.example
Normal file
1
docs/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
NUXT_UI_KIT_TOKEN=
|
||||
53
docs/app.vue
53
docs/app.vue
@@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div>
|
||||
<UHeader>
|
||||
@@ -18,37 +19,54 @@
|
||||
|
||||
<UColorModeButton />
|
||||
|
||||
<USocialButton to="https://twitter.com/nuxtlabs" icon="i-simple-icons-twitter" class="hidden lg:inline-flex" />
|
||||
<USocialButton to="https://github.com/nuxtlabs/ui" icon="i-simple-icons-github" class="hidden lg:inline-flex" />
|
||||
<USocialButton to="https://twitter.com/nuxtlabs" target="_blank" icon="i-simple-icons-twitter" class="hidden lg:inline-flex" />
|
||||
<USocialButton to="https://github.com/nuxtlabs/ui" target="_blank" icon="i-simple-icons-github" class="hidden lg:inline-flex" />
|
||||
</template>
|
||||
|
||||
<template #links>
|
||||
<UDocsAsideAnchors :links="anchors" />
|
||||
<UDocsAsideLinks :links="navigation" />
|
||||
<UAsideAnchors :links="anchors" />
|
||||
<UAsideLinks :links="links" />
|
||||
</template>
|
||||
</UHeader>
|
||||
|
||||
<UContainer>
|
||||
<UDocsLayout :links="navigation" :anchors="anchors">
|
||||
<NuxtPage />
|
||||
</UDocsLayout>
|
||||
</UContainer>
|
||||
<UMain>
|
||||
<UContainer>
|
||||
<UPage>
|
||||
<template #left>
|
||||
<UAside :links="links" :anchors="anchors" />
|
||||
</template>
|
||||
|
||||
<NuxtPage />
|
||||
</UPage>
|
||||
</UContainer>
|
||||
</UMain>
|
||||
|
||||
<ClientOnly>
|
||||
<UDocsSearch :files="files" :links="navigation" />
|
||||
<UDocsSearch :files="files" :links="links" />
|
||||
</ClientOnly>
|
||||
|
||||
<UNotifications />
|
||||
<UNotifications>
|
||||
<template #title="{ title }">
|
||||
<span v-html="title" />
|
||||
</template>
|
||||
|
||||
<template #description="{ description }">
|
||||
<span v-html="description" />
|
||||
</template>
|
||||
</UNotifications>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const { data: navigation } = await useAsyncData('navigation', () => fetchContentNavigation())
|
||||
const { data: links } = await useAsyncData('navigation', () => fetchContentNavigation(), {
|
||||
transform: (navigation) => mapContentLinks(navigation)
|
||||
})
|
||||
|
||||
const { data: files } = await useLazyAsyncData('files', () => queryContent().where({ _type: 'markdown', navigation: { $ne: false } }).find(), { default: () => [] })
|
||||
|
||||
provide('navigation', navigation)
|
||||
provide('links', links)
|
||||
|
||||
const anchors = [{
|
||||
label: 'Documentation',
|
||||
@@ -57,11 +75,13 @@ const anchors = [{
|
||||
}, {
|
||||
label: 'Playground',
|
||||
icon: 'i-simple-icons-stackblitz',
|
||||
to: 'https://stackblitz.com/edit/nuxtlabs-ui?file=app.config.ts,app.vue'
|
||||
to: 'https://stackblitz.com/edit/nuxtlabs-ui?file=app.config.ts,app.vue',
|
||||
target: '_blank'
|
||||
}, {
|
||||
label: 'Releases',
|
||||
icon: 'i-heroicons-rocket-launch-solid',
|
||||
to: 'https://github.com/nuxtlabs/ui/releases'
|
||||
to: 'https://github.com/nuxtlabs/ui/releases',
|
||||
target: '_blank'
|
||||
}]
|
||||
|
||||
// Computed
|
||||
@@ -81,9 +101,6 @@ useHead({
|
||||
],
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
},
|
||||
bodyAttrs: {
|
||||
class: 'antialiased font-sans text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-900'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
</NuxtLink>
|
||||
|
||||
<div class="flex-1 flex items-center justify-end gap-1.5 -my-1 lg:hidden">
|
||||
<USocialButton to="https://twitter.com/nuxtlabs" icon="i-simple-icons-twitter" />
|
||||
<USocialButton to="https://github.com/nuxtlabs/ui" icon="i-simple-icons-github" />
|
||||
<USocialButton to="https://twitter.com/nuxtlabs" target="_blank" icon="i-simple-icons-twitter" />
|
||||
<USocialButton to="https://github.com/nuxtlabs/ui" target="_blank" icon="i-simple-icons-github" />
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<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-${prop.name}`" class="block text-xs px-3 font-medium text-gray-400 dark:text-gray-500 -my-px">{{ prop.label }}</label>
|
||||
<label :for="`prop-${prop.name}`" class="block text-xs px-2.5 font-medium text-gray-400 dark:text-gray-500 -my-px">{{ prop.label }}</label>
|
||||
<UCheckbox
|
||||
v-if="prop.type === 'boolean'"
|
||||
v-if="prop.type.startsWith('boolean')"
|
||||
v-model="componentProps[prop.name]"
|
||||
:name="`prop-${prop.name}`"
|
||||
tabindex="-1"
|
||||
@@ -16,7 +16,7 @@
|
||||
:options="prop.options"
|
||||
:name="`prop-${prop.name}`"
|
||||
variant="none"
|
||||
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md', wrapper: 'relative inline-flex' }"
|
||||
:ui-menu="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md', wrapper: 'relative inline-flex' }"
|
||||
class="!py-0"
|
||||
tabindex="-1"
|
||||
:popper="{ strategy: 'fixed', placement: 'bottom-start' }"
|
||||
@@ -211,8 +211,9 @@ function renderObject (obj: any) {
|
||||
const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify(props)}`, () => transformContent('content:_markdown.md', code.value, {
|
||||
highlight: {
|
||||
theme: {
|
||||
light: 'material-lighter',
|
||||
dark: 'material-palenight'
|
||||
light: 'material-theme-lighter',
|
||||
default: 'material-theme',
|
||||
dark: 'material-theme-palenight'
|
||||
}
|
||||
}
|
||||
}), { watch: [code] })
|
||||
|
||||
@@ -23,14 +23,15 @@ const name = `U${useUpperFirst(camelName)}`
|
||||
const preset = appConfig.ui[camelName]
|
||||
|
||||
const { data: ast } = await useAsyncData(`${name}-preset`, () => transformContent('content:_markdown.md', `
|
||||
\`\`\`json
|
||||
\`\`\`json [appConfig.ui.${camelName}]
|
||||
${JSON.stringify(preset, null, 2)}
|
||||
\`\`\`\
|
||||
`, {
|
||||
highlight: {
|
||||
theme: {
|
||||
light: 'material-lighter',
|
||||
dark: 'material-palenight'
|
||||
light: 'material-theme-lighter',
|
||||
default: 'material-theme',
|
||||
dark: 'material-theme-palenight'
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -1,36 +1,21 @@
|
||||
<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>
|
||||
<FieldGroup>
|
||||
<Field v-for="prop in metaProps" :key="prop.name" v-bind="prop">
|
||||
<code v-if="prop.default">{{ prop.default }}</code>
|
||||
|
||||
<Collapsible v-if="prop.schema?.kind === 'array' && prop.schema?.schema?.filter(schema => schema.kind === 'object').length">
|
||||
<FieldGroup v-for="schema in prop.schema.schema" :key="schema.name" class="!mt-0">
|
||||
<Field v-for="subProp in Object.values(schema.schema)" :key="(subProp as any).name" v-bind="subProp" />
|
||||
</FieldGroup>
|
||||
</Collapsible>
|
||||
<Collapsible v-else-if="prop.schema?.kind === 'object' && Object.values(prop.schema.schema)?.length">
|
||||
<FieldGroup class="!mt-0">
|
||||
<Field v-for="subProp in Object.values(prop.schema.schema)" :key="(subProp as any).name" v-bind="subProp" />
|
||||
</FieldGroup>
|
||||
</Collapsible>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const items = [{
|
||||
<template>
|
||||
<UAccordion :items="items" :ui="{ wrapper: 'flex flex-col w-full' }">
|
||||
<template #default="{ item, index, open }">
|
||||
<UButton color="gray" variant="ghost" class="border-b border-gray-200 dark:border-gray-700" :ui="{ rounded :'rounded-none', padding: { sm:'p-3' } }">
|
||||
<UButton color="gray" variant="ghost" class="border-b border-gray-200 dark:border-gray-700" :ui="{ rounded: 'rounded-none', padding: { sm: 'p-3' } }">
|
||||
<template #leading>
|
||||
<div class="w-6 h-6 rounded-full bg-primary-500 dark:bg-primary-400 flex items-center justify-center -my-1">
|
||||
<UIcon :name="item.icon" class="w-4 h-4 text-white dark:text-gray-900" />
|
||||
|
||||
12
docs/components/content/examples/AlertExampleHtml.vue
Normal file
12
docs/components/content/examples/AlertExampleHtml.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<UAlert title="Heads <i>up</i>!" icon="i-heroicons-command-line">
|
||||
<template #title="{ title }">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="title" />
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
You can add <b>components</b> to your app using the <u>cli</u>.
|
||||
</template>
|
||||
</UAlert>
|
||||
</template>
|
||||
@@ -18,17 +18,17 @@ const actions = [
|
||||
|
||||
const groups = computed(() => commandPaletteRef.value?.query
|
||||
? [{
|
||||
key: 'users',
|
||||
commands: users
|
||||
}]
|
||||
key: 'users',
|
||||
commands: users
|
||||
}]
|
||||
: [{
|
||||
key: 'recent',
|
||||
label: 'Recent searches',
|
||||
commands: users.slice(0, 1)
|
||||
}, {
|
||||
key: 'actions',
|
||||
commands: actions
|
||||
}])
|
||||
key: 'recent',
|
||||
label: 'Recent searches',
|
||||
commands: users.slice(0, 1)
|
||||
}, {
|
||||
key: 'actions',
|
||||
commands: actions
|
||||
}])
|
||||
|
||||
function onSelect (option) {
|
||||
if (option.click) {
|
||||
|
||||
44
docs/components/content/examples/FormExampleBasic.vue
Normal file
44
docs/components/content/examples/FormExampleBasic.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { FormError } from '@nuxthq/ui/dist/runtime/types'
|
||||
|
||||
const state = ref({
|
||||
email: undefined,
|
||||
password: undefined
|
||||
})
|
||||
|
||||
const validate = (state: any): FormError[] => {
|
||||
const errors = []
|
||||
if (!state.email) errors.push({ path: 'email', message: 'Required' })
|
||||
if (!state.password) errors.push({ path: 'password', message: 'Required' })
|
||||
return errors
|
||||
}
|
||||
|
||||
const form = ref()
|
||||
|
||||
async function submit () {
|
||||
await form.value!.validate()
|
||||
// Do something with state.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:validate="validate"
|
||||
:state="state"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<UFormGroup label="Email" name="email">
|
||||
<UInput v-model="state.email" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Password" name="password">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormGroup>
|
||||
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
100
docs/components/content/examples/FormExampleElements.vue
Normal file
100
docs/components/content/examples/FormExampleElements.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { z } from 'zod'
|
||||
import type { Form } from '@nuxthq/ui/dist/runtime/types'
|
||||
|
||||
const options = [
|
||||
{ label: 'Option 1', value: 'option-1' },
|
||||
{ label: 'Option 2', value: 'option-2' },
|
||||
{ label: 'Option 3', value: 'option-3' }
|
||||
]
|
||||
|
||||
const state = ref({
|
||||
input: undefined,
|
||||
textarea: undefined,
|
||||
select: undefined,
|
||||
selectMenu: undefined,
|
||||
checkbox: undefined,
|
||||
toggle: undefined,
|
||||
radio: undefined,
|
||||
switch: undefined,
|
||||
range: undefined
|
||||
})
|
||||
|
||||
const schema = z.object({
|
||||
input: z.string().min(10),
|
||||
textarea: z.string().min(10),
|
||||
select: z.string().refine(value => value === 'option-2', {
|
||||
message: 'Select Option 2'
|
||||
}),
|
||||
selectMenu: z.any().refine(option => option?.value === 'option-2', {
|
||||
message: 'Select Option 2'
|
||||
}),
|
||||
toggle: z.boolean().refine(value => value === true, {
|
||||
message: 'Toggle me'
|
||||
}),
|
||||
checkbox: z.boolean().refine(value => value === true, {
|
||||
message: 'Check me'
|
||||
}),
|
||||
radio: z.string().refine(value => value === 'option-2', {
|
||||
message: 'Select Option 2'
|
||||
}),
|
||||
range: z.number().max(20, { message: 'Must be less than 20' })
|
||||
})
|
||||
|
||||
type Schema = z.infer<typeof schema>
|
||||
|
||||
const form = ref<Form<Schema>>()
|
||||
|
||||
async function submit () {
|
||||
await form.value!.validate()
|
||||
// Do something with state.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:schema="schema"
|
||||
:state="state"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<UFormGroup name="input" label="Input">
|
||||
<UInput v-model="state.input" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup name="textarea" label="Textarea">
|
||||
<UTextarea v-model="state.textarea" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup name="select" label="Select">
|
||||
<USelect v-model="state.select" placeholder="Select..." :options="options" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup name="selectMenu" label="Select Menu">
|
||||
<USelectMenu v-model="state.selectMenu" placeholder="Select..." :options="options" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup name="toggle" label="Toggle">
|
||||
<UToggle v-model="state.toggle" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup name="checkbox" label="Checkbox">
|
||||
<UCheckbox v-model="state.checkbox" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup name="radio" label="Radio">
|
||||
<URadio v-for="option in options" :key="option.value" v-model="state.radio" v-bind="option">
|
||||
{{ option.label }}
|
||||
</URadio>
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup name="range" label="Range">
|
||||
<URange v-model="state.range" />
|
||||
</UFormGroup>
|
||||
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
44
docs/components/content/examples/FormExampleJoi.vue
Normal file
44
docs/components/content/examples/FormExampleJoi.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import Joi from 'joi'
|
||||
|
||||
const schema = Joi.object({
|
||||
emailJoi: Joi.string().required(),
|
||||
passwordJoi: Joi.string()
|
||||
.min(8)
|
||||
.required()
|
||||
})
|
||||
|
||||
const state = ref({
|
||||
email: undefined,
|
||||
password: undefined
|
||||
})
|
||||
|
||||
const form = ref()
|
||||
|
||||
async function submit () {
|
||||
await form.value!.validate()
|
||||
// Do something with state.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:schema="schema"
|
||||
:state="state"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<UFormGroup label="Email" name="emailJoi">
|
||||
<UInput v-model="state.email" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Password" name="passwordJoi">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormGroup>
|
||||
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
47
docs/components/content/examples/FormExampleYup.vue
Normal file
47
docs/components/content/examples/FormExampleYup.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { object, string, InferType } from 'yup'
|
||||
import type { Form } from '@nuxthq/ui/dist/runtime/types'
|
||||
|
||||
const schema = object({
|
||||
emailYup: string().email('Invalid email').required('Required'),
|
||||
passwordYup: string()
|
||||
.min(8, 'Must be at least 8 characters')
|
||||
.required('Required')
|
||||
})
|
||||
|
||||
type Schema = InferType<typeof schema>
|
||||
|
||||
const state = ref({
|
||||
email: undefined,
|
||||
password: undefined
|
||||
})
|
||||
|
||||
const form = ref<Form<Schema>>()
|
||||
|
||||
async function submit () {
|
||||
await form.value!.validate()
|
||||
// Do something with state.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:schema="schema"
|
||||
:state="state"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<UFormGroup label="Email" name="emailYup">
|
||||
<UInput v-model="state.email" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Password" name="passwordYup">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormGroup>
|
||||
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
45
docs/components/content/examples/FormExampleZod.vue
Normal file
45
docs/components/content/examples/FormExampleZod.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { z } from 'zod'
|
||||
import type { Form } from '@nuxthq/ui/dist/runtime/types'
|
||||
|
||||
const schema = z.object({
|
||||
emailZod: z.string().email('Invalid email'),
|
||||
passwordZod: z.string().min(8, 'Must be at least 8 characters')
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
const state = ref({
|
||||
email: undefined,
|
||||
password: undefined
|
||||
})
|
||||
|
||||
const form = ref<Form<Schema>>()
|
||||
|
||||
async function submit () {
|
||||
await form.value!.validate()
|
||||
// Do something with state.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:schema="schema"
|
||||
:state="state"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<UFormGroup label="Email" name="emailZod">
|
||||
<UInput v-model="state.email" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Password" name="passwordZod">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormGroup>
|
||||
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup>
|
||||
const toast = useToast()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Show toast" @click="toast.add({ title: 'Notification <i>italic</i>', description: 'This is an <u>underlined</u> and <b>bold</b> notification.' })" />
|
||||
</template>
|
||||
11
docs/components/content/examples/PopoverExampleMode.vue
Normal file
11
docs/components/content/examples/PopoverExampleMode.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<UPopover mode="hover">
|
||||
<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>
|
||||
@@ -5,22 +5,19 @@ const people = [{
|
||||
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'
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
const people = [{
|
||||
id: 1,
|
||||
name: 'Wade Cooper'
|
||||
}, {
|
||||
id: 2,
|
||||
name: 'Arlene Mccoy'
|
||||
}, {
|
||||
id: 3,
|
||||
name: 'Devon Webb'
|
||||
}, {
|
||||
id: 4,
|
||||
name: 'Tom Cook'
|
||||
}]
|
||||
|
||||
const selected = ref(people[0].id)
|
||||
|
||||
const current = computed(() => people.find(person => person.id === selected.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
v-model="selected"
|
||||
:options="people"
|
||||
placeholder="Select people"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
>
|
||||
<template #label>
|
||||
{{ current.name }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
245
docs/components/content/examples/TableExampleAdvanced.vue
Normal file
245
docs/components/content/examples/TableExampleAdvanced.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script lang="ts" setup>
|
||||
// Columns
|
||||
const columns = [{
|
||||
key: 'id',
|
||||
label: '#',
|
||||
sortable: true
|
||||
}, {
|
||||
key: 'title',
|
||||
label: 'Title',
|
||||
sortable: true
|
||||
}, {
|
||||
key: 'completed',
|
||||
label: 'Status',
|
||||
sortable: true
|
||||
}, {
|
||||
key: 'actions',
|
||||
label: 'Actions',
|
||||
sortable: false
|
||||
}]
|
||||
|
||||
const selectedColumns = ref(columns)
|
||||
const columnsTable = computed(() => columns.filter((column) => selectedColumns.value.includes(column)))
|
||||
|
||||
// Selected Rows
|
||||
const selectedRows = ref([])
|
||||
|
||||
function select (row) {
|
||||
const index = selectedRows.value.findIndex((item) => item.id === row.id)
|
||||
if (index === -1) {
|
||||
selectedRows.value.push(row)
|
||||
} else {
|
||||
selectedRows.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
const actions = [
|
||||
[{
|
||||
key: 'completed',
|
||||
label: 'Completed',
|
||||
icon: 'i-heroicons-check'
|
||||
}], [{
|
||||
key: 'uncompleted',
|
||||
label: 'In Progress',
|
||||
icon: 'i-heroicons-arrow-path'
|
||||
}]
|
||||
]
|
||||
|
||||
// Filters
|
||||
const todoStatus = [{
|
||||
key: 'uncompleted',
|
||||
label: 'In Progress',
|
||||
value: false
|
||||
}, {
|
||||
key: 'completed',
|
||||
label: 'Completed',
|
||||
value: true
|
||||
}]
|
||||
|
||||
const search = ref('')
|
||||
const selectedStatus = ref([])
|
||||
const searchStatus = computed(() => {
|
||||
if (selectedStatus.value?.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (selectedStatus?.value?.length > 1) {
|
||||
return `?completed=${selectedStatus.value[0].value}&completed=${selectedStatus.value[1].value}`
|
||||
}
|
||||
|
||||
return `?completed=${selectedStatus.value[0].value}`
|
||||
})
|
||||
|
||||
const resetFilters = () => {
|
||||
search.value = ''
|
||||
selectedStatus.value = []
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const page = ref(1)
|
||||
const pageCount = ref(10)
|
||||
const pageTotal = ref(200) // This value should be dynamic coming from the API
|
||||
const pageFrom = computed(() => (page.value - 1) * pageCount.value + 1)
|
||||
const pageTo = computed(() => Math.min(page.value * pageCount.value, pageTotal.value))
|
||||
|
||||
// Data
|
||||
const { data: todos, pending } = await useLazyAsyncData('todos', () => $fetch<{
|
||||
id: number
|
||||
title: string
|
||||
completed: string
|
||||
}[]>(`https://jsonplaceholder.typicode.com/todos${searchStatus.value}`, {
|
||||
query: {
|
||||
q: search.value,
|
||||
'_page': page.value,
|
||||
'_limit': pageCount.value
|
||||
}
|
||||
}), {
|
||||
default: () => [],
|
||||
watch: [page, search, searchStatus, pageCount]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCard
|
||||
class="w-full"
|
||||
:ui="{
|
||||
base: '',
|
||||
ring: '',
|
||||
divide: 'divide-y divide-gray-200 dark:divide-gray-700',
|
||||
header: { padding: 'px-4 py-5' },
|
||||
body: { padding: '', base: 'divide-y divide-gray-200 dark:divide-gray-700' },
|
||||
footer: { padding: 'p-4' }
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<h2 class="font-semibold text-xl text-gray-900 dark:text-white leading-tight">
|
||||
Todos
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="flex items-center justify-between gap-3 px-4 py-3">
|
||||
<UInput v-model="search" icon="i-heroicons-magnifying-glass-20-solid" placeholder="Search..." />
|
||||
|
||||
<USelectMenu v-model="selectedStatus" :options="todoStatus" multiple placeholder="Status" class="w-40" />
|
||||
</div>
|
||||
|
||||
<!-- Header and Action buttons -->
|
||||
<div class="flex justify-between items-center w-full px-4 py-3">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="text-sm leading-5">Rows per page:</span>
|
||||
|
||||
<USelect
|
||||
v-model="pageCount"
|
||||
:options="[3, 5, 10, 20, 30, 40]"
|
||||
class="me-2 w-20"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1.5 items-center">
|
||||
<UDropdown v-if="selectedRows.length > 1" :items="actions" :ui="{ width: 'w-36' }">
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-down"
|
||||
trailing
|
||||
variant="soft"
|
||||
size="xs"
|
||||
>
|
||||
Mark as
|
||||
</UButton>
|
||||
</UDropdown>
|
||||
|
||||
<USelectMenu v-model="selectedColumns" :options="columns" multiple>
|
||||
<UButton
|
||||
icon="i-heroicons-view-columns"
|
||||
variant="soft"
|
||||
size="xs"
|
||||
>
|
||||
Columns
|
||||
</UButton>
|
||||
</USelectMenu>
|
||||
|
||||
<UButton
|
||||
icon="i-heroicons-funnel"
|
||||
variant="soft"
|
||||
color="red"
|
||||
size="xs"
|
||||
:disabled="search === '' && selectedStatus.length === 0"
|
||||
@click="resetFilters"
|
||||
>
|
||||
Reset
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<UTable
|
||||
v-model="selectedRows"
|
||||
:rows="todos"
|
||||
:columns="columnsTable"
|
||||
:loading="pending"
|
||||
sort-asc-icon="i-heroicons-arrow-up"
|
||||
sort-desc-icon="i-heroicons-arrow-down"
|
||||
@select="select"
|
||||
>
|
||||
<template #completed-data="{ row }">
|
||||
<UBadge size="xs" :label="row.completed ? 'Completed' : 'In Progress'" :color="row.completed ? 'emerald' : 'orange'" variant="subtle" />
|
||||
</template>
|
||||
|
||||
<template #actions-data="{ row }">
|
||||
<UButton
|
||||
v-if="!row.completed"
|
||||
icon="i-heroicons-check"
|
||||
size="2xs"
|
||||
color="emerald"
|
||||
variant="outline"
|
||||
:ui="{ rounded: 'rounded-full' }"
|
||||
square
|
||||
/>
|
||||
|
||||
<UButton
|
||||
v-else
|
||||
icon="i-heroicons-arrow-path"
|
||||
size="2xs"
|
||||
color="orange"
|
||||
variant="outline"
|
||||
:ui="{ rounded: 'rounded-full' }"
|
||||
square
|
||||
/>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<!-- Number of rows & Pagination -->
|
||||
<template #footer>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<span class="text-sm leading-5">
|
||||
Showing
|
||||
<span class="font-medium">{{ pageFrom }}</span>
|
||||
to
|
||||
<span class="font-medium">{{ pageTo }}</span>
|
||||
of
|
||||
<span class="font-medium">{{ pageTotal }}</span>
|
||||
results
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<UPagination
|
||||
v-model="page"
|
||||
:page-count="pageCount"
|
||||
:total="pageTotal"
|
||||
:ui="{
|
||||
wrapper: 'flex items-center gap-1',
|
||||
rounded: 'rounded-full min-w-[32px] justify-center',
|
||||
default: {
|
||||
activeButton: {
|
||||
variant: 'outline'
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
17
docs/components/content/examples/TabsExampleBasic.vue
Normal file
17
docs/components/content/examples/TabsExampleBasic.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
const items = [{
|
||||
label: 'Tab1',
|
||||
content: 'This is the content shown for Tab1'
|
||||
}, {
|
||||
label: 'Tab2',
|
||||
disabled: true,
|
||||
content: 'And, this is the content for Tab2'
|
||||
}, {
|
||||
label: 'Tab3',
|
||||
content: 'Finally, this is the content for Tab3'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" />
|
||||
</template>
|
||||
17
docs/components/content/examples/TabsExampleCard.vue
Normal file
17
docs/components/content/examples/TabsExampleCard.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
const items = [{
|
||||
label: 'Tab1',
|
||||
content: 'This is the content shown for Tab1'
|
||||
}, {
|
||||
label: 'Tab2',
|
||||
disabled: true,
|
||||
content: 'And, this is the content for Tab2'
|
||||
}, {
|
||||
label: 'Tab3',
|
||||
content: 'Finally, this is the content for Tab3'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" />
|
||||
</template>
|
||||
29
docs/components/content/examples/TabsExampleDefaultSlot.vue
Normal file
29
docs/components/content/examples/TabsExampleDefaultSlot.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
const items = [{
|
||||
label: 'Getting Started',
|
||||
icon: 'i-heroicons-information-circle',
|
||||
content: 'This is the content shown for Tab1'
|
||||
}, {
|
||||
label: 'Installation',
|
||||
icon: 'i-heroicons-arrow-down-tray',
|
||||
content: 'And, this is the content for Tab2'
|
||||
}, {
|
||||
label: 'Theming',
|
||||
icon: 'i-heroicons-eye-dropper',
|
||||
content: 'Finally, this is the content for Tab3'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" class="w-full">
|
||||
<template #default="{ item, index, selected }">
|
||||
<div class="flex items-center gap-2 relative truncate">
|
||||
<UIcon :name="item.icon" class="w-4 h-4 flex-shrink-0" />
|
||||
|
||||
<span class="truncate">{{ index + 1 }}. {{ item.label }}</span>
|
||||
|
||||
<span v-if="selected" class="absolute -right-4 w-2 h-2 rounded-full bg-primary-500 dark:bg-primary-400" />
|
||||
</div>
|
||||
</template>
|
||||
</UTabs>
|
||||
</template>
|
||||
16
docs/components/content/examples/TabsExampleIndex.vue
Normal file
16
docs/components/content/examples/TabsExampleIndex.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
const items = [{
|
||||
label: 'Tab1',
|
||||
content: 'This is the content shown for Tab1'
|
||||
}, {
|
||||
label: 'Tab2',
|
||||
content: 'And, this is the content for Tab2'
|
||||
}, {
|
||||
label: 'Tab3',
|
||||
content: 'Finally, this is the content for Tab3'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" :default-index="2" />
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
const items = [{
|
||||
slot: 'account',
|
||||
label: 'Account'
|
||||
}, {
|
||||
slot: 'password',
|
||||
label: 'Password'
|
||||
}]
|
||||
|
||||
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
|
||||
const passwordForm = reactive({ currentPassword: '', newPassword: '' })
|
||||
|
||||
function onSubmitAccount () {
|
||||
console.log('Submitted form:', accountForm)
|
||||
}
|
||||
|
||||
function onSubmitPassword () {
|
||||
console.log('Submitted form:', passwordForm)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" class="w-full">
|
||||
<template #account="{ item }">
|
||||
<UCard @submit.prevent="onSubmitAccount">
|
||||
<template #header>
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{{ item.label }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Make changes to your account here. Click save when you're done.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<UFormGroup label="Name" name="name" class="mb-3">
|
||||
<UInput v-model="accountForm.name" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Username" name="username">
|
||||
<UInput v-model="accountForm.username" />
|
||||
</UFormGroup>
|
||||
|
||||
<template #footer>
|
||||
<UButton type="submit" color="black">
|
||||
Save account
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<template #password="{ item }">
|
||||
<UCard @submit.prevent="onSubmitPassword">
|
||||
<template #header>
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{{ item.label }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Change your password here. After saving, you'll be logged out.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<UFormGroup label="Current Password" name="current" required class="mb-3">
|
||||
<UInput v-model="passwordForm.currentPassword" type="password" required />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="New Password" name="new" required>
|
||||
<UInput v-model="passwordForm.newPassword" type="password" required />
|
||||
</UFormGroup>
|
||||
|
||||
<template #footer>
|
||||
<UButton type="submit" color="black">
|
||||
Save password
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UTabs>
|
||||
</template>
|
||||
58
docs/components/content/examples/TabsExampleItemSlot.vue
Normal file
58
docs/components/content/examples/TabsExampleItemSlot.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
const items = [{
|
||||
key: 'account',
|
||||
label: 'Account',
|
||||
description: 'Make changes to your account here. Click save when you\'re done.'
|
||||
}, {
|
||||
key: 'password',
|
||||
label: 'Password',
|
||||
description: 'Change your password here. After saving, you\'ll be logged out.'
|
||||
}]
|
||||
|
||||
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
|
||||
const passwordForm = reactive({ currentPassword: '', newPassword: '' })
|
||||
|
||||
function onSubmit (form) {
|
||||
console.log('Submitted form:', form)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" class="w-full">
|
||||
<template #item="{ item }">
|
||||
<UCard @submit.prevent="() => onSubmit(item.key === 'account' ? accountForm : passwordForm)">
|
||||
<template #header>
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{{ item.label }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div v-if="item.key === 'account'" class="space-y-3">
|
||||
<UFormGroup label="Name" name="name">
|
||||
<UInput v-model="accountForm.name" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Username" name="username">
|
||||
<UInput v-model="accountForm.username" />
|
||||
</UFormGroup>
|
||||
</div>
|
||||
<div v-else-if="item.key === 'password'" class="space-y-3">
|
||||
<UFormGroup label="Current Password" name="current" required>
|
||||
<UInput v-model="passwordForm.currentPassword" type="password" required />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="New Password" name="new" required>
|
||||
<UInput v-model="passwordForm.newPassword" type="password" required />
|
||||
</UFormGroup>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<UButton type="submit" color="black">
|
||||
Save {{ item.key === 'account' ? 'account' : 'password' }}
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UTabs>
|
||||
</template>
|
||||
16
docs/components/content/examples/TabsExampleVertical.vue
Normal file
16
docs/components/content/examples/TabsExampleVertical.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
const items = [{
|
||||
label: 'Tab1',
|
||||
content: 'This is the content shown for Tab1'
|
||||
}, {
|
||||
label: 'Tab2',
|
||||
content: 'And, this is the content for Tab2'
|
||||
}, {
|
||||
label: 'Tab3',
|
||||
content: 'Finally, this is the content for Tab3'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" orientation="vertical" :ui="{ wrapper: 'flex items-center gap-4', list: { width: 'w-48' } }" />
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
const links = [{
|
||||
avatar: {
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
},
|
||||
label: 'Benjamin Canac'
|
||||
}, {
|
||||
label: 'KeJun'
|
||||
}]
|
||||
|
||||
const { ui } = useAppConfig()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation :links="links">
|
||||
<template #avatar="{ link }">
|
||||
<UAvatar
|
||||
v-if="link.avatar"
|
||||
v-bind="{ size: ui.verticalNavigation.avatar.size, ...link.avatar }"
|
||||
:class="[ui.verticalNavigation.avatar.base]"
|
||||
/>
|
||||
<UIcon v-else name="i-heroicons-user-circle-20-solid" class="text-lg" />
|
||||
</template>
|
||||
</UVerticalNavigation>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup>
|
||||
const links = [{
|
||||
label: '.github',
|
||||
icon: 'i-heroicons-folder-20-solid',
|
||||
badge: 'chore(github): use pnpm 8',
|
||||
time: 'last month'
|
||||
}, {
|
||||
label: '.editorconfig',
|
||||
icon: 'i-heroicons-document-solid',
|
||||
badge: 'Initial commit',
|
||||
time: '2 years ago'
|
||||
}, {
|
||||
label: '.package.json',
|
||||
icon: 'i-heroicons-document-solid',
|
||||
badge: 'chore(deps): bump',
|
||||
time: '16 hours ago'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation
|
||||
:links="links"
|
||||
class="w-full"
|
||||
:ui="{
|
||||
label: 'truncate relative text-gray-900 dark:text-white flex-initial w-32 text-left'
|
||||
}"
|
||||
>
|
||||
<template #badge="{ link }">
|
||||
<div class="flex-1 flex justify-between relative truncate">
|
||||
<div>{{ link.badge }}</div>
|
||||
<div>{{ link.time }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</UVerticalNavigation>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
const links = [{
|
||||
label: 'Navigation',
|
||||
children: [{
|
||||
label: 'Vertical Navigation',
|
||||
to: '/navigation/vertical-navigation'
|
||||
}, {
|
||||
label: 'Command Palette',
|
||||
to: '/navigation/command-palette'
|
||||
}]
|
||||
}, {
|
||||
label: 'Data',
|
||||
children: [{
|
||||
label: 'Table',
|
||||
to: '/data/table'
|
||||
}]
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation :links="links">
|
||||
<template #default="{ link }">
|
||||
<div class="relative text-left w-full">
|
||||
<div class="mb-2">
|
||||
{{ link.label }}
|
||||
</div>
|
||||
<UVerticalNavigation v-if="link.children" :links="link.children" />
|
||||
</div>
|
||||
</template>
|
||||
</UVerticalNavigation>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup>
|
||||
const types = {
|
||||
bug: {
|
||||
icon: 'i-heroicons-bug-ant-20-solid',
|
||||
color: 'text-red-500'
|
||||
},
|
||||
docs: {
|
||||
icon: 'i-heroicons-document-text-20-solid',
|
||||
color: 'text-blue-500'
|
||||
},
|
||||
lock: {
|
||||
icon: 'i-heroicons-lock-closed-20-solid',
|
||||
color: 'text-gray dark:text-white'
|
||||
},
|
||||
default: {
|
||||
icon: 'i-heroicons-question-mark-circle-20-solid',
|
||||
color: 'text-green-500'
|
||||
}
|
||||
}
|
||||
const links = [{
|
||||
label: 'UDropdown and UPopover dropdown menu, dropdown will be obscured',
|
||||
type: 'bug'
|
||||
}, {
|
||||
label: 'Uncaught (in promise) ReferenceError: ref is not defined',
|
||||
type: 'lock'
|
||||
}, {
|
||||
label: 'Fully styled and customizable components for Nuxt.',
|
||||
type: 'docs'
|
||||
}, {
|
||||
label: 'Can I pass a tailwind color to UNotifications with `toast.add()` ?'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation :links="links">
|
||||
<template #icon="{ link }">
|
||||
<UIcon v-if="link.type" :name="types[link.type].icon" :class="types[link.type].color" class="text-base" />
|
||||
<UIcon v-else :name="types.default.icon" :class="types.default.color" class="text-base" />
|
||||
</template>
|
||||
</UVerticalNavigation>
|
||||
</template>
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup>
|
||||
const commandPaletteRef = ref()
|
||||
|
||||
const navigation = inject('navigation')
|
||||
const links = inject('links')
|
||||
|
||||
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 => ({
|
||||
const groups = computed(() => links.value.map(item => ({
|
||||
key: item.to,
|
||||
label: item.label,
|
||||
commands: files.value.filter(file => file._path.startsWith(item.to)).map(file => ({
|
||||
id: file._id,
|
||||
icon: 'i-heroicons-document',
|
||||
title: file.navigation?.title || file.title,
|
||||
|
||||
@@ -8,6 +8,10 @@ description: 'Learn how to install and configure the module in your Nuxt app.'
|
||||
|
||||
::code-group
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm i -D @nuxthq/ui
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add -D @nuxthq/ui
|
||||
```
|
||||
@@ -16,10 +20,6 @@ yarn add -D @nuxthq/ui
|
||||
npm install -D @nuxthq/ui
|
||||
```
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm i -D @nuxthq/ui
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
2. Add it to your `modules` section in your `nuxt.config`:
|
||||
@@ -73,16 +73,35 @@ If you do so, you'll need to add the following to your `.vscode/settings.json`:
|
||||
}
|
||||
```
|
||||
|
||||
Also, the extension won't work when writing classes in your `app.config.ts` by default. You can add the following to your `.vscode/settings.json` to fix this:
|
||||
Note, the extension won't work when writing classes in your `app.config.ts` by default.
|
||||
|
||||
Also, you might want IntelliSense on objects in your SFC by prefixing with `/*ui*/`.
|
||||
|
||||
To enable these two features, you can add the following to your `.vscode/settings.json`:
|
||||
|
||||
```json [.vscode/settings.json]
|
||||
{
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["ui:\\s*{([^)]*)\\s*}", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
["ui:\\s*{([^)]*)\\s*}", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||
["/\\*ui\\*/\\s*{([^;]*)}", ":\\s*[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
An example SFC using IntelliSense (note the `/*ui*/` prefix, also works with `ref()`):
|
||||
|
||||
```vue [example.vue]
|
||||
<template>
|
||||
<UCard :ui="ui" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const ui = /*ui*/ {
|
||||
background: 'bg-white dark:bg-slate-900'
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
You can also add the following to your `.vscode/settings.json` to enable IntelliSense when using the `ui` prop:
|
||||
|
||||
```json [.vscode/settings.json]
|
||||
@@ -131,4 +150,4 @@ Update your `package.json` to the following:
|
||||
}
|
||||
```
|
||||
|
||||
Then run `npm install` or `yarn install`.
|
||||
Then run `pnpm install`, `yarn install` or `npm install`.
|
||||
|
||||
@@ -48,7 +48,7 @@ defineShortcuts({
|
||||
</script>
|
||||
```
|
||||
|
||||
Shortcuts keys are written as the literal keyboard key value. Combinations are made with `_` separator. Chained shortcuts are made with `-` separator. :u-badge{label="New" class="!rounded-full"}
|
||||
Shortcuts keys are written as the literal keyboard key value. Combinations are made with `_` separator. Chained shortcuts are made with `-` separator.
|
||||
|
||||
Modifiers are also available:
|
||||
- `meta`: acts as `Command` for MacOS and `Control` for others
|
||||
|
||||
@@ -134,6 +134,23 @@ const label = computed(() => date.value.toLocaleDateString('en-us', { weekday: '
|
||||
```
|
||||
::
|
||||
|
||||
### Table
|
||||
|
||||
Here is an example of a Table component with all its features implemented.
|
||||
|
||||
::component-example
|
||||
---
|
||||
padding: false
|
||||
---
|
||||
|
||||
#default
|
||||
:table-example-advanced
|
||||
::
|
||||
|
||||
::callout{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/examples/TableExampleAdvanced.vue"}
|
||||
Take a look at the component!
|
||||
::
|
||||
|
||||
## Theming
|
||||
|
||||
Our theming system provides a lot of flexibility to customize the components.
|
||||
|
||||
@@ -8,7 +8,7 @@ links:
|
||||
icon: i-simple-icons-headlessui
|
||||
to: 'https://headlessui.com/vue/disclosure'
|
||||
navigation:
|
||||
badge: 'Edge'
|
||||
badge: New
|
||||
---
|
||||
|
||||
## Usage
|
||||
189
docs/content/2.elements/2.alert.md
Normal file
189
docs/content/2.elements/2.alert.md
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
description: Display an alert element to draw attention.
|
||||
links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Alert.vue
|
||||
navigation:
|
||||
badge: Edge
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Pass a `title` to your Alert.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
title: 'Heads up!'
|
||||
---
|
||||
::
|
||||
|
||||
### Description
|
||||
|
||||
You can add a `description` in addition of the `title`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
title: 'Heads up!'
|
||||
props:
|
||||
description: 'You can add components to your app using the cli.'
|
||||
---
|
||||
::
|
||||
|
||||
### 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:
|
||||
title: 'Heads up!'
|
||||
props:
|
||||
icon: 'i-heroicons-command-line'
|
||||
description: 'You can add components to your app using the cli.'
|
||||
excludedProps:
|
||||
- icon
|
||||
---
|
||||
::
|
||||
|
||||
### Avatar
|
||||
|
||||
Use the [avatar](/elements/avatar) prop as an `object` and configure it with any of its props.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
title: 'Heads up!'
|
||||
props:
|
||||
description: 'You can add components to your app using the cli.'
|
||||
avatar:
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
excludedProps:
|
||||
- avatar
|
||||
---
|
||||
::
|
||||
|
||||
### Style
|
||||
|
||||
Use the `color` and `variant` props to change the visual style of the Alert.
|
||||
|
||||
- `color` can be any color from the `ui.colors` object or `white` (default).
|
||||
- `variant` can be `solid` (default), `outline`, `soft` or `subtle`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
title: 'Heads up!'
|
||||
description: 'You can add components to your app using the cli.'
|
||||
props:
|
||||
icon: 'i-heroicons-command-line'
|
||||
color: 'primary'
|
||||
variant: 'solid'
|
||||
extraColors:
|
||||
- white
|
||||
excludedProps:
|
||||
- icon
|
||||
---
|
||||
::
|
||||
|
||||
### Close
|
||||
|
||||
Use the `close-button` prop to hide or customize the close button on the Alert.
|
||||
|
||||
You can pass all the props of the [Button](/elements/button) component to customize it through the `close-button` prop or globally through `ui.alert.default.closeButton`.
|
||||
|
||||
It defaults to `null` which means no close button will be displayed. A `close` event will be emitted when the close button is clicked.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
title: 'Heads up!'
|
||||
props:
|
||||
closeButton:
|
||||
icon: 'i-heroicons-x-mark-20-solid'
|
||||
color: 'gray'
|
||||
variant: 'link'
|
||||
padded: false
|
||||
excludedProps:
|
||||
- closeButton
|
||||
---
|
||||
::
|
||||
|
||||
### Actions
|
||||
|
||||
Use the `actions` prop to add actions to the Alert.
|
||||
|
||||
Like for `closeButton`, you can pass all the props of the [Button](/elements/button) component plus a `click` function in the action but also customize the default style for the actions globally through `ui.alert.default.actionButton`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
title: 'Heads up!'
|
||||
props:
|
||||
actions:
|
||||
- label: Action 1
|
||||
- variant: 'ghost'
|
||||
color: 'gray'
|
||||
label: Action 2
|
||||
excludedProps:
|
||||
- actions
|
||||
---
|
||||
::
|
||||
|
||||
Actions will render differently whether you have a `description` set.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
title: 'Heads up!'
|
||||
description: 'You can add components to your app using the cli.'
|
||||
props:
|
||||
actions:
|
||||
- variant: 'solid'
|
||||
color: 'primary'
|
||||
label: Action 1
|
||||
- variant: 'outline'
|
||||
color: 'primary'
|
||||
label: Action 2
|
||||
excludedProps:
|
||||
- actions
|
||||
---
|
||||
::
|
||||
|
||||
## Slots
|
||||
|
||||
### `title` / `description`
|
||||
|
||||
Use the `#title` and `#description` slots to customize the Alert.
|
||||
|
||||
This can be handy when you want to display HTML content. To achieve this, you can define those slots and use the `v-html` directive.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:alert-example-html
|
||||
|
||||
#code
|
||||
```vue
|
||||
<template>
|
||||
<UAlert title="Heads <i>up</i>!" icon="i-heroicons-command-line">
|
||||
<template #title="{ title }">
|
||||
<span v-html="title" />
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
You can add <b>components</b> to your app using the <u>cli</u>.
|
||||
</template>
|
||||
</UAlert>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
description: Display a short text to represent a status or a category.
|
||||
links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Badge.vue
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
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: 'sm'
|
||||
---
|
||||
|
||||
Badge
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
@@ -32,7 +32,7 @@ baseProps:
|
||||
|
||||
### Chip
|
||||
|
||||
Use the `chip-color`, `chip-text` :u-badge{label="New" class="!rounded-full"} and `chip-position` props to display a chip on the Avatar.
|
||||
Use the `chip-color`, `chip-text` and `chip-position` props to display a chip on the Avatar.
|
||||
|
||||
::component-card
|
||||
---
|
||||
139
docs/content/2.elements/4.badge.md
Normal file
139
docs/content/2.elements/4.badge.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
description: Display a short text to represent a status or a category.
|
||||
links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Badge.vue
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
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.
|
||||
|
||||
- `variant` can be `solid` (default), `outline`, `soft` or `subtle`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
color: 'primary'
|
||||
variant: 'solid'
|
||||
---
|
||||
|
||||
Badge
|
||||
::
|
||||
|
||||
Besides all the colors from the `ui.colors` object, you can also use the `white` and `black` colors with their pre-defined variants.
|
||||
|
||||
#### White :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
color: 'white'
|
||||
variant: 'solid'
|
||||
ui:
|
||||
variant:
|
||||
solid: 1
|
||||
excludedProps:
|
||||
- color
|
||||
---
|
||||
|
||||
Badge
|
||||
::
|
||||
|
||||
#### Gray :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
color: 'gray'
|
||||
variant: 'solid'
|
||||
ui:
|
||||
variant:
|
||||
solid: 1
|
||||
excludedProps:
|
||||
- color
|
||||
---
|
||||
|
||||
Badge
|
||||
::
|
||||
|
||||
#### Black :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
color: 'black'
|
||||
variant: 'solid'
|
||||
ui:
|
||||
variant:
|
||||
solid: 1
|
||||
link: 1
|
||||
excludedProps:
|
||||
- color
|
||||
---
|
||||
|
||||
Badge
|
||||
::
|
||||
|
||||
### Size
|
||||
|
||||
Use the `size` prop to change the size of the Badge.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
size: 'sm'
|
||||
---
|
||||
|
||||
Badge
|
||||
::
|
||||
|
||||
### Rounded
|
||||
|
||||
To customize the border radius of the Badge, you can use the `ui` prop.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
ui:
|
||||
rounded: 'rounded-full'
|
||||
excludedProps:
|
||||
- ui
|
||||
---
|
||||
|
||||
Badge
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
You can customize the whole [preset](#preset) by using the `ui` prop.
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
@@ -112,6 +112,26 @@ props:
|
||||
Button
|
||||
::
|
||||
|
||||
### Rounded
|
||||
|
||||
To customize the border radius of the Button, you can use the `ui` prop.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
ui:
|
||||
rounded: 'rounded-full'
|
||||
excludedProps:
|
||||
- ui
|
||||
---
|
||||
|
||||
Button
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
You can customize the whole [preset](#preset) by using the `ui` prop.
|
||||
::
|
||||
|
||||
### Icon
|
||||
|
||||
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
|
||||
@@ -204,6 +224,8 @@ props:
|
||||
Button
|
||||
::
|
||||
|
||||
You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `target`, `exact`, etc.
|
||||
|
||||
### Padded
|
||||
|
||||
Use the `padded` prop to remove the padding of the Button.
|
||||
@@ -22,7 +22,7 @@ Pass an array of arrays to the `items` prop of the Dropdown component. Each arra
|
||||
- `disabled` - Whether the item is disabled.
|
||||
- `click` - The click handler of the item.
|
||||
|
||||
You can also pass properties from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
|
||||
You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
24
docs/content/2.elements/9.link.md
Normal file
24
docs/content/2.elements/9.link.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: 'Link'
|
||||
description: Render a NuxtLink but with superpowers.
|
||||
links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Link.vue
|
||||
navigation:
|
||||
badge: Edge
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
The Link component is a wrapper around [`<NuxtLink>`](https://nuxt.com/docs/api/components/nuxt-link) through the [`custom`](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-custom) prop that provides a few extra props:
|
||||
|
||||
- `inactive-class` prop to set a class when the link is inactive, `active-class` is used when active.
|
||||
- `exact` prop to style with `active-class` when the link is active and the route is exactly the same as the current route.
|
||||
- `exact-query` and `exact-hash` props to style with `active-class` when the link is active and the query or hash is exactly the same as the current query or hash.
|
||||
|
||||
The incentive behind this is to provide the same API as NuxtLink back in Nuxt 2 / Vue 2. You can read more about it in the Vue Router [migration from Vue 2](https://router.vuejs.org/guide/migration/#removal-of-the-exact-prop-in-router-link) guide.
|
||||
|
||||
It also renders an `<a>` tag when a `to` prop is provided, otherwise it renders a `<button>` tag.
|
||||
|
||||
It is used underneath by the [Button](/elements/button), [Dropdown](/elements/dropdown) and [VerticalNavigation](/navigation/vertical-navigation) components.
|
||||
321
docs/content/3.forms/10.form.md
Normal file
321
docs/content/3.forms/10.form.md
Normal file
@@ -0,0 +1,321 @@
|
||||
---
|
||||
description: Collect and validate form data.
|
||||
links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Form.ts
|
||||
navigation:
|
||||
badge: Edge
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Use the Form component to validate form data using schema libraries such as [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi) or your own validation logic. It works seamlessly with the FormGroup component to automatically display error messages around form elements.
|
||||
|
||||
The Form component requires the `validate` and `state` props for form validation.
|
||||
|
||||
- `state` - a reactive object that holds the current state of the form.
|
||||
- `validate` - a function that takes the form's state as input and returns an array of `FormError` objects with the following fields:
|
||||
- `message` - the error message to display.
|
||||
- `path` - the path to the form element matching the `name`.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:form-example-basic{class="space-y-4 w-60"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { FormError } from '@nuxthq/ui/dist/runtime/types'
|
||||
|
||||
const state = ref({
|
||||
email: undefined,
|
||||
password: undefined
|
||||
})
|
||||
|
||||
const validate = (state: any): FormError[] => {
|
||||
const errors = []
|
||||
if (!state.email) errors.push({ path: 'email', message: 'Required' })
|
||||
if (!state.password) errors.push({ path: 'password', message: 'Required' })
|
||||
return errors
|
||||
}
|
||||
|
||||
const form = ref()
|
||||
|
||||
async function submit () {
|
||||
await form.value!.validate()
|
||||
// Do something with state.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:validate="validate"
|
||||
:state="state"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<UFormGroup label="Email" name="email">
|
||||
<UInput v-model="state.email" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Password" name="password">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormGroup>
|
||||
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Schema
|
||||
|
||||
You can provide a schema from [Yup](#yup), [Zod](#zod) or [Joi](#joi) through the `schema` prop to validate the state. It's important to note that **none of these libraries are included** by default, so make sure to **install the one you need**.
|
||||
|
||||
### Yup
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:form-example-yup{class="space-y-4 w-60"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { object, string, InferType } from 'yup'
|
||||
|
||||
const schema = object({
|
||||
email: string().email('Invalid email').required('Required'),
|
||||
password: string()
|
||||
.min(8, 'Must be at least 8 characters')
|
||||
.required('Required')
|
||||
})
|
||||
|
||||
const state = ref({
|
||||
email: undefined,
|
||||
password: undefined
|
||||
})
|
||||
|
||||
const form = ref()
|
||||
|
||||
async function submit () {
|
||||
await form.value!.validate()
|
||||
// Do something with state.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:schema="schema"
|
||||
:state="state"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<UFormGroup label="Email" name="email">
|
||||
<UInput v-model="state.email" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Password" name="password">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormGroup>
|
||||
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### Zod
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:form-example-zod{class="space-y-4 w-60"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email'),
|
||||
password: z.string().min(8, 'Must be at least 8 characters')
|
||||
})
|
||||
|
||||
const state = ref({
|
||||
email: undefined,
|
||||
password: undefined
|
||||
})
|
||||
|
||||
const form = ref()
|
||||
|
||||
async function submit () {
|
||||
await form.value!.validate()
|
||||
// Do something with state.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:schema="schema"
|
||||
:state="state"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<UFormGroup label="Email" name="email">
|
||||
<UInput v-model="state.email" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Password" name="password">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormGroup>
|
||||
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### Joi
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:form-example-joi{class="space-y-4 w-60"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import Joi from 'joi'
|
||||
import type { Schema } from 'joi'
|
||||
|
||||
const schema = Joi.object({
|
||||
email: Joi.string().required(),
|
||||
password: Joi.string()
|
||||
.min(8)
|
||||
.required()
|
||||
})
|
||||
|
||||
const state = ref({
|
||||
email: undefined,
|
||||
password: undefined
|
||||
})
|
||||
|
||||
async function submit () {
|
||||
await form.value!.validate()
|
||||
// Do something with state.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:schema="schema"
|
||||
:state="state"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<UFormGroup label="Email" name="email">
|
||||
<UInput v-model="state.email" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Password" name="password">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormGroup>
|
||||
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Type Inference
|
||||
|
||||
You can utilize Zod and Yup's type inference feature to automatically infer the types of your schema and form data. This allows for strong type checking and better code validation, reducing the likelihood of errors.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { Form } from '@nuxthq/ui/dist/runtime/types'
|
||||
|
||||
// [...]
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email'),
|
||||
password: z.string().min(8, 'Must be at least 8 characters')
|
||||
})
|
||||
|
||||
const state: Partial<Schema> = ref({
|
||||
email: undefined,
|
||||
password: undefined
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
// For Yup, use:
|
||||
// type Schema = InferType<typeof schema>
|
||||
const form = ref<Form<Schema>>()
|
||||
|
||||
async function submit() {
|
||||
const data: Schema = await form.value!.validate()
|
||||
// Do something with data
|
||||
}
|
||||
</script>
|
||||
// [...]
|
||||
```
|
||||
|
||||
## Other libraries
|
||||
|
||||
For other validation libraries, you can define your own component with custom validation logic.
|
||||
|
||||
Here is an example with [Vuelidate](https://github.com/vuelidate/vuelidate):
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import useVuelidate from '@vuelidate/core';
|
||||
|
||||
const props = defineProps({
|
||||
rules: { type: Object, required: true },
|
||||
model: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const form = ref();
|
||||
const v = useVuelidate(props.rules, props.model);
|
||||
|
||||
async function validateWithVuelidate() {
|
||||
v.value.$touch();
|
||||
await v.value.$validate();
|
||||
return v.value.$errors.map((error) => ({
|
||||
message: error.$message,
|
||||
path: error.$propertyPath,
|
||||
}));
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
validate: async () => {
|
||||
await form.value.validate();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm ref="form" :model="model" :validate="validateWithVuelidate">
|
||||
<slot />
|
||||
</UForm>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Input events
|
||||
|
||||
The Form component automatically triggers validation upon input `blur` or `change` events. This ensures that any errors are displayed as soon as the user interacts with the form elements.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:form-example-elements{class="space-y-4 w-60"}
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
@@ -17,7 +17,7 @@ Like the Select component, you can use the `options` prop to pass an array of st
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:select-menu-example-basic{class="max-w-[12rem] w-full"}
|
||||
:select-menu-example-basic{class="w-full lg:w-48"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
@@ -39,7 +39,7 @@ You can use the `multiple` prop to select multiple values.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:select-menu-example-multiple{class="max-w-[12rem] w-full"}
|
||||
:select-menu-example-multiple{class="w-full lg:w-48"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
@@ -61,7 +61,7 @@ You can pass an array of objects to `options` and either compare on the whole ob
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:select-menu-example-objects{class="max-w-[12rem] w-full"}
|
||||
:select-menu-example-objects{class="w-full lg:w-48"}
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
@@ -108,6 +108,50 @@ const selected = ref(people[0])
|
||||
```
|
||||
::
|
||||
|
||||
If you only want to select a single object property rather than the whole object as value, you can set the `value-attribute` property. This prop defaults to `null`. :u-badge{label="Edge" class="!rounded-full" variant="subtle"}
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:select-menu-example-objects-value-attribute{class="w-full lg:w-48"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const people = [{
|
||||
id: 1,
|
||||
name: 'Wade Cooper'
|
||||
}, {
|
||||
id: 2,
|
||||
name: 'Arlene Mccoy'
|
||||
}, {
|
||||
id: 3,
|
||||
name: 'Devon Webb'
|
||||
}, {
|
||||
id: 4,
|
||||
name: 'Tom Cook'
|
||||
}]
|
||||
|
||||
const selected = ref(people[0].id)
|
||||
|
||||
const current = computed(() => people.find(person => person.id === selected.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
v-model="selected"
|
||||
:options="people"
|
||||
placeholder="Select people"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
>
|
||||
<template #label>
|
||||
{{ current.name }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### Icon
|
||||
|
||||
Use the `selected-icon` prop to set a different icon or change it globally in `ui.selectMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
|
||||
@@ -115,7 +159,7 @@ Use the `selected-icon` prop to set a different icon or change it globally in `u
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'max-w-[12rem] w-full'
|
||||
class: 'w-full lg:w-48'
|
||||
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:
|
||||
@@ -140,7 +184,7 @@ This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) compon
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'max-w-[12rem] w-full'
|
||||
class: 'w-full lg:w-48'
|
||||
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:
|
||||
@@ -149,7 +193,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Async search
|
||||
### Async search :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
|
||||
|
||||
Pass a function to the `searchable` prop to customize the search behavior and filter options according to your needs. The function will receive the query as its first argument and should return an array.
|
||||
|
||||
@@ -157,7 +201,7 @@ Use the `debounce` prop to adjust the delay of the function.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:select-menu-example-async-search{class="max-w-[12rem] w-full"}
|
||||
:select-menu-example-async-search{class="w-full lg:w-48"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
@@ -191,7 +235,7 @@ You can override the `#label` slot and handle the display yourself.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:select-menu-example-multiple-slot{class="max-w-[12rem] w-full"}
|
||||
:select-menu-example-multiple-slot{class="w-full lg:w-48"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
@@ -218,7 +262,7 @@ You can also override the `#default` slot entirely.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:select-menu-example-button{class="max-w-[12rem] w-full"}
|
||||
:select-menu-example-button{class="w-full lg:w-48"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
|
||||
@@ -39,7 +39,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Style :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
|
||||
### Style
|
||||
|
||||
Use the `color` prop to change the style of the Checkbox.
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Style :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
|
||||
### Style
|
||||
|
||||
Use the `color` prop to change the style of the Radio.
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const selected = ref(false)
|
||||
```
|
||||
::
|
||||
|
||||
### Style :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
|
||||
### Style
|
||||
|
||||
Use the `color` prop to change the style of the Toggle.
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/forms/Range.vue
|
||||
navigation:
|
||||
badge: New
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -134,6 +134,32 @@ You can also use the `error` prop as a boolean to mark the form element as inval
|
||||
The `error` prop will automatically set the `color` prop of the form element to `red`.
|
||||
::
|
||||
|
||||
### Size :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `size` prop to change the size of the label and the form element.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
size: 'xl'
|
||||
label: 'Email'
|
||||
hint: 'Optional'
|
||||
description: "We'll only use this for spam."
|
||||
help: 'We will never share your email with anyone else.'
|
||||
excludedProps:
|
||||
- label
|
||||
- hint
|
||||
- description
|
||||
- help
|
||||
code: >-
|
||||
|
||||
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
|
||||
---
|
||||
|
||||
#default
|
||||
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
@@ -75,7 +75,7 @@ Use the `columns` prop to configure which columns to display. It's an array of o
|
||||
- `key` - The field to display from the row data.
|
||||
- `sortable` - Whether the column is sortable. Defaults to `false`.
|
||||
- `direction` - The sort direction to use on first click. Defaults to `asc`.
|
||||
- `class` - The class to apply to the column cells.
|
||||
- `class` :u-badge{label="New" class="!rounded-full" variant="subtle"} - The class to apply to the column cells.
|
||||
|
||||
::component-example{class="grid"}
|
||||
---
|
||||
@@ -327,7 +327,7 @@ const selected = ref([people[1]])
|
||||
You can use the `by` prop to compare objects by a field instead of comparing object instances. We've replicated the behavior of Headless UI [Combobox](https://headlessui.com/vue/combobox#binding-objects-as-values).
|
||||
::
|
||||
|
||||
You can alsso add a `select` listener on your Table to make the rows clickable. The function will receive the row as the first argument.
|
||||
You can also add a `select` listener on your Table to make the rows clickable. The function will receive the row as the first argument. :u-badge{label="New" class="!rounded-full" variant="subtle"}
|
||||
|
||||
You can use this to navigate to a page, open a modal or even to select the row manually.
|
||||
|
||||
|
||||
@@ -8,6 +8,17 @@ links:
|
||||
|
||||
## Usage
|
||||
|
||||
Pass an array to the `links` prop of the VerticalNavigation component. Each link can have the following properties:
|
||||
|
||||
- `label` - The label of the link.
|
||||
- `icon` - The icon of the link.
|
||||
- `iconClass` - The class of the icon of the link.
|
||||
- `avatar` - The avatar of the link. You can pass all the props of the [Avatar](/elements/avatar) component.
|
||||
- `badge` - A badge to display next to the label.
|
||||
- `click` - The click handler of the link.
|
||||
|
||||
You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:vertical-navigation-example
|
||||
@@ -42,6 +53,198 @@ const links = [{
|
||||
```
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
Learn how to build a Tailwind like vertical navigation in the [Examples](/getting-started/examples#verticalnavigation) page.
|
||||
::
|
||||
|
||||
## Slots
|
||||
|
||||
You can use slots to customize links display.
|
||||
|
||||
### `default`
|
||||
|
||||
Use the `#default` slot to customize the link label. You will have access to the `link` and `isActive` properties in the slot scope.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:vertical-navigation-example-default-slot
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const links = [{
|
||||
label: 'Navigation',
|
||||
children: [{
|
||||
label: 'Vertical Navigation',
|
||||
to: '/navigation/vertical-navigation'
|
||||
}, {
|
||||
label: 'Command Palette',
|
||||
to: '/navigation/command-palette'
|
||||
}]
|
||||
}, {
|
||||
label: 'Data',
|
||||
children: [{
|
||||
label: 'Table',
|
||||
to: '/data/table'
|
||||
}]
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation :links="links">
|
||||
<template #default="{ link }">
|
||||
<div class="relative text-left w-full">
|
||||
<div class="mb-2">
|
||||
{{ link.label }}
|
||||
</div>
|
||||
<UVerticalNavigation v-if="link.children" :links="link.children" />
|
||||
</div>
|
||||
</template>
|
||||
</UVerticalNavigation>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### `avatar`
|
||||
|
||||
Use the `#avatar` slot to customize the link avatar. You will have access to the `link` and `isActive` properties in the slot scope.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:vertical-navigation-example-avatar-slot
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const links = [{
|
||||
avatar: {
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
},
|
||||
label: 'Benjamin Canac'
|
||||
}, {
|
||||
label: 'KeJun'
|
||||
}]
|
||||
|
||||
const { ui } = useAppConfig()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation :links="links">
|
||||
<template #avatar="{ link }">
|
||||
<UAvatar
|
||||
v-if="link.avatar"
|
||||
v-bind="{ size: ui.verticalNavigation.avatar.size, ...link.avatar }"
|
||||
:class="[ui.verticalNavigation.avatar.base]"
|
||||
/>
|
||||
<UIcon v-else name="i-heroicons-user-circle-20-solid" class="text-lg" />
|
||||
</template>
|
||||
</UVerticalNavigation>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### `icon`
|
||||
|
||||
Use the `#icon` slot to customize the link icon. You will have access to the `link` and `isActive` properties in the slot scope.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:vertical-navigation-example-icon-slot
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const types = {
|
||||
bug: {
|
||||
icon: 'i-heroicons-bug-ant-20-solid',
|
||||
color: 'text-red-500'
|
||||
},
|
||||
docs: {
|
||||
icon: 'i-heroicons-document-text-20-solid',
|
||||
color: 'text-blue-500'
|
||||
},
|
||||
lock: {
|
||||
icon: 'i-heroicons-lock-closed-20-solid',
|
||||
color: 'text-gray dark:text-white'
|
||||
},
|
||||
default: {
|
||||
icon: 'i-heroicons-question-mark-circle-20-solid',
|
||||
color: 'text-green-500'
|
||||
}
|
||||
}
|
||||
const links = [{
|
||||
label: 'UDropdown and UPopover dropdown menu, dropdown will be obscured',
|
||||
type: 'bug'
|
||||
}, {
|
||||
label: 'Uncaught (in promise) ReferenceError: ref is not defined',
|
||||
type: 'lock'
|
||||
}, {
|
||||
label: 'Fully styled and customizable components for Nuxt.',
|
||||
type: 'docs'
|
||||
}, {
|
||||
label: 'Can I pass a tailwind color to UNotifications with `toast.add()` ?'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation :links="links">
|
||||
<template #icon="{ link }">
|
||||
<UIcon v-if="link.type" :name="types[link.type].icon" :class="types[link.type].color" class="text-base" />
|
||||
<UIcon v-else :name="types.default.icon" :class="types.default.color" class="text-base" />
|
||||
</template>
|
||||
</UVerticalNavigation>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### `badge`
|
||||
|
||||
Use the `#badge` slot to customize the link badge. You will have access to the `link` and `isActive` properties in the slot scope.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:vertical-navigation-example-badge-slot
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const links = [{
|
||||
label: '.github',
|
||||
icon: 'i-heroicons-folder-20-solid',
|
||||
badge: 'chore(github): use pnpm 8',
|
||||
time: 'last month'
|
||||
}, {
|
||||
label: '.editorconfig',
|
||||
icon: 'i-heroicons-document-solid',
|
||||
badge: 'Initial commit',
|
||||
time: '2 years ago'
|
||||
}, {
|
||||
label: '.package.json',
|
||||
icon: 'i-heroicons-document-solid',
|
||||
badge: 'chore(deps): bump',
|
||||
time: '16 hours ago'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation
|
||||
:links="links"
|
||||
class="w-full"
|
||||
:ui="{
|
||||
label: 'truncate relative text-gray-900 dark:text-white flex-initial w-32 text-left'
|
||||
}"
|
||||
>
|
||||
<template #badge="{ link }">
|
||||
<div class="flex-1 flex justify-between relative truncate">
|
||||
<div>{{ link.badge }}</div>
|
||||
<div>{{ link.time }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</UVerticalNavigation>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
296
docs/content/5.navigation/4.tabs.md
Normal file
296
docs/content/5.navigation/4.tabs.md
Normal file
@@ -0,0 +1,296 @@
|
||||
---
|
||||
description: A set of tab panels that are displayed one at a time.
|
||||
links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/navigation/Tabs.vue
|
||||
navigation:
|
||||
badge: Edge
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Pass an array to the `items` prop of the Tabs component. Each item can have the following properties:
|
||||
|
||||
- `label` - The label of the item.
|
||||
- `slot` - A key to customize the item with a slot.
|
||||
- `content` - The content to display in the panel by default.
|
||||
- `disabled` - Determines whether the item is disabled or not.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:tabs-example-basic{class="w-full"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const items = [{
|
||||
label: 'Tab1',
|
||||
content: 'This is the content shown for Tab1'
|
||||
}, {
|
||||
label: 'Tab2',
|
||||
disabled: true,
|
||||
content: 'And, this is the content for Tab2'
|
||||
}, {
|
||||
label: 'Tab3',
|
||||
content: 'Finally, this is the content for Tab3'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### Vertical
|
||||
|
||||
You can change the orientation of the tabs by setting the `orientation` prop to `vertical`.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:tabs-example-vertical{class="w-full"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const items = [...]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" orientation="vertical" :ui="{ wrapper: 'flex items-center gap-4', list: { width: 'w-48' } }" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### Default index
|
||||
|
||||
You can set the default index of the tabs by setting the `default-index` prop.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:tabs-example-index{class="w-full"}
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const items = [...]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" :default-index="2" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Slots
|
||||
|
||||
You can use slots to customize the buttons and items content of the Accordion.
|
||||
|
||||
### `default`
|
||||
|
||||
Use the `#default` slot to customize the content of the trigger buttons. You will have access to the `item`, `index`, `selected` and `disabled` in the slot scope.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:tabs-example-default-slot
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const items = [{
|
||||
label: 'Introduction',
|
||||
icon: 'i-heroicons-information-circle',
|
||||
content: 'This is the content shown for Tab1'
|
||||
}, {
|
||||
label: 'Installation',
|
||||
icon: 'i-heroicons-arrow-down-tray',
|
||||
content: 'And, this is the content for Tab2'
|
||||
}, {
|
||||
label: 'Theming',
|
||||
icon: 'i-heroicons-eye-dropper',
|
||||
content: 'Finally, this is the content for Tab3'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" class="w-full">
|
||||
<template #default="{ item, index, selected }">
|
||||
<div class="flex items-center gap-2 relative truncate">
|
||||
<UIcon :name="item.icon" class="w-4 h-4 flex-shrink-0" />
|
||||
|
||||
<span class="truncate">{{ index + 1 }}. {{ item.label }}</span>
|
||||
|
||||
<span v-if="selected" class="absolute -right-4 w-2 h-2 rounded-full bg-primary-500 dark:bg-primary-400" />
|
||||
</div>
|
||||
</template>
|
||||
</UTabs>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
### `item`
|
||||
|
||||
Use the `#item` slot to customize the items content. You will have access to the `item`, `index` and `selected` properties in the slot scope.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:tabs-example-item-slot
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const items = [{
|
||||
key: 'account',
|
||||
label: 'Account',
|
||||
description: 'Make changes to your account here. Click save when you\'re done.'
|
||||
}, {
|
||||
key: 'password',
|
||||
label: 'Password',
|
||||
description: 'Change your password here. After saving, you\'ll be logged out.'
|
||||
}]
|
||||
|
||||
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
|
||||
const passwordForm = reactive({ currentPassword: '', newPassword: '' })
|
||||
|
||||
function onSubmit (form) {
|
||||
console.log('Submitted form:', form)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" class="w-full">
|
||||
<template #item="{ item }">
|
||||
<UCard @submit.prevent="() => onSubmit(item.key === 'account' ? accountForm : passwordForm)">
|
||||
<template #header>
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{{ item.label }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div v-if="item.key === 'account'" class="space-y-3">
|
||||
<UFormGroup label="Name" name="name">
|
||||
<UInput v-model="accountForm.name" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Username" name="username">
|
||||
<UInput v-model="accountForm.username" />
|
||||
</UFormGroup>
|
||||
</div>
|
||||
<div v-else-if="item.key === 'password'" class="space-y-3">
|
||||
<UFormGroup label="Current Password" name="current" required>
|
||||
<UInput v-model="passwordForm.currentPassword" type="password" required />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="New Password" name="new" required>
|
||||
<UInput v-model="passwordForm.newPassword" type="password" required />
|
||||
</UFormGroup>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<UButton type="submit" color="black">
|
||||
Save {{ item.key === 'account' ? 'account' : 'password' }}
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UTabs>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
You can also pass a `slot` property to customize a specific item.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:tabs-example-item-custom-slot
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const items = [{
|
||||
slot: 'account',
|
||||
label: 'Account'
|
||||
}, {
|
||||
slot: 'password',
|
||||
label: 'Password'
|
||||
}]
|
||||
|
||||
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
|
||||
const passwordForm = reactive({ currentPassword: '', newPassword: '' })
|
||||
|
||||
function onSubmitAccount () {
|
||||
console.log('Submitted form:', accountForm)
|
||||
}
|
||||
|
||||
function onSubmitPassword () {
|
||||
console.log('Submitted form:', passwordForm)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items" class="w-full">
|
||||
<template #account="{ item }">
|
||||
<UCard @submit.prevent="onSubmitAccount">
|
||||
<template #header>
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{{ item.label }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Make changes to your account here. Click save when you're done.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<UFormGroup label="Name" name="name" class="mb-3">
|
||||
<UInput v-model="accountForm.name" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Username" name="username">
|
||||
<UInput v-model="accountForm.username" />
|
||||
</UFormGroup>
|
||||
|
||||
<template #footer>
|
||||
<UButton type="submit" color="black">
|
||||
Save account
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<template #password="{ item }">
|
||||
<UCard @submit.prevent="onSubmitPassword">
|
||||
<template #header>
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{{ item.label }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Change your password here. After saving, you'll be logged out.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<UFormGroup label="Current Password" name="current" required class="mb-3">
|
||||
<UInput v-model="passwordForm.currentPassword" type="password" required />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="New Password" name="new" required>
|
||||
<UInput v-model="passwordForm.newPassword" type="password" required />
|
||||
</UFormGroup>
|
||||
|
||||
<template #footer>
|
||||
<UButton type="submit" color="black">
|
||||
Save password
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UTabs>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Preset
|
||||
|
||||
:component-preset
|
||||
@@ -125,7 +125,7 @@ const isOpen = ref(false)
|
||||
```
|
||||
::
|
||||
|
||||
### Prevent close
|
||||
### Prevent close :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `prevent-close` prop to disable the outside click alongside the `esc` keyboard shortcut.
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ const isOpen = ref(false)
|
||||
```
|
||||
::
|
||||
|
||||
### Prevent close
|
||||
### Prevent close :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `prevent-close` prop to disable the outside click alongside the `esc` keyboard shortcut.
|
||||
|
||||
|
||||
@@ -28,6 +28,27 @@ links:
|
||||
```
|
||||
::
|
||||
|
||||
### Mode
|
||||
|
||||
Use the `mode` prop to switch between `click` and `hover` modes.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:popover-example-mode
|
||||
#code
|
||||
```vue
|
||||
<template>
|
||||
<UPopover mode="hover">
|
||||
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
|
||||
<template #panel>
|
||||
<!-- Content -->
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
@@ -283,9 +283,7 @@ baseProps:
|
||||
timeout: 0
|
||||
props:
|
||||
actions:
|
||||
- variant: 'ghost'
|
||||
color: 'gray'
|
||||
label: Action 1
|
||||
- label: Action 1
|
||||
- variant: 'solid'
|
||||
color: 'gray'
|
||||
label: Action 2
|
||||
@@ -316,6 +314,50 @@ excludedProps:
|
||||
---
|
||||
::
|
||||
|
||||
## Slots
|
||||
|
||||
### `title` / `description` :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `#title` and `#description` slots to customize the Notification.
|
||||
|
||||
This can be handy when you want to display HTML content. To achieve this, you can define those slots in the top-level `<UNotifications />` component in your `app.vue` and use the `v-html` directive.
|
||||
|
||||
```vue [app.vue]
|
||||
<template>
|
||||
<UNotifications>
|
||||
<template #title="{ title }">
|
||||
<span v-html="title" />
|
||||
</template>
|
||||
|
||||
<template #description="{ description }">
|
||||
<span v-html="description" />
|
||||
</template>
|
||||
</UNotifications>
|
||||
</template>
|
||||
```
|
||||
|
||||
This way, you can use HTML tags in the `title` and `description` props when using `useToast`.
|
||||
|
||||
::component-example
|
||||
#default
|
||||
:notification-example-html
|
||||
|
||||
#code
|
||||
```vue
|
||||
<script setup>
|
||||
const toast = useToast()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Show toast" @click="toast.add({ title: 'Notification <i>italic</i>', description: 'This is an <u>underlined</u> and <b>bold</b> notification.' })" />
|
||||
</template>
|
||||
```
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
Slots defined in the `<UNotifications />` component are automatically passed down to the `Notification` children.
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
@@ -18,7 +18,7 @@ Use to show a placeholder while content is loading.
|
||||
```vue
|
||||
<template>
|
||||
<div class="flex items-center space-x-4">
|
||||
<USkeleton class="h-12 w-12 rounded-full" />
|
||||
<USkeleton class="h-12 w-12" :ui="{ rounded: 'rounded-full' }" />
|
||||
<div class="space-y-2">
|
||||
<USkeleton class="h-4 w-[250px]" />
|
||||
<USkeleton class="h-4 w-[200px]" />
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { createResolver } from '@nuxt/kit'
|
||||
import colors from 'tailwindcss/colors'
|
||||
import module from '../src/module'
|
||||
import { excludeColors } from '../src/colors'
|
||||
import pkg from '../package.json'
|
||||
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
|
||||
export default defineNuxtConfig({
|
||||
extends: '@nuxt-themes/ui-kit',
|
||||
modules: [
|
||||
'@nuxt/content',
|
||||
'@nuxt/devtools',
|
||||
'@nuxthq/studio',
|
||||
// '@nuxthq/studio',
|
||||
module,
|
||||
'@nuxtjs/fontaine',
|
||||
'@nuxtjs/google-fonts',
|
||||
'@nuxtjs/plausible',
|
||||
'@vueuse/nuxt',
|
||||
@@ -40,9 +44,11 @@ export default defineNuxtConfig({
|
||||
}
|
||||
},
|
||||
experimental: {
|
||||
payloadExtraction: true
|
||||
payloadExtraction: false
|
||||
},
|
||||
componentMeta: {
|
||||
globalsOnly: true,
|
||||
exclude: [resolve('./components'), resolve('@nuxt-themes/ui-kit/components')],
|
||||
metaFields: {
|
||||
props: true,
|
||||
slots: false,
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
{
|
||||
"name": "@nuxthq/ui-docs",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@nuxthq/ui": "workspace:latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/heroicons": "latest",
|
||||
"@iconify-json/simple-icons": "latest",
|
||||
"@nuxt-themes/ui-kit": "npm:@nuxt-themes/ui-kit-edge@0.0.1-28161530.eabde2d",
|
||||
"@nuxt/content": "^2.7.0",
|
||||
"@nuxt-themes/ui-kit": "npm:@nuxt-themes/ui-kit-edge@0.0.1-28178970.ce8b67a",
|
||||
"@nuxt/content": "^2.7.2",
|
||||
"@nuxt/devtools": "^0.6.7",
|
||||
"@nuxt/eslint-config": "^0.1.1",
|
||||
"@nuxthq/studio": "^0.13.4",
|
||||
"@nuxtjs/google-fonts": "^3.0.1",
|
||||
"@nuxtjs/fontaine": "^0.4.1",
|
||||
"@nuxtjs/google-fonts": "^3.0.2",
|
||||
"@nuxtjs/plausible": "^0.2.1",
|
||||
"@vueuse/nuxt": "^10.2.1",
|
||||
"eslint": "^8.45.0",
|
||||
"nuxt": "^3.6.3",
|
||||
"nuxt": "^3.6.5",
|
||||
"nuxt-component-meta": "^0.5.3",
|
||||
"nuxt-lodash": "^2.5.0",
|
||||
"typescript": "^5.1.6",
|
||||
"v-calendar": "^3.0.3"
|
||||
"v-calendar": "^3.0.3",
|
||||
"yup": "^1.2.0",
|
||||
"joi": "^17.9.2",
|
||||
"zod": "^3.21.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
<template>
|
||||
<UDocsPage v-if="page" :page="page" :surround="(surround as ParsedContent[])">
|
||||
<template #footer>
|
||||
<UPage v-if="page">
|
||||
<UPageHeader v-bind="page" :headline="headline" />
|
||||
|
||||
<UPageBody prose>
|
||||
<ContentRenderer v-if="page && page.body" :value="page" />
|
||||
|
||||
<UButton
|
||||
:to="githubLink"
|
||||
variant="link"
|
||||
icon="i-heroicons-pencil-square"
|
||||
label="Edit this page on GitHub"
|
||||
:padded="false"
|
||||
class="mt-12"
|
||||
/>
|
||||
|
||||
<hr v-if="surround?.length" class="border-gray-200 dark:border-gray-800 my-8">
|
||||
|
||||
<UDocsSurround :surround="surround" />
|
||||
|
||||
<Footer />
|
||||
</UPageBody>
|
||||
|
||||
<template v-if="page.body?.toc?.links?.length" #right>
|
||||
<UDocsToc :links="page.body.toc.links" />
|
||||
</template>
|
||||
</UDocsPage>
|
||||
<div v-else class="pt-8">
|
||||
Page not found
|
||||
</div>
|
||||
</UPage>
|
||||
<UPageError v-else />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { data: page } = await useAsyncData(`docs-${route.path}`, () => queryContent(route.path).findOne())
|
||||
const { data: surround } = await useAsyncData(`docs-${route.path}-surround`, () => queryContent()
|
||||
.only(['_path', 'title', 'navigation', 'description'])
|
||||
.where({ _extension: 'md', navigation: { $ne: false } })
|
||||
.findSurround(route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
|
||||
)
|
||||
@@ -27,4 +43,7 @@ if (process.server && !page.value) {
|
||||
}
|
||||
|
||||
useContentHead(page)
|
||||
|
||||
const githubLink = computed(() => `https://github.com/nuxtlabs/ui/edit/dev/docs/content/${page?.value?._file}`)
|
||||
const headline = computed(() => page.value._dir?.title ? page.value._dir.title : useLowerCase(page.value._dir))
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,21 @@ export default <Partial<Config>>{
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', ...defaultTheme.fontFamily.sans]
|
||||
sans: ['Inter', 'Inter fallback', ...defaultTheme.fontFamily.sans]
|
||||
},
|
||||
colors: {
|
||||
green: {
|
||||
50: '#d6ffee',
|
||||
100: '#acffdd',
|
||||
200: '#83ffcc',
|
||||
300: '#30ffaa',
|
||||
400: '#00dc82',
|
||||
500: '#00bd6f',
|
||||
600: '#009d5d',
|
||||
700: '#007e4a',
|
||||
800: '#005e38',
|
||||
900: '#003f25'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nuxthq/ui",
|
||||
"version": "2.6.0",
|
||||
"version": "2.7.0",
|
||||
"repository": "https://github.com/nuxtlabs/ui",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
@@ -21,6 +21,7 @@
|
||||
"build": "nuxt-module-build",
|
||||
"prepack": "pnpm build",
|
||||
"dev": "nuxi dev docs",
|
||||
"play": "nuxi dev playground",
|
||||
"build:docs": "nuxi generate docs",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
@@ -30,9 +31,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@egoist/tailwindcss-icons": "^1.1.0",
|
||||
"@headlessui/vue": "^1.7.14",
|
||||
"@headlessui/vue": "^1.7.15",
|
||||
"@iconify-json/heroicons": "^1.1.11",
|
||||
"@nuxt/kit": "^3.6.3",
|
||||
"@nuxt/kit": "^3.6.5",
|
||||
"@nuxtjs/color-mode": "^3.3.0",
|
||||
"@nuxtjs/tailwindcss": "^6.8.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
@@ -53,10 +54,13 @@
|
||||
"@nuxt/module-builder": "^0.4.0",
|
||||
"@release-it/conventional-changelog": "^7.0.0",
|
||||
"eslint": "^8.45.0",
|
||||
"nuxt": "^3.6.3",
|
||||
"release-it": "^16.1.2",
|
||||
"unbuild": "^1.2.1",
|
||||
"nuxt": "^3.6.5",
|
||||
"release-it": "^16.1.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vue-tsc": "^1.8.5"
|
||||
"unbuild": "^1.2.1",
|
||||
"vue-tsc": "^1.8.8",
|
||||
"yup": "^1.2.0",
|
||||
"joi": "^17.9.2",
|
||||
"zod": "^3.21.4"
|
||||
}
|
||||
}
|
||||
|
||||
23
playground/app.vue
Normal file
23
playground/app.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<UContainer class="min-h-screen flex items-center">
|
||||
<UCard class="flex-1" :ui="{ background: 'bg-gray-50 dark:bg-gray-800/50', ring: 'ring-1 ring-gray-300 dark:ring-gray-700', divide: 'divide-y divide-gray-300 dark:divide-gray-700', header: { base: 'font-bold' } }">
|
||||
<template #header>
|
||||
Welcome to the playground!
|
||||
</template>
|
||||
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
Try your components here!
|
||||
</p>
|
||||
</UCard>
|
||||
</UContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
@apply antialiased font-sans text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-900;
|
||||
}
|
||||
</style>
|
||||
7
playground/nuxt.config.ts
Normal file
7
playground/nuxt.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import module from '../src/module'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
module
|
||||
]
|
||||
})
|
||||
3
playground/tsconfig.json
Normal file
3
playground/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
1171
pnpm-lock.yaml
generated
1171
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
import { omit, kebabCase, camelCase, upperFirst } from 'lodash-es'
|
||||
|
||||
const colorsToExclude = [
|
||||
'inherit',
|
||||
'transparent',
|
||||
@@ -12,20 +14,25 @@ const colorsToExclude = [
|
||||
'cool'
|
||||
]
|
||||
|
||||
const omit = (obj: object, keys: string[]) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).filter(([key]) => !keys.includes(key))
|
||||
)
|
||||
}
|
||||
|
||||
const kebabCase = (str: string) => {
|
||||
return str
|
||||
?.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
|
||||
?.map(x => x.toLowerCase())
|
||||
?.join('-')
|
||||
}
|
||||
|
||||
const safelistByComponent = {
|
||||
alert: (colorsAsRegex) => [{
|
||||
pattern: new RegExp(`bg-(${colorsAsRegex})-50`)
|
||||
}, {
|
||||
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
|
||||
variants: ['dark']
|
||||
}, {
|
||||
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
|
||||
}, {
|
||||
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
|
||||
variants: ['dark']
|
||||
}, {
|
||||
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
|
||||
}, {
|
||||
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
|
||||
variants: ['dark']
|
||||
}, {
|
||||
pattern: new RegExp(`ring-(${colorsAsRegex})-500`)
|
||||
}],
|
||||
avatar: (colorsAsRegex) => [{
|
||||
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
|
||||
variants: ['dark']
|
||||
@@ -37,6 +44,8 @@ const safelistByComponent = {
|
||||
}, {
|
||||
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
|
||||
variants: ['dark']
|
||||
}, {
|
||||
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
|
||||
}, {
|
||||
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
|
||||
variants: ['dark']
|
||||
@@ -182,13 +191,23 @@ const safelistComponentAliasesMap = {
|
||||
|
||||
const colorsAsRegex = (colors: string[]): string => colors.join('|')
|
||||
|
||||
export const excludeColors = (colors: object) => Object.keys(omit(colors, colorsToExclude)).map(color => kebabCase(color)) as string[]
|
||||
export const excludeColors = (colors: object): string[] => {
|
||||
return Object.entries(omit(colors, colorsToExclude))
|
||||
.filter(([, value]) => typeof value === 'object')
|
||||
.map(([key]) => kebabCase(key))
|
||||
}
|
||||
|
||||
export const generateSafelist = (colors: string[]) => {
|
||||
const safelist = Object.keys(safelistByComponent).flatMap(component => safelistByComponent[component](colorsAsRegex(colors)))
|
||||
export const generateSafelist = (colors: string[], globalColors) => {
|
||||
const baseSafelist = Object.keys(safelistByComponent).flatMap(component => safelistByComponent[component](colorsAsRegex(colors)))
|
||||
|
||||
// Ensure `red` color is safelisted for form elements so that `error` prop of `UFormGroup` always works
|
||||
const formsSafelist = ['input', 'radio', 'checkbox', 'toggle', 'range'].flatMap(component => safelistByComponent[component](colorsAsRegex(['red'])))
|
||||
|
||||
return [
|
||||
...safelist,
|
||||
...baseSafelist,
|
||||
...formsSafelist,
|
||||
// Ensure all global colors are safelisted for the Notification (toast.add)
|
||||
...safelistByComponent['notification'](colorsAsRegex(globalColors)),
|
||||
// Gray safelist for Avatar & Notification
|
||||
'bg-gray-500',
|
||||
'dark:bg-gray-400',
|
||||
@@ -199,7 +218,7 @@ export const generateSafelist = (colors: string[]) => {
|
||||
|
||||
export const customSafelistExtractor = (prefix, content: string, colors: string[], safelistColors: string[]) => {
|
||||
const classes = []
|
||||
const regex = /<(\w+)\s+(?![^>]*:color\b)[^>]*\bcolor=["']([^"']+)["'][^>]*>/gs
|
||||
const regex = /<([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z][A-Za-z0-9]*)*)\s+(?![^>]*:color\b)[^>]*\bcolor=["']([^"']+)["'][^>]*>/gs
|
||||
|
||||
const matches = content.matchAll(regex)
|
||||
|
||||
@@ -208,11 +227,13 @@ export const customSafelistExtractor = (prefix, content: string, colors: string[
|
||||
for (const match of matches) {
|
||||
const [, component, color] = match
|
||||
|
||||
const camelComponent = upperFirst(camelCase(component))
|
||||
|
||||
if (!colors.includes(color) || safelistColors.includes(color)) {
|
||||
continue
|
||||
}
|
||||
|
||||
let name = safelistComponentAliasesMap[component] ? safelistComponentAliasesMap[component] : component
|
||||
let name = safelistComponentAliasesMap[camelComponent] ? safelistComponentAliasesMap[camelComponent] : camelComponent
|
||||
|
||||
if (!components.includes(name)) {
|
||||
continue
|
||||
|
||||
@@ -16,7 +16,7 @@ delete defaultColors.trueGray
|
||||
delete defaultColors.coolGray
|
||||
delete defaultColors.blueGray
|
||||
|
||||
declare module 'nuxt/schema' {
|
||||
declare module '@nuxt/schema' {
|
||||
interface AppConfigInput {
|
||||
ui?: {
|
||||
primary?: string
|
||||
@@ -123,7 +123,7 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
}
|
||||
|
||||
tailwindConfig.safelist = tailwindConfig.safelist || []
|
||||
tailwindConfig.safelist.push(...generateSafelist(options.safelistColors))
|
||||
tailwindConfig.safelist.push(...generateSafelist(options.safelistColors, colors))
|
||||
|
||||
tailwindConfig.plugins = tailwindConfig.plugins || []
|
||||
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
|
||||
|
||||
@@ -119,9 +119,22 @@ const badge = {
|
||||
md: 'text-sm px-2 py-1',
|
||||
lg: 'text-sm px-2.5 py-1.5'
|
||||
},
|
||||
color: {},
|
||||
color: {
|
||||
white: {
|
||||
solid: 'ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-900 dark:text-white bg-white dark:bg-gray-900'
|
||||
},
|
||||
gray: {
|
||||
solid: 'ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-700 dark:text-gray-200 bg-gray-50 dark:bg-gray-800'
|
||||
},
|
||||
black: {
|
||||
solid: 'text-white dark:text-gray-900 bg-gray-900 dark:bg-white'
|
||||
}
|
||||
},
|
||||
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'
|
||||
solid: 'bg-{color}-500 dark:bg-{color}-400 text-white dark:text-gray-900',
|
||||
outline: 'text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400',
|
||||
soft: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400',
|
||||
subtle: '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-25 dark:ring-opacity-25'
|
||||
},
|
||||
default: {
|
||||
size: 'sm',
|
||||
@@ -280,6 +293,44 @@ const accordion = {
|
||||
}
|
||||
}
|
||||
|
||||
const alert = {
|
||||
wrapper: 'w-full relative overflow-hidden',
|
||||
title: 'text-sm font-medium',
|
||||
description: 'mt-1 text-sm leading-4 opacity-90',
|
||||
shadow: '',
|
||||
rounded: 'rounded-lg',
|
||||
padding: 'p-3',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 w-5 h-5'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0 self-center',
|
||||
size: 'md'
|
||||
},
|
||||
color: {
|
||||
white: {
|
||||
solid: 'text-gray-900 dark:text-white bg-white dark:bg-gray-900 ring-1 ring-gray-200 dark:ring-gray-800'
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
solid: 'bg-{color}-500 dark:bg-{color}-400 text-white dark:text-gray-900',
|
||||
outline: 'text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400',
|
||||
soft: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400',
|
||||
subtle: '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-25 dark:ring-opacity-25'
|
||||
},
|
||||
default: {
|
||||
color: 'white',
|
||||
variant: 'solid',
|
||||
icon: null,
|
||||
closeButton: null,
|
||||
actionButton: {
|
||||
size: 'xs',
|
||||
color: 'primary',
|
||||
variant: 'link'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const kbd = {
|
||||
base: 'inline-flex items-center justify-center text-gray-900 dark:text-white',
|
||||
padding: 'px-1',
|
||||
@@ -407,15 +458,24 @@ const input = {
|
||||
const formGroup = {
|
||||
wrapper: '',
|
||||
label: {
|
||||
wrapper: 'flex content-center justify-between',
|
||||
base: 'block text-sm font-medium text-gray-700 dark:text-gray-200',
|
||||
required: 'after:content-[\'*\'] after:ms-0.5 after:text-red-500 dark:after:text-red-400'
|
||||
wrapper: 'flex content-center items-center justify-between',
|
||||
base: 'block font-medium text-gray-700 dark:text-gray-200',
|
||||
// eslint-disable-next-line quotes
|
||||
required: `after:content-['*'] after:ms-0.5 after:text-red-500 dark:after:text-red-400`
|
||||
},
|
||||
size: {
|
||||
'2xs': 'text-xs',
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm',
|
||||
md: 'text-sm',
|
||||
lg: 'text-sm',
|
||||
xl: 'text-base'
|
||||
},
|
||||
description: 'text-sm text-gray-500 dark:text-gray-400',
|
||||
container: 'mt-1 relative',
|
||||
hint: 'text-sm text-gray-500 dark:text-gray-400',
|
||||
help: 'mt-2 text-sm text-gray-500 dark:text-gray-400',
|
||||
error: 'mt-2 text-sm text-red-500 dark:text-red-400'
|
||||
description: 'text-gray-500 dark:text-gray-400',
|
||||
hint: 'text-gray-500 dark:text-gray-400',
|
||||
help: 'mt-2 text-gray-500 dark:text-gray-400',
|
||||
error: 'mt-2 text-red-500 dark:text-red-400'
|
||||
}
|
||||
|
||||
const textarea = {
|
||||
@@ -552,15 +612,20 @@ const toggle = {
|
||||
}
|
||||
|
||||
const range = {
|
||||
wrapper: 'relative w-full',
|
||||
base: 'w-full absolute appearance-none cursor-pointer disabled:cursor-not-allowed disabled:bg-opacity-50 focus:outline-none [&::-webkit-slider-runnable-track]:h-full [&::-moz-slider-runnable-track]:h-full peer',
|
||||
background: 'bg-gray-200 dark:bg-gray-700',
|
||||
wrapper: 'relative w-full flex items-center',
|
||||
base: 'w-full absolute appearance-none cursor-pointer disabled:cursor-not-allowed disabled:bg-opacity-50 focus:outline-none peer group',
|
||||
rounded: 'rounded-lg',
|
||||
background: 'bg-transparent',
|
||||
ring: 'focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
|
||||
progress: {
|
||||
base: 'absolute inset-0 h-full pointer-events-none peer-disabled:bg-opacity-50',
|
||||
base: 'absolute pointer-events-none peer-disabled:bg-opacity-50',
|
||||
rounded: 'rounded-s-lg',
|
||||
background: 'bg-{color}-500 dark:bg-{color}-400'
|
||||
background: 'bg-{color}-500 dark:bg-{color}-400',
|
||||
size: {
|
||||
sm: 'h-1',
|
||||
md: 'h-2',
|
||||
lg: 'h-3'
|
||||
}
|
||||
},
|
||||
thumb: {
|
||||
base: '[&::-webkit-slider-thumb]:relative [&::-moz-range-thumb]:relative [&::-webkit-slider-thumb]:z-[1] [&::-moz-range-thumb]:z-[1] [&::-webkit-slider-thumb]:appearance-none [&::-moz-range-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0',
|
||||
@@ -573,10 +638,20 @@ const range = {
|
||||
lg: '[&::-webkit-slider-thumb]:h-5 [&::-moz-range-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-moz-range-thumb]:w-5 [&::-webkit-slider-thumb]:-mt-1 [&::-moz-range-thumb]:-mt-1'
|
||||
}
|
||||
},
|
||||
track: {
|
||||
base: '[&::-webkit-slider-runnable-track]:group-disabled:bg-opacity-50 [&::-moz-slider-runnable-track]:group-disabled:bg-opacity-50',
|
||||
background: '[&::-webkit-slider-runnable-track]:bg-gray-200 [&::-moz-slider-runnable-track]:bg-gray-200 [&::-webkit-slider-runnable-track]:dark:bg-gray-700 [&::-moz-slider-runnable-track]:dark:bg-gray-700',
|
||||
rounded: '[&::-webkit-slider-runnable-track]:rounded-lg [&::-moz-slider-runnable-track]:rounded-lg',
|
||||
size: {
|
||||
sm: '[&::-webkit-slider-runnable-track]:h-1 [&::-moz-slider-runnable-track]:h-1',
|
||||
md: '[&::-webkit-slider-runnable-track]:h-2 [&::-moz-slider-runnable-track]:h-2',
|
||||
lg: '[&::-webkit-slider-runnable-track]:h-3 [&::-moz-slider-runnable-track]:h-3'
|
||||
}
|
||||
},
|
||||
size: {
|
||||
sm: 'h-1',
|
||||
md: 'h-2',
|
||||
lg: 'h-3'
|
||||
sm: 'h-3',
|
||||
md: 'h-4',
|
||||
lg: 'h-5'
|
||||
},
|
||||
default: {
|
||||
size: 'md',
|
||||
@@ -745,6 +820,40 @@ const pagination = {
|
||||
}
|
||||
}
|
||||
|
||||
const tabs = {
|
||||
wrapper: 'relative space-y-2',
|
||||
container: 'relative w-full',
|
||||
base: 'focus:outline-none',
|
||||
list: {
|
||||
base: 'relative',
|
||||
background: 'bg-gray-100 dark:bg-gray-800',
|
||||
rounded: 'rounded-lg',
|
||||
shadow: '',
|
||||
padding: 'p-1',
|
||||
height: 'h-10',
|
||||
width: 'w-full',
|
||||
marker: {
|
||||
wrapper: 'absolute top-[4px] left-[4px] duration-200 ease-out focus:outline-none',
|
||||
base: 'w-full h-full',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
rounded: 'rounded-md',
|
||||
shadow: 'shadow-sm'
|
||||
},
|
||||
tab: {
|
||||
base: 'relative inline-flex items-center justify-center flex-shrink-0 w-full whitespace-nowrap focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors duration-200 ease-out',
|
||||
background: '',
|
||||
active: 'text-gray-900 dark:text-white',
|
||||
inactive: 'text-gray-500 dark:text-gray-400',
|
||||
height: 'h-8',
|
||||
padding: 'px-3',
|
||||
size: 'text-sm',
|
||||
font: 'font-medium',
|
||||
rounded: 'rounded-md',
|
||||
shadow: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Overlays
|
||||
|
||||
const modal = {
|
||||
@@ -888,7 +997,7 @@ 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',
|
||||
description: 'mt-1 text-sm leading-4 text-gray-500 dark:text-gray-400',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-lg',
|
||||
@@ -949,6 +1058,7 @@ export default {
|
||||
dropdown,
|
||||
kbd,
|
||||
accordion,
|
||||
alert,
|
||||
input,
|
||||
formGroup,
|
||||
textarea,
|
||||
@@ -964,6 +1074,7 @@ export default {
|
||||
verticalNavigation,
|
||||
commandPalette,
|
||||
pagination,
|
||||
tabs,
|
||||
modal,
|
||||
slideover,
|
||||
popover,
|
||||
|
||||
@@ -22,18 +22,6 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody :class="ui.tbody">
|
||||
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected, $attrs.onSelect && ui.tr.active]" @click="() => onSelect(row)">
|
||||
<td v-if="modelValue" class="ps-4">
|
||||
<UCheckbox v-model="selected" :value="row" @click.stop />
|
||||
</td>
|
||||
|
||||
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]">
|
||||
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index">
|
||||
{{ row[column.key] }}
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="loadingState && loading">
|
||||
<td :colspan="columns.length + (modelValue ? 1 : 0)">
|
||||
<slot name="loading-state">
|
||||
@@ -59,6 +47,20 @@
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<template v-else>
|
||||
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected, $attrs.onSelect && ui.tr.active]" @click="() => onSelect(row)">
|
||||
<td v-if="modelValue" class="ps-4">
|
||||
<UCheckbox v-model="selected" :value="row" @click.stop />
|
||||
</td>
|
||||
|
||||
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]">
|
||||
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index">
|
||||
{{ row[column.key] }}
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -109,7 +111,7 @@ export default defineComponent({
|
||||
default: () => ({})
|
||||
},
|
||||
sortButton: {
|
||||
type: Object as PropType<Partial<Button>>,
|
||||
type: Object as PropType<Button>,
|
||||
default: () => appConfig.ui.table.default.sortButton
|
||||
},
|
||||
sortAscIcon: {
|
||||
|
||||
130
src/runtime/components/elements/Alert.vue
Normal file
130
src/runtime/components/elements/Alert.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div :class="alertClass">
|
||||
<div class="flex gap-3" :class="{ 'items-start': (description || $slots.description), 'items-center': !description && !$slots.description }">
|
||||
<UIcon v-if="icon" :name="icon" :class="ui.icon.base" />
|
||||
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
|
||||
|
||||
<div class="w-0 flex-1">
|
||||
<p :class="ui.title">
|
||||
<slot name="title" :title="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</p>
|
||||
<p v-if="description || $slots.description" :class="ui.description">
|
||||
<slot name="description" :description="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
</p>
|
||||
|
||||
<div v-if="(description || $slots.description) && actions.length" class="mt-3 flex items-center gap-2">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="action.click" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-shrink-0 flex items-center gap-3">
|
||||
<div v-if="!description && !$slots.description && actions.length" class="flex items-center gap-2">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="action.click" />
|
||||
</div>
|
||||
|
||||
<UButton v-if="closeButton" v-bind="{ ...ui.default.closeButton, ...closeButton }" @click.stop="$emit('close')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import UButton from '../elements/Button.vue'
|
||||
import type { Avatar } from '../../types/avatar'
|
||||
import type { Button } from '../../types/button'
|
||||
import { classNames } from '../../utils'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UIcon,
|
||||
UAvatar,
|
||||
UButton
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.alert.default.icon
|
||||
},
|
||||
avatar: {
|
||||
type: Object as PropType<Avatar>,
|
||||
default: null
|
||||
},
|
||||
closeButton: {
|
||||
type: Object as PropType<Button>,
|
||||
default: () => appConfig.ui.alert.default.closeButton
|
||||
},
|
||||
actions: {
|
||||
type: Array as PropType<Button & { click: Function }[]>,
|
||||
default: () => []
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.alert.default.color,
|
||||
validator (value: string) {
|
||||
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.alert.color)].includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.alert.default.variant,
|
||||
validator (value: string) {
|
||||
return [
|
||||
...Object.keys(appConfig.ui.alert.variant),
|
||||
...Object.values(appConfig.ui.alert.color).flatMap(value => Object.keys(value))
|
||||
].includes(value)
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.alert>>,
|
||||
default: () => appConfig.ui.alert
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.alert>>(() => defu({}, props.ui, appConfig.ui.alert))
|
||||
|
||||
const alertClass = computed(() => {
|
||||
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
|
||||
return classNames(
|
||||
ui.value.wrapper,
|
||||
ui.value.rounded,
|
||||
ui.value.shadow,
|
||||
ui.value.padding,
|
||||
variant?.replaceAll('{color}', props.color)
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
alertClass
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<component
|
||||
:is="buttonIs"
|
||||
:class="buttonClass"
|
||||
:aria-label="ariaLabel"
|
||||
v-bind="buttonProps"
|
||||
>
|
||||
<ULink :type="type" :disabled="disabled || loading" :class="buttonClass">
|
||||
<slot name="leading" :disabled="disabled" :loading="loading">
|
||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
|
||||
</slot>
|
||||
@@ -18,17 +13,16 @@
|
||||
<slot name="trailing" :disabled="disabled" :loading="loading">
|
||||
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
|
||||
</slot>
|
||||
</component>
|
||||
</ULink>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useSlots } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
import { defu } from 'defu'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import ULink from '../elements/Link.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { NuxtLink } from '#components'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -39,7 +33,7 @@ import appConfig from '#build/app.config'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UIcon,
|
||||
NuxtLink
|
||||
ULink
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
@@ -114,18 +108,6 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | RouteLocationRaw>,
|
||||
default: null
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
square: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -143,25 +125,9 @@ export default defineComponent({
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.button>>(() => defu({}, props.ui, appConfig.ui.button))
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const buttonIs = computed(() => {
|
||||
if (props.to) {
|
||||
return 'NuxtLink'
|
||||
}
|
||||
|
||||
return 'button'
|
||||
})
|
||||
|
||||
const buttonProps = computed(() => {
|
||||
if (props.to) {
|
||||
return { to: props.to, target: props.target }
|
||||
} else {
|
||||
return { disabled: props.disabled || props.loading, type: props.type }
|
||||
}
|
||||
})
|
||||
const ui = computed<Partial<typeof appConfig.ui.button>>(() => defu({}, props.ui, appConfig.ui.button))
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
|
||||
@@ -221,8 +187,6 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
return {
|
||||
buttonIs,
|
||||
buttonProps,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
isSquare,
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
<HMenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.height]" static>
|
||||
<div v-for="(subItems, index) of items" :key="index" :class="ui.padding">
|
||||
<HMenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
|
||||
<ULinkCustom
|
||||
v-bind="omit(item, ['label', 'icon', 'iconClass', 'avatar', 'shortcuts', 'click'])"
|
||||
<ULink
|
||||
v-bind="omit(item, ['label', 'slot', 'icon', 'iconClass', 'avatar', 'shortcuts', 'disabled', 'click'])"
|
||||
:class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]"
|
||||
@click="item.click"
|
||||
>
|
||||
@@ -35,7 +35,7 @@
|
||||
<UKbd v-for="shortcut of item.shortcuts" :key="shortcut">{{ shortcut }}</UKbd>
|
||||
</span>
|
||||
</slot>
|
||||
</ULinkCustom>
|
||||
</ULink>
|
||||
</HMenuItem>
|
||||
</div>
|
||||
</HMenuItems>
|
||||
@@ -53,7 +53,7 @@ import { omit } from 'lodash-es'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import UKbd from '../elements/Kbd.vue'
|
||||
import ULinkCustom from '../elements/LinkCustom.vue'
|
||||
import ULink from '../elements/Link.vue'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { DropdownItem } from '../../types/dropdown'
|
||||
import type { PopperOptions } from '../../types'
|
||||
@@ -73,7 +73,7 @@ export default defineComponent({
|
||||
UIcon,
|
||||
UAvatar,
|
||||
UKbd,
|
||||
ULinkCustom
|
||||
ULink
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
|
||||
86
src/runtime/components/elements/Link.vue
Normal file
86
src/runtime/components/elements/Link.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<button v-if="!to" :type="type" :disabled="disabled" v-bind="$attrs" :class="inactiveClass">
|
||||
<slot />
|
||||
</button>
|
||||
<NuxtLink
|
||||
v-else
|
||||
v-slot="{ route, href, target, rel, navigate, isActive, isExactActive, isExternal }"
|
||||
v-bind="$props"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
v-bind="$attrs"
|
||||
:href="!disabled ? href : undefined"
|
||||
:aria-disabled="disabled ? 'true' : undefined"
|
||||
:role="disabled ? 'link' : undefined"
|
||||
:rel="rel"
|
||||
:target="target"
|
||||
:class="resolveLinkClass(route, { isActive, isExactActive })"
|
||||
@click="(e) => !isExternal && navigate(e)"
|
||||
>
|
||||
<slot v-bind="{ isActive: exact ? isExactActive : isActive }" />
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { isEqual } from 'lodash-es'
|
||||
import { defineComponent } from 'vue'
|
||||
import { NuxtLink } from '#components'
|
||||
import { useRoute } from '#imports'
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...NuxtLink.props,
|
||||
type: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
exactQuery: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
exactHash: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
function resolveLinkClass (route, { isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
|
||||
if (props.exactQuery && !isEqual(route.query, useRoute().query)) {
|
||||
return props.inactiveClass
|
||||
}
|
||||
if (props.exactHash && route.hash !== useRoute().hash) {
|
||||
return props.inactiveClass
|
||||
}
|
||||
|
||||
if (props.exact && isExactActive) {
|
||||
return props.activeClass
|
||||
}
|
||||
|
||||
if (!props.exact && isActive) {
|
||||
return props.activeClass
|
||||
}
|
||||
|
||||
return props.inactiveClass
|
||||
}
|
||||
|
||||
return {
|
||||
resolveLinkClass
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<button v-if="!$attrs.to" v-bind="$attrs" :class="inactiveClass">
|
||||
<slot />
|
||||
</button>
|
||||
<NuxtLink
|
||||
v-else
|
||||
v-slot="{ href, navigate, exact, isActive, isExactActive }"
|
||||
:to="$attrs.to"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:class="resolveLinkClass({ isActive, isExactActive })"
|
||||
@click="navigate"
|
||||
>
|
||||
<slot v-bind="{ isActive: exact ? isExactActive : isActive }" />
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
function resolveLinkClass ({ isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
|
||||
if (isActive || isExactActive) {
|
||||
return props.activeClass
|
||||
}
|
||||
|
||||
return props.inactiveClass
|
||||
}
|
||||
</script>
|
||||
@@ -14,6 +14,7 @@
|
||||
class="form-checkbox"
|
||||
:class="inputClass"
|
||||
v-bind="$attrs"
|
||||
@change="onChange"
|
||||
>
|
||||
</div>
|
||||
<div v-if="label || $slots.label" class="ms-3 text-sm">
|
||||
@@ -33,6 +34,7 @@ import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -91,13 +93,15 @@ export default defineComponent({
|
||||
default: () => appConfig.ui.checkbox
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.checkbox>>(() => defu({}, props.ui, appConfig.ui.checkbox))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
|
||||
const toggle = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
@@ -107,6 +111,11 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
emit('change', event)
|
||||
emitFormBlur()
|
||||
}
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
@@ -122,7 +131,8 @@ export default defineComponent({
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
toggle,
|
||||
inputClass
|
||||
inputClass,
|
||||
onChange
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
162
src/runtime/components/forms/Form.ts
Normal file
162
src/runtime/components/forms/Form.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { provide, ref, type PropType, h, defineComponent } from 'vue'
|
||||
import { useEventBus } from '@vueuse/core'
|
||||
import type { ZodSchema, ZodError } from 'zod'
|
||||
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
|
||||
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
|
||||
import type { FormError, FormEvent } from '../../types'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
schema: {
|
||||
type: Object as
|
||||
| PropType<ZodSchema>
|
||||
| PropType<YupObjectSchema<any>>
|
||||
| PropType<JoiSchema>,
|
||||
default: undefined
|
||||
},
|
||||
state: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
validate: {
|
||||
type: Function as PropType<(state: any) => Promise<FormError[]>> | PropType<(state: any) => FormError[]>,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
setup (props, { slots, expose }) {
|
||||
const seed = Math.random().toString(36).substring(7)
|
||||
const bus = useEventBus<FormEvent>(`form-${seed}`)
|
||||
|
||||
bus.on(async (event) => {
|
||||
if (event.type === 'blur') {
|
||||
const otherErrors = errors.value.filter(
|
||||
(error) => error.path !== event.path
|
||||
)
|
||||
const pathErrors = (await getErrors()).filter(
|
||||
(error) => error.path === event.path
|
||||
)
|
||||
errors.value = otherErrors.concat(pathErrors)
|
||||
}
|
||||
})
|
||||
|
||||
const errors = ref<FormError[]>([])
|
||||
provide('form-errors', errors)
|
||||
provide('form-events', bus)
|
||||
|
||||
async function getErrors (): Promise<FormError[]> {
|
||||
let errs = await props.validate(props.state)
|
||||
|
||||
if (props.schema) {
|
||||
if (isZodSchema(props.schema)) {
|
||||
errs = errs.concat(await getZodErrors(props.state, props.schema))
|
||||
} else if (isYupSchema(props.schema)) {
|
||||
errs = errs.concat(await getYupErrors(props.state, props.schema))
|
||||
} else if (isJoiSchema(props.schema)) {
|
||||
errs = errs.concat(await getJoiErrors(props.state, props.schema))
|
||||
} else {
|
||||
throw new Error('Form validation failed: Unsupported form schema')
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
async function validate () {
|
||||
errors.value = await getErrors()
|
||||
if (errors.value.length > 0) {
|
||||
throw new Error(
|
||||
`Form validation failed: ${JSON.stringify(errors.value, null, 2)}`
|
||||
)
|
||||
}
|
||||
|
||||
return props.state
|
||||
}
|
||||
|
||||
expose({
|
||||
validate
|
||||
})
|
||||
|
||||
return () => h('form', slots.default?.())
|
||||
}
|
||||
})
|
||||
|
||||
function isYupSchema (schema: any): schema is YupObjectSchema<any> {
|
||||
return schema.validate && schema.__isYupSchema__
|
||||
}
|
||||
|
||||
function isYupError (error: any): error is YupError {
|
||||
return error.inner !== undefined
|
||||
}
|
||||
|
||||
async function getYupErrors (
|
||||
state: any,
|
||||
schema: YupObjectSchema<any>
|
||||
): Promise<FormError[]> {
|
||||
try {
|
||||
await schema.validate(state, { abortEarly: false })
|
||||
return []
|
||||
} catch (error) {
|
||||
if (isYupError(error)) {
|
||||
return error.inner.map((issue) => ({
|
||||
path: issue.path ?? '',
|
||||
message: issue.message
|
||||
}))
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isZodSchema (schema: any): schema is ZodSchema {
|
||||
return schema.parse !== undefined
|
||||
}
|
||||
|
||||
function isZodError (error: any): error is ZodError {
|
||||
return error.issues !== undefined
|
||||
}
|
||||
|
||||
async function getZodErrors (
|
||||
state: any,
|
||||
schema: ZodSchema
|
||||
): Promise<FormError[]> {
|
||||
try {
|
||||
schema.parse(state)
|
||||
return []
|
||||
} catch (error) {
|
||||
if (isZodError(error)) {
|
||||
return error.issues.map((issue) => ({
|
||||
path: issue.path.join('.'),
|
||||
message: issue.message
|
||||
}))
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isJoiSchema (schema: any): schema is JoiSchema {
|
||||
return schema.validateAsync !== undefined && schema.id !== undefined
|
||||
}
|
||||
|
||||
function isJoiError (error: any): error is JoiError {
|
||||
return error.isJoi === true
|
||||
}
|
||||
|
||||
async function getJoiErrors (
|
||||
state: any,
|
||||
schema: JoiSchema
|
||||
): Promise<FormError[]> {
|
||||
try {
|
||||
await schema.validateAsync(state, { abortEarly: false })
|
||||
return []
|
||||
} catch (error) {
|
||||
if (isJoiError(error)) {
|
||||
return error.details.map((detail) => ({
|
||||
path: detail.path.join('.'),
|
||||
message: detail.message
|
||||
}))
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { h, cloneVNode, computed, defineComponent } from 'vue'
|
||||
import { h, cloneVNode, computed, defineComponent, provide, inject } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { getSlotsChildren } from '../../utils'
|
||||
import type { FormError } from '../../types'
|
||||
import { useAppConfig } from '#imports'
|
||||
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
@@ -15,6 +17,13 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.formGroup.size).includes(value)
|
||||
}
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
@@ -50,13 +59,21 @@ export default defineComponent({
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.formGroup>>(() => defu({}, props.ui, appConfig.ui.formGroup))
|
||||
|
||||
provide('form-path', props.name)
|
||||
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
|
||||
const errorMessage = computed(() => {
|
||||
return props.error && typeof props.error === 'string'
|
||||
? props.error
|
||||
: formErrors?.value?.find((error) => error.path === props.name)?.message
|
||||
})
|
||||
|
||||
const children = computed(() => getSlotsChildren(slots))
|
||||
|
||||
const clones = computed(() => children.value.map((node) => {
|
||||
const vProps: any = {}
|
||||
|
||||
if (props.error) {
|
||||
vProps.oldColor = node.props.color
|
||||
if (errorMessage.value) {
|
||||
vProps.oldColor = node.props?.color
|
||||
vProps.color = 'red'
|
||||
} else if (vProps.oldColor) {
|
||||
vProps.color = vProps.oldColor
|
||||
@@ -66,18 +83,23 @@ export default defineComponent({
|
||||
vProps.name = props.name
|
||||
}
|
||||
|
||||
if (props.size) {
|
||||
vProps.size = props.size
|
||||
}
|
||||
|
||||
return cloneVNode(node, vProps)
|
||||
}))
|
||||
|
||||
return () => h('div', { class: [ui.value.wrapper] }, [
|
||||
props.label && h('div', { class: [ui.value.label.wrapper] }, [
|
||||
props.label && h('div', { class: [ui.value.label.wrapper, ui.value.size[props.size]] }, [
|
||||
h('label', { for: props.name, class: [ui.value.label.base, props.required && ui.value.label.required] }, props.label),
|
||||
props.hint && h('span', { class: [ui.value.hint] }, props.hint)
|
||||
]),
|
||||
props.description && h('p', { class: [ui.value.description] }, props.description),
|
||||
props.description && h('p', { class: [ui.value.description, ui.value.size[props.size]
|
||||
] }, props.description),
|
||||
h('div', { class: [!!props.label && ui.value.container] }, [
|
||||
...clones.value,
|
||||
props.error && typeof props.error === 'string' ? h('p', { class: [ui.value.error] }, props.error) : props.help ? h('p', { class: [ui.value.help] }, props.help) : null
|
||||
errorMessage.value ? h('p', { class: [ui.value.error, ui.value.size[props.size]] }, errorMessage.value) : props.help ? h('p', { class: [ui.value.help, ui.value.size[props.size]] }, props.help) : null
|
||||
])
|
||||
])
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:class="inputClass"
|
||||
v-bind="$attrs"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -35,6 +36,7 @@ import { ref, computed, onMounted, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { classNames } from '../../utils'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
@@ -138,13 +140,15 @@ export default defineComponent({
|
||||
default: () => appConfig.ui.input
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
emits: ['update:modelValue', 'blur'],
|
||||
setup (props, { emit, slots }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.input>>(() => defu({}, props.ui, appConfig.ui.input))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const autoFocus = () => {
|
||||
@@ -157,6 +161,11 @@ export default defineComponent({
|
||||
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
emitFormBlur()
|
||||
emit('blur', event)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
@@ -249,7 +258,8 @@ export default defineComponent({
|
||||
trailingIconName,
|
||||
trailingIconClass,
|
||||
trailingWrapperIconClass,
|
||||
onInput
|
||||
onInput,
|
||||
onBlur
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
class="form-radio"
|
||||
:class="inputClass"
|
||||
v-bind="$attrs"
|
||||
@change="onChange"
|
||||
>
|
||||
</div>
|
||||
<div v-if="label || $slots.label" class="ms-3 text-sm">
|
||||
@@ -31,6 +32,7 @@ import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -81,19 +83,24 @@ export default defineComponent({
|
||||
default: () => appConfig.ui.radio
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.radio>>(() => defu({}, props.ui, appConfig.ui.radio))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
|
||||
const pick = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
if (value) {
|
||||
emitFormBlur()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<input
|
||||
:id="name"
|
||||
@@ -10,8 +10,9 @@
|
||||
:disabled="disabled"
|
||||
:step="step"
|
||||
type="range"
|
||||
:class="[inputClass, thumbClass]"
|
||||
:class="[inputClass, thumbClass, trackClass]"
|
||||
v-bind="$attrs"
|
||||
@change="onChange"
|
||||
>
|
||||
|
||||
<span :class="progressClass" :style="progressStyle" />
|
||||
@@ -23,6 +24,7 @@ import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -74,13 +76,15 @@ export default defineComponent({
|
||||
default: () => appConfig.ui.range
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.range>>(() => defu({}, props.ui, appConfig.ui.range))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
|
||||
const value = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
@@ -90,6 +94,11 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
emit('change', event)
|
||||
emitFormBlur()
|
||||
}
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.wrapper,
|
||||
@@ -118,12 +127,21 @@ export default defineComponent({
|
||||
)
|
||||
})
|
||||
|
||||
const trackClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.track.base,
|
||||
ui.value.track.background,
|
||||
ui.value.track.rounded,
|
||||
ui.value.track.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const progressClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.progress.base,
|
||||
ui.value.progress.rounded,
|
||||
ui.value.progress.background.replaceAll('{color}', props.color),
|
||||
ui.value.size[props.size]
|
||||
ui.value.progress.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -143,8 +161,10 @@ export default defineComponent({
|
||||
wrapperClass,
|
||||
inputClass,
|
||||
thumbClass,
|
||||
trackClass,
|
||||
progressClass,
|
||||
progressStyle
|
||||
progressStyle,
|
||||
onChange
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
:class="selectClass"
|
||||
v-bind="$attrs"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
>
|
||||
<template v-for="(option, index) in normalizedOptionsWithPlaceholder">
|
||||
<optgroup
|
||||
@@ -59,6 +60,7 @@ import { get } from 'lodash-es'
|
||||
import { defu } from 'defu'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -165,17 +167,24 @@ export default defineComponent({
|
||||
default: () => appConfig.ui.select
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup (props, { emit, slots }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.ui, appConfig.ui.select))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
|
||||
const onInput = (event: InputEvent) => {
|
||||
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
emitFormBlur()
|
||||
emit('change', event)
|
||||
}
|
||||
|
||||
const guessOptionValue = (option: any) => {
|
||||
return get(option, props.valueAttribute, get(option, props.optionAttribute))
|
||||
}
|
||||
@@ -314,7 +323,8 @@ export default defineComponent({
|
||||
trailingIconName,
|
||||
trailingIconClass,
|
||||
trailingWrapperIconClass,
|
||||
onInput
|
||||
onInput,
|
||||
onChange
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:multiple="multiple"
|
||||
:disabled="disabled || loading"
|
||||
as="div"
|
||||
:class="ui.wrapper"
|
||||
:class="uiMenu.wrapper"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<input
|
||||
@@ -28,7 +28,7 @@
|
||||
class="inline-flex w-full"
|
||||
>
|
||||
<slot :open="open" :disabled="disabled" :loading="loading">
|
||||
<button :class="selectMenuClass" :disabled="disabled || loading" type="button" v-bind="$attrs">
|
||||
<button :class="selectClass" :disabled="disabled || loading" type="button" v-bind="$attrs">
|
||||
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
|
||||
<slot name="leading" :disabled="disabled" :loading="loading">
|
||||
<UIcon :name="leadingIconName" :class="leadingIconClass" />
|
||||
@@ -38,7 +38,7 @@
|
||||
<slot name="label">
|
||||
<span v-if="multiple && Array.isArray(modelValue) && modelValue.length" class="block truncate">{{ modelValue.length }} selected</span>
|
||||
<span v-else-if="!multiple && modelValue" class="block truncate">{{ typeof modelValue === 'string' ? modelValue : modelValue[optionAttribute] }}</span>
|
||||
<span v-else class="block truncate" :class="ui.placeholder">{{ placeholder || ' ' }}</span>
|
||||
<span v-else class="block truncate" :class="uiMenu.placeholder">{{ placeholder || ' ' }}</span>
|
||||
</slot>
|
||||
|
||||
<span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
|
||||
@@ -50,9 +50,9 @@
|
||||
</slot>
|
||||
</component>
|
||||
|
||||
<div v-if="open" ref="container" :class="[ui.container, ui.width]">
|
||||
<Transition appear v-bind="ui.transition">
|
||||
<component :is="searchable ? 'HComboboxOptions' : 'HListboxOptions'" static :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.padding, ui.height]">
|
||||
<div v-if="open" ref="container" :class="[uiMenu.container, uiMenu.width]">
|
||||
<Transition appear v-bind="uiMenu.transition">
|
||||
<component :is="searchable ? 'HComboboxOptions' : 'HListboxOptions'" static :class="[uiMenu.base, uiMenu.divide, uiMenu.ring, uiMenu.rounded, uiMenu.shadow, uiMenu.background, uiMenu.padding, uiMenu.height]">
|
||||
<HComboboxInput
|
||||
v-if="searchable"
|
||||
ref="searchInput"
|
||||
@@ -61,7 +61,7 @@
|
||||
:placeholder="searchablePlaceholder"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
:class="ui.input"
|
||||
:class="uiMenu.input"
|
||||
@change="query = $event.target.value"
|
||||
/>
|
||||
<component
|
||||
@@ -70,41 +70,41 @@
|
||||
v-slot="{ active, selected, disabled: optionDisabled }"
|
||||
:key="index"
|
||||
as="template"
|
||||
:value="option"
|
||||
:value="valueAttribute ? option[valueAttribute] : option"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
<li :class="[ui.option.base, ui.option.rounded, ui.option.padding, ui.option.size, ui.option.color, active ? ui.option.active : ui.option.inactive, selected && ui.option.selected, optionDisabled && ui.option.disabled]">
|
||||
<div :class="ui.option.container">
|
||||
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive, selected && uiMenu.option.selected, optionDisabled && uiMenu.option.disabled]">
|
||||
<div :class="uiMenu.option.container">
|
||||
<slot name="option" :option="option" :active="active" :selected="selected">
|
||||
<UIcon v-if="option.icon" :name="option.icon" :class="[ui.option.icon.base, active ? ui.option.icon.active : ui.option.icon.inactive, option.iconClass]" aria-hidden="true" />
|
||||
<UIcon v-if="option.icon" :name="option.icon" :class="[uiMenu.option.icon.base, active ? uiMenu.option.icon.active : uiMenu.option.icon.inactive, option.iconClass]" aria-hidden="true" />
|
||||
<UAvatar
|
||||
v-else-if="option.avatar"
|
||||
v-bind="{ size: ui.option.avatar.size, ...option.avatar }"
|
||||
:class="ui.option.avatar.base"
|
||||
v-bind="{ size: uiMenu.option.avatar.size, ...option.avatar }"
|
||||
:class="uiMenu.option.avatar.base"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span v-else-if="option.chip" :class="ui.option.chip.base" :style="{ background: `#${option.chip}` }" />
|
||||
<span v-else-if="option.chip" :class="uiMenu.option.chip.base" :style="{ background: `#${option.chip}` }" />
|
||||
|
||||
<span class="truncate">{{ typeof option === 'string' ? option : option[optionAttribute] }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<span v-if="selected" :class="[ui.option.selectedIcon.wrapper, ui.option.selectedIcon.padding]">
|
||||
<UIcon :name="selectedIcon" :class="ui.option.selectedIcon.base" aria-hidden="true" />
|
||||
<span v-if="selected" :class="[uiMenu.option.selectedIcon.wrapper, uiMenu.option.selectedIcon.padding]">
|
||||
<UIcon :name="selectedIcon" :class="uiMenu.option.selectedIcon.base" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</component>
|
||||
|
||||
<component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" as="template">
|
||||
<li :class="[ui.option.base, ui.option.rounded, ui.option.padding, ui.option.size, ui.option.color, active ? ui.option.active : ui.option.inactive]">
|
||||
<div :class="ui.option.container">
|
||||
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive]">
|
||||
<div :class="uiMenu.option.container">
|
||||
<slot name="option-create" :option="queryOption" :active="active" :selected="selected">
|
||||
<span class="block truncate">Create "{{ queryOption[optionAttribute] }}"</span>
|
||||
</slot>
|
||||
</div>
|
||||
</li>
|
||||
</component>
|
||||
<p v-else-if="searchable && query && !filteredOptions.length" :class="ui.option.empty">
|
||||
<p v-else-if="searchable && query && !filteredOptions.length" :class="uiMenu.option.empty">
|
||||
<slot name="option-empty" :query="query">
|
||||
No results for "{{ query }}".
|
||||
</slot>
|
||||
@@ -135,6 +135,7 @@ import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
@@ -271,6 +272,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
valueAttribute: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
searchAttributes: {
|
||||
type: Array,
|
||||
default: null
|
||||
@@ -280,42 +285,43 @@ export default defineComponent({
|
||||
default: () => ({})
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.selectMenu>>,
|
||||
default: () => appConfig.ui.selectMenu
|
||||
},
|
||||
uiSelect: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.select>>,
|
||||
default: () => appConfig.ui.select
|
||||
},
|
||||
uiMenu: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.selectMenu>>,
|
||||
default: () => appConfig.ui.selectMenu
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'open', 'close'],
|
||||
emits: ['update:modelValue', 'open', 'close', 'change'],
|
||||
setup (props, { emit, slots }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.selectMenu>>(() => defu({}, props.ui, appConfig.ui.selectMenu))
|
||||
const uiSelect = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.uiSelect, appConfig.ui.select))
|
||||
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.ui, appConfig.ui.select))
|
||||
const uiMenu = computed<Partial<typeof appConfig.ui.selectMenu>>(() => defu({}, props.uiMenu, appConfig.ui.selectMenu))
|
||||
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, uiMenu.value.popper as PopperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popper.value)
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
|
||||
const query = ref('')
|
||||
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
|
||||
|
||||
const selectMenuClass = computed(() => {
|
||||
const variant = uiSelect.value.color?.[props.color as string]?.[props.variant as string] || uiSelect.value.variant[props.variant]
|
||||
const selectClass = computed(() => {
|
||||
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
|
||||
return classNames(
|
||||
uiSelect.value.base,
|
||||
uiSelect.value.rounded,
|
||||
ui.value.base,
|
||||
ui.value.rounded,
|
||||
'text-left cursor-default',
|
||||
uiSelect.value.size[props.size],
|
||||
uiSelect.value.gap[props.size],
|
||||
props.padded ? uiSelect.value.padding[props.size] : 'p-0',
|
||||
ui.value.size[props.size],
|
||||
ui.value.gap[props.size],
|
||||
props.padded ? ui.value.padding[props.size] : 'p-0',
|
||||
variant?.replaceAll('{color}', props.color),
|
||||
(isLeading.value || slots.leading) && uiSelect.value.leading.padding[props.size],
|
||||
(isTrailing.value || slots.trailing) && uiSelect.value.trailing.padding[props.size],
|
||||
(isLeading.value || slots.leading) && ui.value.leading.padding[props.size],
|
||||
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[props.size],
|
||||
'inline-flex items-center'
|
||||
)
|
||||
})
|
||||
@@ -346,34 +352,34 @@ export default defineComponent({
|
||||
|
||||
const leadingWrapperIconClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.icon.leading.wrapper,
|
||||
uiSelect.value.icon.leading.pointer,
|
||||
uiSelect.value.icon.leading.padding[props.size]
|
||||
ui.value.icon.leading.wrapper,
|
||||
ui.value.icon.leading.pointer,
|
||||
ui.value.icon.leading.padding[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.icon.base,
|
||||
appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
|
||||
uiSelect.value.icon.size[props.size],
|
||||
ui.value.icon.base,
|
||||
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||
ui.value.icon.size[props.size],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const trailingWrapperIconClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.icon.trailing.wrapper,
|
||||
uiSelect.value.icon.trailing.pointer,
|
||||
uiSelect.value.icon.trailing.padding[props.size]
|
||||
ui.value.icon.trailing.wrapper,
|
||||
ui.value.icon.trailing.pointer,
|
||||
ui.value.icon.trailing.padding[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.icon.base,
|
||||
appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
|
||||
uiSelect.value.icon.size[props.size],
|
||||
ui.value.icon.base,
|
||||
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||
ui.value.icon.size[props.size],
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
)
|
||||
})
|
||||
@@ -405,6 +411,7 @@ export default defineComponent({
|
||||
emit('open')
|
||||
} else {
|
||||
emit('close')
|
||||
emitFormBlur()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -415,16 +422,18 @@ export default defineComponent({
|
||||
searchInput.value.$el.value = ''
|
||||
}
|
||||
emit('update:modelValue', event)
|
||||
emit('change', event)
|
||||
emitFormBlur()
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
uiMenu,
|
||||
trigger,
|
||||
container,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
selectMenuClass,
|
||||
selectClass,
|
||||
leadingIconName,
|
||||
leadingIconClass,
|
||||
leadingWrapperIconClass,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:class="textareaClass"
|
||||
v-bind="$attrs"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,6 +23,7 @@ import { ref, computed, watch, onMounted, nextTick, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -101,7 +103,7 @@ export default defineComponent({
|
||||
default: () => appConfig.ui.textarea
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
emits: ['update:modelValue', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
const textarea = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
@@ -110,6 +112,8 @@ export default defineComponent({
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.textarea>>(() => defu({}, props.ui, appConfig.ui.textarea))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
textarea.value?.focus()
|
||||
@@ -144,6 +148,17 @@ export default defineComponent({
|
||||
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
emitFormBlur()
|
||||
emit('blur', event)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
nextTick(autoResize)
|
||||
})
|
||||
@@ -174,7 +189,8 @@ export default defineComponent({
|
||||
ui,
|
||||
textarea,
|
||||
textareaClass,
|
||||
onInput
|
||||
onInput,
|
||||
onBlur
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ import { defu } from 'defu'
|
||||
import { Switch as HSwitch } from '@headlessui/vue'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { useFormEvents } from '../../composables/useFormEvents'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
@@ -75,16 +76,19 @@ export default defineComponent({
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.toggle>>(() => defu({}, props.ui, appConfig.ui.toggle))
|
||||
|
||||
const { emitFormBlur } = useFormEvents()
|
||||
|
||||
const active = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
emitFormBlur()
|
||||
}
|
||||
})
|
||||
|
||||
const switchClass = computed(()=>{
|
||||
const switchClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.rounded,
|
||||
@@ -93,13 +97,13 @@ export default defineComponent({
|
||||
)
|
||||
})
|
||||
|
||||
const onIconClass = computed(()=>{
|
||||
const onIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.on.replaceAll('{color}', props.color)
|
||||
)
|
||||
})
|
||||
|
||||
const offIconClass = computed(()=>{
|
||||
const offIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.off.replaceAll('{color}', props.color)
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<component
|
||||
:is="$attrs.onSubmit ? 'form': as"
|
||||
:is="$attrs.onSubmit ? 'form' : as"
|
||||
:class="[ui.base, ui.rounded, ui.divide, ui.ring, ui.shadow, ui.background]"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
|
||||
@@ -138,7 +138,7 @@ export default defineComponent({
|
||||
default: () => appConfig.ui.commandPalette.default.selectedIcon
|
||||
},
|
||||
closeButton: {
|
||||
type: Object as PropType<Partial<Button>>,
|
||||
type: Object as PropType<Button>,
|
||||
default: () => appConfig.ui.commandPalette.default.closeButton
|
||||
},
|
||||
emptyState: {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
>
|
||||
<div :class="[ui.group.command.base, active ? ui.group.command.active : ui.group.command.inactive, command.disabled ? 'cursor-not-allowed' : 'cursor-pointer']">
|
||||
<div :class="ui.group.command.container">
|
||||
<slot :name="`${group.key}-icon`" :group="group" :command="command">
|
||||
<slot :name="`${group.key}-icon`" :group="group" :command="command" :active="active" :selected="selected">
|
||||
<UIcon v-if="command.icon" :name="command.icon" :class="[ui.group.command.icon.base, active ? ui.group.command.icon.active : ui.group.command.icon.inactive, command.iconClass]" aria-hidden="true" />
|
||||
<UAvatar
|
||||
v-else-if="command.avatar"
|
||||
@@ -27,7 +27,7 @@
|
||||
</slot>
|
||||
|
||||
<div :class="[ui.group.command.label, command.disabled && ui.group.command.disabled]">
|
||||
<slot :name="`${group.key}-command`" :group="group" :command="command">
|
||||
<slot :name="`${group.key}-command`" :group="group" :command="command" :active="active" :selected="selected">
|
||||
<span v-if="command.prefix" class="flex-shrink-0" :class="command.prefixClass || ui.group.command.prefix">{{ command.prefix }}</span>
|
||||
|
||||
<span class="truncate" :class="{ 'flex-none': command.suffix || command.matches?.length }">{{ command[commandAttribute] }}</span>
|
||||
@@ -40,10 +40,24 @@
|
||||
</div>
|
||||
|
||||
<UIcon v-if="selected" :name="selectedIcon" :class="ui.group.command.selectedIcon.base" aria-hidden="true" />
|
||||
<slot v-else-if="active && (group.active || $slots[`${group.key}-active`])" :name="`${group.key}-active`" :group="group" :command="command">
|
||||
<slot
|
||||
v-else-if="active && (group.active || $slots[`${group.key}-active`])"
|
||||
:name="`${group.key}-active`"
|
||||
:group="group"
|
||||
:command="command"
|
||||
:active="active"
|
||||
:selected="selected"
|
||||
>
|
||||
<span v-if="group.active" :class="ui.group.active">{{ group.active }}</span>
|
||||
</slot>
|
||||
<slot v-else :name="`${group.key}-inactive`" :group="group" :command="command">
|
||||
<slot
|
||||
v-else
|
||||
:name="`${group.key}-inactive`"
|
||||
:group="group"
|
||||
:command="command"
|
||||
:active="active"
|
||||
:selected="selected"
|
||||
>
|
||||
<span v-if="command.shortcuts?.length" :class="ui.group.command.shortcuts">
|
||||
<UKbd v-for="shortcut of command.shortcuts" :key="shortcut">{{ shortcut }}</UKbd>
|
||||
</span>
|
||||
@@ -109,7 +123,7 @@ export default defineComponent({
|
||||
return typeof label === 'function' ? label(props.query) : label
|
||||
})
|
||||
|
||||
function highlight (text: string, { indices, value }: { indices: number[][], value:string }): string {
|
||||
function highlight (text: string, { indices, value }: { indices: number[][], value: string }): string {
|
||||
if (text === value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -82,19 +82,19 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
activeButton: {
|
||||
type: Object as PropType<Partial<Button>>,
|
||||
type: Object as PropType<Button>,
|
||||
default: () => appConfig.ui.pagination.default.activeButton
|
||||
},
|
||||
inactiveButton: {
|
||||
type: Object as PropType<Partial<Button>>,
|
||||
type: Object as PropType<Button>,
|
||||
default: () => appConfig.ui.pagination.default.inactiveButton
|
||||
},
|
||||
prevButton: {
|
||||
type: Object as PropType<Partial<Button>>,
|
||||
type: Object as PropType<Button>,
|
||||
default: () => appConfig.ui.pagination.default.prevButton
|
||||
},
|
||||
nextButton: {
|
||||
type: Object as PropType<Partial<Button>>,
|
||||
type: Object as PropType<Button>,
|
||||
default: () => appConfig.ui.pagination.default.nextButton
|
||||
},
|
||||
divider: {
|
||||
|
||||
117
src/runtime/components/navigation/Tabs.vue
Normal file
117
src/runtime/components/navigation/Tabs.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<HTabGroup :vertical="orientation === 'vertical'" :default-index="defaultIndex" as="div" :class="ui.wrapper" @change="onChange">
|
||||
<HTabList
|
||||
:class="[ui.list.base, ui.list.background, ui.list.rounded, ui.list.shadow, ui.list.padding, ui.list.width, orientation === 'horizontal' && ui.list.height, orientation === 'horizontal' && 'inline-grid items-center']"
|
||||
:style="[orientation === 'horizontal' && `grid-template-columns: repeat(${items.length}, minmax(0, 1fr))`]"
|
||||
>
|
||||
<div ref="markerRef" :class="ui.list.marker.wrapper">
|
||||
<div :class="[ui.list.marker.base, ui.list.marker.background, ui.list.marker.rounded, ui.list.marker.shadow]" />
|
||||
</div>
|
||||
|
||||
<HTab
|
||||
v-for="(item, index) of items"
|
||||
:key="index"
|
||||
ref="itemRefs"
|
||||
v-slot="{ selected, disabled }"
|
||||
:disabled="item.disabled"
|
||||
as="template"
|
||||
>
|
||||
<button :class="[ui.list.tab.base, ui.list.tab.background, ui.list.tab.height, ui.list.tab.padding, ui.list.tab.size, ui.list.tab.font, ui.list.tab.rounded, ui.list.tab.shadow, selected ? ui.list.tab.active : ui.list.tab.inactive]">
|
||||
<slot :item="item" :index="index" :selected="selected" :disabled="disabled">
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
</button>
|
||||
</HTab>
|
||||
</HTabList>
|
||||
|
||||
<HTabPanels :class="ui.container">
|
||||
<HTabPanel
|
||||
v-for="(item, index) of items"
|
||||
:key="index"
|
||||
v-slot="{ selected }"
|
||||
:class="ui.base"
|
||||
>
|
||||
<slot :name="item.slot || 'item'" :item="item" :index="index" :selected="selected">
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
</HTabPanel>
|
||||
</HTabPanels>
|
||||
</HTabGroup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, onMounted, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { TabGroup as HTabGroup, TabList as HTabList, Tab as HTab, TabPanels as HTabPanels, TabPanel as HTabPanel } from '@headlessui/vue'
|
||||
import { defu } from 'defu'
|
||||
import type { TabItem } from '../../types/tabs'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
HTabGroup,
|
||||
HTabList,
|
||||
HTab,
|
||||
HTabPanels,
|
||||
HTabPanel
|
||||
},
|
||||
props: {
|
||||
orientation: {
|
||||
type: String as PropType<'horizontal' | 'vertical'>,
|
||||
default: 'horizontal',
|
||||
validator: (value: string) => ['horizontal', 'vertical'].includes(value)
|
||||
},
|
||||
defaultIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
items: {
|
||||
type: Array as PropType<TabItem[]>,
|
||||
default: () => []
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.tabs>>,
|
||||
default: () => appConfig.ui.tabs
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.tabs>>(() => defu({}, props.ui, appConfig.ui.tabs))
|
||||
|
||||
const itemRefs = ref<HTMLElement[]>([])
|
||||
const markerRef = ref<HTMLElement>()
|
||||
|
||||
// Methods
|
||||
|
||||
function onChange (index) {
|
||||
// @ts-ignore
|
||||
const tab = itemRefs.value[index]?.$el
|
||||
if (!tab) {
|
||||
return
|
||||
}
|
||||
|
||||
markerRef.value.style.top = `${tab.offsetTop}px`
|
||||
markerRef.value.style.left = `${tab.offsetLeft}px`
|
||||
markerRef.value.style.width = `${tab.offsetWidth}px`
|
||||
markerRef.value.style.height = `${tab.offsetHeight}px`
|
||||
}
|
||||
|
||||
onMounted(() => onChange(props.defaultIndex))
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
itemRefs,
|
||||
markerRef,
|
||||
onChange
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<nav :class="ui.wrapper">
|
||||
<ULinkCustom
|
||||
<ULink
|
||||
v-for="(link, index) of links"
|
||||
v-slot="{ isActive }"
|
||||
:key="index"
|
||||
@@ -11,7 +11,7 @@
|
||||
@click="link.click"
|
||||
@keyup.enter="$event.target.blur()"
|
||||
>
|
||||
<slot name="avatar" :link="link">
|
||||
<slot name="avatar" :link="link" :is-active="isActive">
|
||||
<UAvatar
|
||||
v-if="link.avatar"
|
||||
v-bind="{ size: ui.avatar.size, ...link.avatar }"
|
||||
@@ -25,7 +25,7 @@
|
||||
:class="[ui.icon.base, isActive ? ui.icon.active : ui.icon.inactive, link.iconClass]"
|
||||
/>
|
||||
</slot>
|
||||
<slot :link="link">
|
||||
<slot :link="link" :is-active="isActive">
|
||||
<span v-if="link.label" :class="ui.label">{{ link.label }}</span>
|
||||
</slot>
|
||||
<slot name="badge" :link="link" :is-active="isActive">
|
||||
@@ -33,7 +33,7 @@
|
||||
{{ link.badge }}
|
||||
</span>
|
||||
</slot>
|
||||
</ULinkCustom>
|
||||
</ULink>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
@@ -44,7 +44,7 @@ import { defu } from 'defu'
|
||||
import { omit } from 'lodash-es'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import ULinkCustom from '../elements/LinkCustom.vue'
|
||||
import ULink from '../elements/Link.vue'
|
||||
import type { VerticalNavigationLink } from '../../types/vertical-navigation'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
@@ -57,7 +57,7 @@ export default defineComponent({
|
||||
components: {
|
||||
UIcon,
|
||||
UAvatar,
|
||||
ULinkCustom
|
||||
ULink
|
||||
},
|
||||
props: {
|
||||
links: {
|
||||
|
||||
@@ -3,24 +3,28 @@
|
||||
<div :class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]" @mouseover="onMouseover" @mouseleave="onMouseleave">
|
||||
<div :class="[ui.container, ui.rounded, ui.ring]">
|
||||
<div :class="ui.padding">
|
||||
<div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }">
|
||||
<div class="flex gap-3" :class="{ 'items-start': description || $slots.description, 'items-center': !description && !$slots.description }">
|
||||
<UIcon v-if="icon" :name="icon" :class="iconClass" />
|
||||
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
|
||||
|
||||
<div class="w-0 flex-1">
|
||||
<p :class="ui.title">
|
||||
{{ title }}
|
||||
<slot name="title" :title="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</p>
|
||||
<p v-if="description" :class="ui.description">
|
||||
{{ description }}
|
||||
<p v-if="(description || $slots.description)" :class="ui.description">
|
||||
<slot name="description" :description="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
</p>
|
||||
|
||||
<div v-if="description && actions.length" class="mt-3 flex items-center gap-2">
|
||||
<div v-if="(description || $slots.description) && actions.length" class="mt-3 flex items-center gap-2">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-shrink-0 flex items-center gap-3">
|
||||
<div v-if="!description && actions.length" class="flex items-center gap-2">
|
||||
<div v-if="!description && !$slots.description && actions.length" class="flex items-center gap-2">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
|
||||
</div>
|
||||
|
||||
@@ -43,7 +47,7 @@ import UAvatar from '../elements/Avatar.vue'
|
||||
import UButton from '../elements/Button.vue'
|
||||
import { useTimer } from '../../composables/useTimer'
|
||||
import type { NotificationAction } from '../../types'
|
||||
import type { Avatar} from '../../types/avatar'
|
||||
import type { Avatar } from '../../types/avatar'
|
||||
import type { Button } from '../../types/button'
|
||||
import { classNames } from '../../utils'
|
||||
import { useAppConfig } from '#imports'
|
||||
@@ -77,11 +81,11 @@ export default defineComponent({
|
||||
default: () => appConfig.ui.notification.default.icon
|
||||
},
|
||||
avatar: {
|
||||
type: Object as PropType<Partial<Avatar>>,
|
||||
type: Object as PropType<Avatar>,
|
||||
default: null
|
||||
},
|
||||
closeButton: {
|
||||
type: Object as PropType<Partial<Button>>,
|
||||
type: Object as PropType<Button>,
|
||||
default: () => appConfig.ui.notification.default.closeButton
|
||||
},
|
||||
timeout: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user