mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-16 04:58:12 +01:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e4230fd63 | ||
|
|
0be676a9ef | ||
|
|
e48b61b5df | ||
|
|
56e0c9a9a0 | ||
|
|
c6841d06a4 | ||
|
|
8508e84958 | ||
|
|
6154ae94a9 | ||
|
|
6384edf92a | ||
|
|
4f3af6cfdb | ||
|
|
893b2466ff | ||
|
|
9807e58f8f | ||
|
|
4124406032 | ||
|
|
3258167a14 | ||
|
|
520624bd64 | ||
|
|
e4b8fffc32 | ||
|
|
5d781112f1 | ||
|
|
1c8122a00b | ||
|
|
0976833753 | ||
|
|
bc00f9c4b2 | ||
|
|
c6aa4215d7 | ||
|
|
3334e2af3d | ||
|
|
3844714644 | ||
|
|
84e6392981 | ||
|
|
c2ef6237d8 | ||
|
|
f735db04d6 | ||
|
|
e8f573b6bb | ||
|
|
288abf239f | ||
|
|
44d93a1cfd | ||
|
|
217840bb41 | ||
|
|
ea2a24b5fe | ||
|
|
4a25a12390 | ||
|
|
d64cb8a6fd | ||
|
|
00d0fd5919 | ||
|
|
30e7a3ca20 | ||
|
|
7d572c81bb | ||
|
|
97a3975197 | ||
|
|
43b999c88e | ||
|
|
7151b7b97d | ||
|
|
ffd20b3991 | ||
|
|
29e64ca963 | ||
|
|
556ee0d9c4 | ||
|
|
debafef0fa | ||
|
|
2d9038bcb0 | ||
|
|
f7f8f06b91 | ||
|
|
56e1fed373 | ||
|
|
648eec31b9 | ||
|
|
d0ce8ee1c4 | ||
|
|
9d8f358139 | ||
|
|
bc6474a9ad | ||
|
|
31924e32f2 | ||
|
|
c963ba688f | ||
|
|
4dee128524 | ||
|
|
4c84839a01 | ||
|
|
fd30022550 | ||
|
|
1a1c640220 | ||
|
|
5c99ae131d | ||
|
|
b22bd70d54 | ||
|
|
0c8ab9d98e | ||
|
|
0fdc8f70b6 | ||
|
|
23770d8cf0 | ||
|
|
84e75ad237 | ||
|
|
00dd8c27bd | ||
|
|
5f81a79edf | ||
|
|
1a02b3abe7 | ||
|
|
83631ccbca | ||
|
|
0f9b5d47a6 | ||
|
|
f623ec1130 | ||
|
|
a79f7c0a34 | ||
|
|
1c9835d7f1 | ||
|
|
6d8d82a265 | ||
|
|
66a80c7486 | ||
|
|
0546c7922c | ||
|
|
eafe707c7d | ||
|
|
5d1919a538 | ||
|
|
781365a5ed | ||
|
|
0129e2db40 | ||
|
|
45b1a4bd32 | ||
|
|
f32f578125 | ||
|
|
4b044866a5 | ||
|
|
9b768ec12b |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,7 +4,7 @@
|
||||
|
||||
### 🔗 Linked issue
|
||||
|
||||
<!-- Please ensure there is an open issue and mention its number as #123 -->
|
||||
<!-- Please ensure there is an open issue and mention its number as: Fixes #123 -->
|
||||
|
||||
### ❓ Type of change
|
||||
|
||||
|
||||
1
.npmrc
1
.npmrc
@@ -1,2 +1,3 @@
|
||||
shamefully-hoist=true
|
||||
auto-install-peers=true
|
||||
ignore-workspace-root-check=true
|
||||
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
@@ -1,5 +1,58 @@
|
||||
# Changelog
|
||||
|
||||
## [2.12.0](https://github.com/nuxt/ui/compare/v2.11.1...v2.12.0) (2024-01-09)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **Card:** remove `overflow-hidden` on wrapper
|
||||
|
||||
### Features
|
||||
|
||||
* **Breadcrumb:** handle `labelClass` and merge `iconClass` ([f623ec1](https://github.com/nuxt/ui/commit/f623ec1130edf448988784b36c15a850470685c4))
|
||||
* **Dropdown:** handle `labelClass` and merge `iconClass` ([1c9835d](https://github.com/nuxt/ui/commit/1c9835d7f149231cf2e3e053e5ea08eceeaaa61d)), closes [#716](https://github.com/nuxt/ui/issues/716)
|
||||
* **Dropdown:** handle manual mode ([3844714](https://github.com/nuxt/ui/commit/38447146445618a1310a6315c608f4cd21069e17)), closes [#1143](https://github.com/nuxt/ui/issues/1143)
|
||||
* **Form:** expose submit function ([#1186](https://github.com/nuxt/ui/issues/1186)) ([4a25a12](https://github.com/nuxt/ui/commit/4a25a12390f8ecae83c1081c89eba99a8fda14f8))
|
||||
* **InputMenu:** new component ([#1095](https://github.com/nuxt/ui/issues/1095)) ([6d8d82a](https://github.com/nuxt/ui/commit/6d8d82a265692aaee556e40b09e4b3048ae044da))
|
||||
* **Pagination:** add `disabled` prop ([0976833](https://github.com/nuxt/ui/commit/0976833753cd2140649bc324f53a263d4e09ecff)), closes [#1189](https://github.com/nuxt/ui/issues/1189)
|
||||
* **Popover:** open and close events ([#1038](https://github.com/nuxt/ui/issues/1038)) ([f32f578](https://github.com/nuxt/ui/commit/f32f578125c12b35e59db2f7981c8b1b5a146397))
|
||||
* **SelectMenu:** add `empty` slot when no options ([5d1919a](https://github.com/nuxt/ui/commit/5d1919a5381b316637d50405d287428f67f2b9cc)), closes [#1089](https://github.com/nuxt/ui/issues/1089)
|
||||
* **SelectMenu:** allow control of search query ([f735db0](https://github.com/nuxt/ui/commit/f735db04d62fca678ca30ecd565b32e70bcda3e0)), closes [#1174](https://github.com/nuxt/ui/issues/1174)
|
||||
* **SelectMenu:** allow creating option despite search ([#1080](https://github.com/nuxt/ui/issues/1080)) ([0fdc8f7](https://github.com/nuxt/ui/commit/0fdc8f70b6a656114d30b07d682e4edcd61a23fb))
|
||||
* **Table:** add `sort-mode` prop ([56e0c9a](https://github.com/nuxt/ui/commit/56e0c9a9a05e1e8491e2d460b8d51084bd2c1305)), closes [#1149](https://github.com/nuxt/ui/issues/1149)
|
||||
* **Table:** add custom sort function to columns ([#1075](https://github.com/nuxt/ui/issues/1075)) ([4f3af6c](https://github.com/nuxt/ui/commit/4f3af6cfdb5213d1be3d2680fcf3a95f7b3bc0b3))
|
||||
* **VerticalNavigation:** ability to add dividers ([#963](https://github.com/nuxt/ui/issues/963)) ([ffd20b3](https://github.com/nuxt/ui/commit/ffd20b3991a35ae7fa0e249fa009e330fd963705))
|
||||
* **VerticalNavigation:** handle `labelClass` and merge `iconClass` ([a79f7c0](https://github.com/nuxt/ui/commit/a79f7c0a34c0414fe4feb95691e1f044b07ef087))
|
||||
* **VerticalNavigation:** improve accessibility ([#948](https://github.com/nuxt/ui/issues/948)) ([29e64ca](https://github.com/nuxt/ui/commit/29e64ca963eeed1e82640957860f43391d8683ed))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Alert:** always pass a function to actions click events ([5d78111](https://github.com/nuxt/ui/commit/5d781112f1eb464658c83047bf80c2ea7c9a2b05)), closes [#1197](https://github.com/nuxt/ui/issues/1197)
|
||||
* **Card:** remove `overflow-hidden` on wrapper ([4124406](https://github.com/nuxt/ui/commit/412440603206151d63b04ffe6bed1bbc5b0e6615)), closes [#806](https://github.com/nuxt/ui/issues/806) [#1034](https://github.com/nuxt/ui/issues/1034)
|
||||
* **config:** prevent class merge of `avatar` size ([b22bd70](https://github.com/nuxt/ui/commit/b22bd70d54e68c3217ba42690210084749fee656))
|
||||
* **Dropdown:** improve placement with `hover` mode ([c6aa421](https://github.com/nuxt/ui/commit/c6aa4215d7f9003adeefa7cdff76c7a88715f20c)), closes [#1179](https://github.com/nuxt/ui/issues/1179)
|
||||
* **Dropdown:** merge item `class` ([7151b7b](https://github.com/nuxt/ui/commit/7151b7b97d42f389506521044ebaffa8a299e7fb)), closes [#1157](https://github.com/nuxt/ui/issues/1157)
|
||||
* **Form:** invalid errors when using `clear` by path ([#1165](https://github.com/nuxt/ui/issues/1165)) ([97a3975](https://github.com/nuxt/ui/commit/97a39751977bf1e942e2bafd5839141383b7af2f))
|
||||
* **Form:** memory leak ([#1185](https://github.com/nuxt/ui/issues/1185)) ([ea2a24b](https://github.com/nuxt/ui/commit/ea2a24b5fe6ddc87e6eb951a662ce8b84b9d987f))
|
||||
* **forms:** dont disable inputs and selects on `loading` ([3258167](https://github.com/nuxt/ui/commit/3258167a1431b664cd1dcc925a4b3fe06a996831)), closes [#1117](https://github.com/nuxt/ui/issues/1117)
|
||||
* **Link:** handle `active` override when value is false ([83631cc](https://github.com/nuxt/ui/commit/83631ccbca1364f012b0c2899f97e2166dd1d360))
|
||||
* **Popover:** allow manual mode without blocking normal behaviour ([3334e2a](https://github.com/nuxt/ui/commit/3334e2af3de2844de08ee530e62f2e4e2fd7ed24))
|
||||
* **Popover:** improve placement with `hover` mode ([bc00f9c](https://github.com/nuxt/ui/commit/bc00f9c4b25dd4b99cb6e53014624f41ee929654)), closes [#781](https://github.com/nuxt/ui/issues/781)
|
||||
* **RadioGroup:** pass `option.disabled` to children ([0c8ab9d](https://github.com/nuxt/ui/commit/0c8ab9d98e494c49cceac111edc0606ee4d63638)), closes [#1109](https://github.com/nuxt/ui/issues/1109)
|
||||
* **SelectMenu:** input border focus after `tailwindcss` 3.4 ([e8f573b](https://github.com/nuxt/ui/commit/e8f573b6bb32a22873d9f93b40883ca12b481d7e))
|
||||
* **Table:** display nothing instead of error when key is missing ([00d0fd5](https://github.com/nuxt/ui/commit/00d0fd59192cc171abb3d2ddaee46b2b9fa9422f)), closes [#1173](https://github.com/nuxt/ui/issues/1173)
|
||||
* **Table:** respect sort prop updates from parent component ([#1208](https://github.com/nuxt/ui/issues/1208)) ([c6841d0](https://github.com/nuxt/ui/commit/c6841d06a48ffef95d238f94a4822a1e48b85422))
|
||||
* **Toggle:** add missing `change` event ([4c84839](https://github.com/nuxt/ui/commit/4c84839a0183756b9f8df8674aace8cd40e44dcd)), closes [#1113](https://github.com/nuxt/ui/issues/1113)
|
||||
* update vue and fix type issues ([#1112](https://github.com/nuxt/ui/issues/1112)) ([5c99ae1](https://github.com/nuxt/ui/commit/5c99ae131d1a50a8db21f1d5794a06080c515831))
|
||||
* **useShortcuts:** include `contenteditable="plaintext-only"` elements in `usingInput` ([#1159](https://github.com/nuxt/ui/issues/1159)) ([648eec3](https://github.com/nuxt/ui/commit/648eec31b99fcffb65c042e0a5587da941c8e90f))
|
||||
* **useShortcuts:** invalid code after [#1159](https://github.com/nuxt/ui/issues/1159) ([56e1fed](https://github.com/nuxt/ui/commit/56e1fed373786fc158ca9da9f02a9ec4e273afce))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "docs: pull `nuxt/ui-pro` docs from `main` branch" ([d0ce8ee](https://github.com/nuxt/ui/commit/d0ce8ee1c4a3d7b2285885d76e02e03168011110))
|
||||
|
||||
## [2.11.1](https://github.com/nuxt/ui/compare/v2.11.0...v2.11.1) (2023-12-11)
|
||||
|
||||
|
||||
|
||||
45
docs/components/ads/AdsCarbon.vue
Normal file
45
docs/components/ads/AdsCarbon.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div ref="carbonads" class="carbon" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const carbonads = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (carbonads.value) {
|
||||
const script = document.createElement('script')
|
||||
script.setAttribute('type', 'text/javascript')
|
||||
script.setAttribute('src', 'https://cdn.carbonads.com/carbon.js?serve=CWYIVK3E&placement=uinuxtcom')
|
||||
script.setAttribute('id', '_carbonads_js')
|
||||
carbonads.value.appendChild(script)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="postcss">
|
||||
.carbon > #carbonads {
|
||||
@apply relative border border-gray-200 dark:border-gray-800 rounded-lg bg-white dark:bg-gray-800/50 hover:border-gray-300 dark:hover:border-gray-700 w-full transition-colors min-h-[220px];
|
||||
|
||||
&:hover {
|
||||
.carbon-text {
|
||||
@apply text-gray-700 dark:text-gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.carbon-img {
|
||||
@apply flex justify-center p-2 w-full;
|
||||
|
||||
& > img {
|
||||
@apply !max-w-full w-full rounded;
|
||||
}
|
||||
}
|
||||
|
||||
.carbon-text {
|
||||
@apply flex px-2 text-sm text-gray-500 dark:text-gray-400 transition-colors text-center w-full;
|
||||
}
|
||||
|
||||
.carbon-poweredby {
|
||||
@apply block text-xs text-center text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 pt-1 pb-2 px-2 transition-colors;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
27
docs/components/ads/AdsPro.vue
Normal file
27
docs/components/ads/AdsPro.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="relative group/ad border border-gray-200 dark:border-gray-800 rounded-lg bg-white dark:bg-gray-800/50 hover:border-gray-300 dark:hover:border-gray-700 p-2 w-full transition-colors">
|
||||
<NuxtLink to="/pro" class="focus:outline-none" tabindex="-1">
|
||||
<span class="absolute inset-0" aria-hidden="true" />
|
||||
</NuxtLink>
|
||||
|
||||
<UColorModeImage
|
||||
light="/pro/illustrations/docs-light.svg"
|
||||
dark="/pro/illustrations/docs-dark.svg"
|
||||
alt="Nuxt UI Pro"
|
||||
loading="lazy"
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col items-center mt-2 text-center">
|
||||
<div class="inline-flex gap-1.5">
|
||||
<Logo class="h-4 w-auto" />
|
||||
|
||||
<UBadge variant="subtle" size="xs" label="Pro" class="font-semibold" />
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 group-hover/ad:text-gray-700 dark:group-hover/ad:text-gray-200 mt-1 transition-colors">
|
||||
The Building Blocks for Modern Web Apps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Field v-bind="prop">
|
||||
<code v-if="prop.default">{{ prop.default }}</code>
|
||||
<code v-if="prop.default" class="leading-6">{{ prop.default }}</code>
|
||||
<p v-if="prop.description">
|
||||
{{ prop.description }}
|
||||
</p>
|
||||
@@ -22,8 +22,8 @@
|
||||
<ComponentPropsField v-for="subProp in Object.values(prop.schema.schema)" :key="(subProp as any).name" :prop="subProp" />
|
||||
</FieldGroup>
|
||||
</Collapsible>
|
||||
<div v-else-if="prop.schema?.kind === 'enum' && prop.schema.type !== 'boolean' && startsWithCapital(prop.schema.type) && !prop.schema.type.startsWith(prop.schema.schema[0])" class="space-x-1 leading-7 -my-1">
|
||||
<code v-for="value in prop.schema.schema.filter(value => typeof value === 'string')" :key="value" class="whitespace-pre-wrap break-words">{{ value }}</code>
|
||||
<div v-else-if="prop.schema?.kind === 'enum' && prop.schema.type !== 'boolean' && startsWithCapital(prop.schema.type) && !prop.schema.type.startsWith(prop.schema.schema[0])" class="flex items-center flex-wrap gap-1 -my-1">
|
||||
<code v-for="value in prop.schema.schema.filter(value => typeof value === 'string')" :key="value" class="whitespace-pre-wrap break-words leading-6">{{ value }}</code>
|
||||
</div>
|
||||
</Field>
|
||||
</template>
|
||||
|
||||
22
docs/components/content/examples/DropdownExampleOpen.vue
Normal file
22
docs/components/content/examples/DropdownExampleOpen.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
const items = [
|
||||
[{
|
||||
label: 'Profile',
|
||||
avatar: {
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
}
|
||||
}]
|
||||
]
|
||||
|
||||
const open = ref(true)
|
||||
|
||||
defineShortcuts({
|
||||
o: () => open.value = !open.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDropdown v-model:open="open" :items="items" :popper="{ placement: 'bottom-start' }">
|
||||
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
</UDropdown>
|
||||
</template>
|
||||
@@ -10,6 +10,7 @@ const options = [
|
||||
|
||||
const state = reactive({
|
||||
input: undefined,
|
||||
inputMenu: undefined,
|
||||
textarea: undefined,
|
||||
select: undefined,
|
||||
selectMenu: undefined,
|
||||
@@ -23,6 +24,9 @@ const state = reactive({
|
||||
|
||||
const schema = z.object({
|
||||
input: z.string().min(10),
|
||||
inputMenu: z.any().refine(option => option?.value === 'option-2', {
|
||||
message: 'Select Option 2'
|
||||
}),
|
||||
textarea: z.string().min(10),
|
||||
select: z.string().refine(value => value === 'option-2', {
|
||||
message: 'Select Option 2'
|
||||
@@ -61,6 +65,10 @@ async function onSubmit (event: FormSubmitEvent<Schema>) {
|
||||
<UInput v-model="state.input" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup name="inputMenu" label="Input Menu">
|
||||
<UInputMenu v-model="state.inputMenu" :options="options" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup name="textarea" label="Textarea">
|
||||
<UTextarea v-model="state.textarea" />
|
||||
</UFormGroup>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" />
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
const people = []
|
||||
|
||||
const selected = ref()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people">
|
||||
<template #empty>
|
||||
No people
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
36
docs/components/content/examples/InputMenuExampleObjects.vue
Normal file
36
docs/components/content/examples/InputMenuExampleObjects.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
const people = [{
|
||||
id: 'benjamincanac',
|
||||
label: 'benjamincanac',
|
||||
href: 'https://github.com/benjamincanac',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
|
||||
}, {
|
||||
id: 'Atinux',
|
||||
label: 'Atinux',
|
||||
href: 'https://github.com/Atinux',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
|
||||
}, {
|
||||
id: 'smarroufin',
|
||||
label: 'smarroufin',
|
||||
href: 'https://github.com/smarroufin',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' }
|
||||
}, {
|
||||
id: 'nobody',
|
||||
label: 'Nobody',
|
||||
icon: 'i-heroicons-user-circle'
|
||||
}]
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people">
|
||||
<template #leading>
|
||||
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4 mx-0.5" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" class="mx-0.5" />
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<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].name)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
v-model="selected"
|
||||
:options="people"
|
||||
value-attribute="name"
|
||||
option-attribute="name"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" searchable>
|
||||
<template #option-empty="{ query }">
|
||||
<q>{{ query }}</q> not found
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
const people = [
|
||||
{ name: 'Wade Cooper', online: true },
|
||||
{ name: 'Arlene Mccoy', online: false },
|
||||
{ name: 'Devon Webb', online: false },
|
||||
{ name: 'Tom Cook', online: true },
|
||||
{ name: 'Tanya Fox', online: false },
|
||||
{ name: 'Hellen Schmidt', online: true },
|
||||
{ name: 'Caroline Schultz', online: true },
|
||||
{ name: 'Mason Heaney', online: false },
|
||||
{ name: 'Claudie Smitham', online: true },
|
||||
{ name: 'Emil Schaefer', online: false }
|
||||
]
|
||||
|
||||
const selected = ref(people[3])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" option-attribute="name">
|
||||
<template #option="{ option: person }">
|
||||
<span :class="[person.online ? 'bg-green-400' : 'bg-gray-200', 'inline-block h-2 w-2 flex-shrink-0 rounded-full']" aria-hidden="true" />
|
||||
<span class="truncate">{{ person.name }}</span>
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" :popper="{ arrow: true }" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" :popper="{ offsetDistance: 0 }" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" :popper="{ placement: 'right-start' }" />
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
const loading = ref(false)
|
||||
const selected = ref()
|
||||
|
||||
async function search (q) {
|
||||
loading.value = true
|
||||
|
||||
const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
|
||||
|
||||
loading.value = false
|
||||
|
||||
return users
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
v-model="selected"
|
||||
:search="search"
|
||||
:loading="loading"
|
||||
placeholder="Search for a user..."
|
||||
option-attribute="name"
|
||||
trailing
|
||||
by="id"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
const options = [
|
||||
{ id: 1, name: 'Wade Cooper', colors: ['red', 'yellow'] },
|
||||
{ id: 2, name: 'Arlene Mccoy', colors: ['blue', 'yellow'] },
|
||||
{ id: 3, name: 'Devon Webb', colors: ['green', 'blue'] },
|
||||
{ id: 4, name: 'Tom Cook', colors: ['blue', 'red'] },
|
||||
{ id: 5, name: 'Tanya Fox', colors: ['green', 'red'] },
|
||||
{ id: 5, name: 'Hellen Schmidt', colors: ['green', 'yellow'] }
|
||||
]
|
||||
|
||||
const selected = ref(options[1])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
v-model="selected"
|
||||
:options="options"
|
||||
placeholder="Select a person"
|
||||
by="id"
|
||||
option-attribute="name"
|
||||
:search-attributes="['name', 'colors']"
|
||||
>
|
||||
<template #option="{ option: person }">
|
||||
<span v-for="color in person.colors" :key="color.id" class="h-2 w-2 rounded-full" :class="`bg-${color}-500 dark:bg-${color}-400`" />
|
||||
<span class="truncate">{{ person.name }}</span>
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref()
|
||||
const query = ref('Wade')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
v-model="selected"
|
||||
v-model:query="query"
|
||||
:options="people"
|
||||
placeholder="Select a person"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,18 +1,19 @@
|
||||
<script setup>
|
||||
const open = ref(false)
|
||||
const open = ref(true)
|
||||
|
||||
defineShortcuts({
|
||||
o: () => open.value = !open.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-4 items-center">
|
||||
<UToggle v-model="open" />
|
||||
<UPopover :open="open">
|
||||
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
<UPopover v-model:open="open">
|
||||
<UButton color="white" :label="open.toString()" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
|
||||
<template #panel>
|
||||
<div class="p-4">
|
||||
<Placeholder class="h-20 w-48" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
<template #panel>
|
||||
<div class="p-4">
|
||||
<Placeholder class="h-20 w-48" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<template>
|
||||
<div class="flex gap-4 items-center">
|
||||
<UPopover overlay>
|
||||
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
<UPopover overlay>
|
||||
<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>
|
||||
</div>
|
||||
<template #panel>
|
||||
<div class="p-4">
|
||||
<Placeholder class="h-20 w-48" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
|
||||
@@ -23,6 +23,7 @@ const labels = computed({
|
||||
|
||||
// In a real app, you would make an API call to create the label
|
||||
const response = {
|
||||
id: options.value.length + 1,
|
||||
name: label.name,
|
||||
color: generateColorFromString(label.name)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
const options = ref([
|
||||
{ id: 1, name: 'bug' },
|
||||
{ id: 2, name: 'documentation' },
|
||||
{ id: 3, name: 'duplicate' },
|
||||
{ id: 4, name: 'enhancement' },
|
||||
{ id: 5, name: 'good first issue' },
|
||||
{ id: 6, name: 'help wanted' },
|
||||
{ id: 7, name: 'invalid' },
|
||||
{ id: 8, name: 'question' },
|
||||
{ id: 9, name: 'wontfix' }
|
||||
])
|
||||
|
||||
const selected = ref([])
|
||||
|
||||
const labels = computed({
|
||||
get: () => selected.value,
|
||||
set: async (labels) => {
|
||||
const promises = labels.map(async (label) => {
|
||||
if (label.id) {
|
||||
return label
|
||||
}
|
||||
|
||||
// In a real app, you would make an API call to create the label
|
||||
const response = {
|
||||
id: options.value.length + 1,
|
||||
name: label.name
|
||||
}
|
||||
|
||||
options.value.push(response)
|
||||
|
||||
return response
|
||||
})
|
||||
|
||||
selected.value = await Promise.all(promises)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
v-model="labels"
|
||||
by="id"
|
||||
name="labels"
|
||||
:options="options"
|
||||
option-attribute="name"
|
||||
multiple
|
||||
searchable
|
||||
creatable
|
||||
show-create-option-when="always"
|
||||
placeholder="Select labels"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
const people = []
|
||||
|
||||
const selected = ref()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu v-model="selected" :options="people">
|
||||
<template #empty>
|
||||
No people
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
@@ -28,11 +28,9 @@ const selected = ref(people[0])
|
||||
|
||||
<template>
|
||||
<USelectMenu v-model="selected" :options="people">
|
||||
<template #label>
|
||||
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" />
|
||||
|
||||
{{ selected.label }}
|
||||
<template #leading>
|
||||
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4 mx-0.5" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" class="mx-0.5" />
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
|
||||
@@ -13,9 +13,7 @@ const people = [{
|
||||
name: 'Tom Cook'
|
||||
}]
|
||||
|
||||
const selected = ref(people[0].id)
|
||||
|
||||
const current = computed(() => people.find(person => person.id === selected.value))
|
||||
const selected = ref(people[0].name)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -23,11 +21,7 @@ const current = computed(() => people.find(person => person.id === selected.valu
|
||||
v-model="selected"
|
||||
:options="people"
|
||||
placeholder="Select people"
|
||||
value-attribute="id"
|
||||
value-attribute="name"
|
||||
option-attribute="name"
|
||||
>
|
||||
<template #label>
|
||||
{{ current.name }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
<script setup>
|
||||
const search = async (q) => {
|
||||
const loading = ref(false)
|
||||
const selected = ref([])
|
||||
|
||||
async function search (q) {
|
||||
loading.value = true
|
||||
|
||||
const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
|
||||
|
||||
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email })).filter(Boolean)
|
||||
}
|
||||
loading.value = false
|
||||
|
||||
const selected = ref([])
|
||||
return users
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
v-model="selected"
|
||||
:loading="loading"
|
||||
:searchable="search"
|
||||
placeholder="Search for a user..."
|
||||
option-attribute="name"
|
||||
multiple
|
||||
trailing
|
||||
by="id"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
const options = [
|
||||
{ id: 1, name: 'Wade Cooper', favoriteColors: ['red', 'yellow'] },
|
||||
{ id: 2, name: 'Arlene Mccoy', favoriteColors: ['blue', 'yellow'] },
|
||||
{ id: 3, name: 'Devon Webb', favoriteColors: ['green', 'blue'] },
|
||||
{ id: 4, name: 'Tom Cook', favoriteColors: ['blue', 'red'] },
|
||||
{ id: 5, name: 'Tanya Fox', favoriteColors: ['green', 'red'] },
|
||||
{ id: 5, name: 'Hellen Schmidt', favoriteColors: ['green', 'yellow'] }
|
||||
{ id: 1, name: 'Wade Cooper', colors: ['red', 'yellow'] },
|
||||
{ id: 2, name: 'Arlene Mccoy', colors: ['blue', 'yellow'] },
|
||||
{ id: 3, name: 'Devon Webb', colors: ['green', 'blue'] },
|
||||
{ id: 4, name: 'Tom Cook', colors: ['blue', 'red'] },
|
||||
{ id: 5, name: 'Tanya Fox', colors: ['green', 'red'] },
|
||||
{ id: 5, name: 'Hellen Schmidt', colors: ['green', 'yellow'] }
|
||||
]
|
||||
|
||||
const selected = ref(options[1])
|
||||
@@ -15,16 +15,15 @@ const selected = ref(options[1])
|
||||
<USelectMenu
|
||||
v-model="selected"
|
||||
:options="options"
|
||||
class="w-full lg:w-96"
|
||||
placeholder="Select an user"
|
||||
placeholder="Select a person"
|
||||
searchable
|
||||
searchable-placeholder="Search by name or favorite colors"
|
||||
searchable-placeholder="Search by name or color"
|
||||
option-attribute="name"
|
||||
by="id"
|
||||
:search-attributes="['name', 'favoriteColors']"
|
||||
:search-attributes="['name', 'colors']"
|
||||
>
|
||||
<template #option="{ option: person }">
|
||||
<span v-for="color in person.favoriteColors" :key="color.id" class="h-2 w-2 rounded-full" :class="`bg-${color}-500 dark:bg-${color}-400`" />
|
||||
<span v-for="color in person.colors" :key="color.id" class="h-2 w-2 rounded-full" :class="`bg-${color}-500 dark:bg-${color}-400`" />
|
||||
<span class="truncate">{{ person.name }}</span>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref()
|
||||
const query = ref('Wade')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
v-model="selected"
|
||||
v-model:query="query"
|
||||
:options="people"
|
||||
placeholder="Select a person"
|
||||
searchable
|
||||
/>
|
||||
</template>
|
||||
@@ -77,6 +77,7 @@ const resetFilters = () => {
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const sort = ref({ column: 'id', direction: 'asc' as const })
|
||||
const page = ref(1)
|
||||
const pageCount = ref(10)
|
||||
const pageTotal = ref(200) // This value should be dynamic coming from the API
|
||||
@@ -92,11 +93,13 @@ const { data: todos, pending } = await useLazyAsyncData<{
|
||||
query: {
|
||||
q: search.value,
|
||||
'_page': page.value,
|
||||
'_limit': pageCount.value
|
||||
'_limit': pageCount.value,
|
||||
'_sort': sort.value.column,
|
||||
'_order': sort.value.direction
|
||||
}
|
||||
}), {
|
||||
default: () => [],
|
||||
watch: [page, search, searchStatus, pageCount]
|
||||
watch: [page, search, searchStatus, pageCount, sort]
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -175,11 +178,13 @@ const { data: todos, pending } = await useLazyAsyncData<{
|
||||
<!-- Table -->
|
||||
<UTable
|
||||
v-model="selectedRows"
|
||||
v-model:sort="sort"
|
||||
:rows="todos"
|
||||
:columns="columnsTable"
|
||||
:loading="pending"
|
||||
sort-asc-icon="i-heroicons-arrow-up"
|
||||
sort-desc-icon="i-heroicons-arrow-down"
|
||||
sort-mode="manual"
|
||||
class="w-full"
|
||||
:ui="{ td: { base: 'max-w-[0] truncate' } }"
|
||||
@select="select"
|
||||
|
||||
@@ -60,5 +60,5 @@ const people = [{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :columns="columns" :rows="people" :sort="{ column: 'title' }" />
|
||||
<UTable :columns="columns" :rows="people" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup>
|
||||
const links = [
|
||||
[
|
||||
{
|
||||
label: 'Profile',
|
||||
avatar: {
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
},
|
||||
badge: 100
|
||||
}, {
|
||||
label: 'Installation',
|
||||
icon: 'i-heroicons-home',
|
||||
to: '/getting-started/installation'
|
||||
}, {
|
||||
label: 'Vertical Navigation',
|
||||
icon: 'i-heroicons-chart-bar',
|
||||
to: '/navigation/vertical-navigation'
|
||||
}, {
|
||||
label: 'Command Palette',
|
||||
icon: 'i-heroicons-command-line',
|
||||
to: '/navigation/command-palette'
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Examples',
|
||||
icon: 'i-heroicons-light-bulb',
|
||||
to: '/getting-started/examples#verticalnavigation'
|
||||
},
|
||||
{
|
||||
label: 'Help',
|
||||
icon: 'i-heroicons-question-mark-circle',
|
||||
to: '/getting-started/examples'
|
||||
}
|
||||
]
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation :links="links" />
|
||||
</template>
|
||||
@@ -6,7 +6,7 @@
|
||||
<NuxtLink v-if="date.release" :to="`https://github.com/nuxt/ui/releases/tag/${date.release.name}`" target="_blank" class="text-gray-900 dark:text-white font-bold text-3xl mt-2 group hover:text-primary-500 dark:hover:text-primary-400 transition-[color]">
|
||||
{{ date.release.name }}
|
||||
</NuxtLink>
|
||||
<ul v-else-if="date.pulls?.length" class="mt-2 space-y-1 text-gray-600 dark:text-gray-300">
|
||||
<ul v-if="date.pulls?.length" class="mt-2 space-y-1 text-gray-600 dark:text-gray-300">
|
||||
<li v-for="pull in date.pulls" :key="pull.id" class="text-sm/6 break-all">
|
||||
<NuxtLink :to="`https://github.com/${pull.user.login}`" target="_blank" class="text-gray-900 dark:text-white transition-colors inline-flex items-center gap-1 rounded-full bg-gray-100/50 dark:bg-gray-800/50 dark:hover:bg-gray-800 p-0.5 pr-1 ring-1 ring-gray-300 dark:ring-gray-700 text-xs font-medium flex-shrink-0 align-middle">
|
||||
<UAvatar :src="`https://github.com/${pull.user.login}.png`" size="3xs" />
|
||||
|
||||
@@ -20,6 +20,10 @@ yarn add @nuxt/ui
|
||||
npm install @nuxt/ui
|
||||
```
|
||||
|
||||
```bash [bun]
|
||||
bun add @nuxt/ui
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
2. Add it to your `modules` section in your `nuxt.config`:
|
||||
@@ -32,8 +36,114 @@ export default defineNuxtConfig({
|
||||
|
||||
That's it! You can now use all the components and composables in your Nuxt app ✨
|
||||
|
||||
## Modules
|
||||
|
||||
Nuxt UI will automatically install the [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/), [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) and [nuxt-icon](https://github.com/nuxt-modules/icon) modules for you.
|
||||
|
||||
::callout{icon="i-heroicons-exclamation-triangle"}
|
||||
As this module installs [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/) and [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) for you, you should remove them from your `modules` and `dependencies` if you've previously installed them manually.
|
||||
You should remove them from your `modules` and `dependencies` if you've previously installed them.
|
||||
::
|
||||
|
||||
### `@nuxtjs/tailwindcs`
|
||||
|
||||
This module is pre-configured and will automatically load the following plugins:
|
||||
|
||||
- [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms)
|
||||
- [@tailwindcss/typography](https://github.com/tailwindlabs/tailwindcss-typography)
|
||||
- [@tailwindcss/aspect-ratio](https://github.com/tailwindlabs/tailwindcss-aspect-ratio)
|
||||
- [@tailwindcss/container-queries](https://github.com/tailwindlabs/tailwindcss-container-queries)
|
||||
- [@headlessui/tailwindcss](https://github.com/tailwindlabs/headlessui/tree/main/packages/%40headlessui-vue)
|
||||
|
||||
Note that the `@tailwindcss/aspect-ratio` plugin disables the default aspect ratio utilities:
|
||||
|
||||
- `aspect-auto`
|
||||
- `aspect-square`
|
||||
- `aspect-video`
|
||||
|
||||
You can re-enable them by adding the following to your `tailwind.config.ts`:
|
||||
|
||||
```ts [tailwind.config.ts]
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
export default <Partial<Config>>{
|
||||
theme: {
|
||||
extend: {
|
||||
aspectRatio: {
|
||||
auto: 'auto',
|
||||
square: '1 / 1',
|
||||
video: '16 / 9'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `@nuxtjs/color-mode`
|
||||
|
||||
This module is installed to provide dark mode support out of the box thanks to the Tailwind CSS dark mode `class` strategy.
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
You can read more about this in the [Theming](/getting-started/theming#dark-mode) section.
|
||||
::
|
||||
|
||||
### `nuxt-icon`
|
||||
|
||||
This module is installed when using the `dynamic` prop on the `Icon` component or globally through the `ui.icons.dynamic` option in your `app.config.ts`.
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
You can read more about this in the [Theming](/getting-started/theming#dynamic-icons) section and on the [Icon](/elements/icon) component page.
|
||||
::
|
||||
|
||||
## TypeScript
|
||||
|
||||
This module is written in TypeScript and provides typings for all the components and composables. You can look at the [source code](https://github.com/nuxt/ui/tree/dev/src/runtime/types) to see all the available types.
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb" to="https://nuxt.com/docs/guide/concepts/typescript" target="_blank"}
|
||||
You can read more about TypeScript on the official Nuxt documentation.
|
||||
::
|
||||
|
||||
You can use those types in your own components by importing them from `#ui/types`, for example when defining wrapper components:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<UBreadcrumb :links="links">
|
||||
<template #icon="{ link }">
|
||||
<UIcon :name="link.icon" />
|
||||
</template>
|
||||
</UBreadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { BreadcrumbLink } from '#ui/types'
|
||||
|
||||
export interface Props {
|
||||
links: BreadcrumbLink[]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
```
|
||||
|
||||
You don't have to use TypeScript yourself, but doing so will give you access to prop validation and autocomplete.
|
||||
|
||||
We've managed to provide dynamic typings on props such as `color`, `size`, `variant`, etc. based on your custom config. For example, you'll be suggested the `custom` color and the `subtle` variant when using the `Button` component with an `app.config.ts` as such:
|
||||
|
||||
```ts [app.config.ts]
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
button: {
|
||||
color: {
|
||||
custom: {
|
||||
subtle: '...'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
You can read more about components configuration in the [Theming](/getting-started/theming#appconfigts) section.
|
||||
::
|
||||
|
||||
## IntelliSense
|
||||
|
||||
@@ -281,12 +281,14 @@ export default defineNuxtConfig({
|
||||
Search the icon you want to use on https://icones.js.org built by [@antfu](https://github.com/antfu).
|
||||
::
|
||||
|
||||
Unlike the official [nuxt-icon](https://github.com/nuxt-modules/icon/) module, this module will not fetch any icon from the web and will only bundle the icons you use in your app thanks to [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons).
|
||||
|
||||
However, you will need to install either `@iconify/json` (full icon collections, 50MB) or the individual icon packages you want to use in your app.
|
||||
Thanks to [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons) plugin, only the icons you use in your app will be bundled in your CSS. However, you need to install the icon collections you specified in the `ui.icons` key:
|
||||
|
||||
::code-group
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm i @iconify-json/{collection_name}
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add @iconify-json/{collection_name}
|
||||
```
|
||||
@@ -295,25 +297,21 @@ yarn add @iconify-json/{collection_name}
|
||||
npm install @iconify-json/{collection_name}
|
||||
```
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm i @iconify-json/{collection_name}
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
When using `@iconify/json`, you can specifiy `icons: 'all'` in your `nuxt.config.ts` to use any icon in your app.
|
||||
If you choose to use the full `@iconify/json` icon collection (50MB), you can specifiy `icons: 'all'` or `icons: {}` in your `nuxt.config.ts` to use any icon in your app.
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
ui: {
|
||||
icons: 'all'
|
||||
icons: {}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Custom config
|
||||
|
||||
If you have specific needs, like using a custom icon collection, you can use the `icons` option in your `nuxt.config.ts` as an object to override the config of the [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
|
||||
If you have specific needs, like using a custom icon collection, you can use the `icons` option in your `nuxt.config.ts` as an object to override the config of the [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
import { getIconCollections } from '@egoist/tailwindcss-icons'
|
||||
@@ -345,7 +343,13 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
### Dynamic icons
|
||||
|
||||
The `Icon` component also has a `dynamic` prop to use the [nuxt-icon](https://github.com/nuxt-modules/icon/) module instead of the [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
|
||||
|
||||
Read more about this in the [Icon](/elements/icon#dynamic) component page.
|
||||
|
||||
### Defaults
|
||||
|
||||
You can easily replace all the default icons of the components in your `app.config.ts`.
|
||||
|
||||
|
||||
@@ -14,12 +14,14 @@ links:
|
||||
Pass an array of arrays to the `items` prop of the Dropdown component. Each array represents a group of items. Each item can have the following properties:
|
||||
|
||||
- `label` - The label of the item.
|
||||
- `labelClass` - The class of the item label. :u-badge{label="New" class="!rounded-full" variant="subtle"}
|
||||
- `icon` - The icon of the item.
|
||||
- `iconClass` - The class of the icon of the item.
|
||||
- `iconClass` - The class of the item icon.
|
||||
- `avatar` - The avatar of the item. You can pass all the props of the [Avatar](/elements/avatar) component.
|
||||
- `shortcuts` - The shortcuts of the item.
|
||||
- `slot` - The slot of the item.
|
||||
- `disabled` - Whether the item is disabled.
|
||||
- `class` - The class of the item.
|
||||
- `click` - The click handler of the item.
|
||||
|
||||
You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
|
||||
@@ -32,6 +34,12 @@ Use the `mode` prop to switch between `click` and `hover` modes.
|
||||
|
||||
:component-example{component="dropdown-example-mode"}
|
||||
|
||||
### Manual :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use a `v-model:open` to manually control the state. In this example, press :shortcut{value="O"} to toggle the dropdown.
|
||||
|
||||
:component-example{component="dropdown-example-open"}
|
||||
|
||||
## Popper
|
||||
|
||||
Use the `popper` prop to customize the popper instance.
|
||||
|
||||
@@ -172,13 +172,13 @@ Use the `#leading` slot to set the content of the leading icon.
|
||||
::component-card
|
||||
---
|
||||
slots:
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5" />
|
||||
baseProps:
|
||||
placeholder: 'Search...'
|
||||
---
|
||||
|
||||
#leading
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5"}
|
||||
::
|
||||
|
||||
### `trailing`
|
||||
|
||||
@@ -203,6 +203,9 @@ componentProps:
|
||||
## API
|
||||
|
||||
::field-group
|
||||
::field{name="submit ()" type="Promise<void>"}
|
||||
Triggers form submission.
|
||||
::
|
||||
::field{name="validate (path?: string, opts: { silent?: boolean })" type="Promise<T>"}
|
||||
Triggers form validation. Will raise any errors unless `opts.silent` is set to true.
|
||||
::
|
||||
@@ -217,5 +220,5 @@ componentProps:
|
||||
::
|
||||
::field{name="errors" type="Ref<FormError[]>"}
|
||||
A reference to the array containing validation errors. Use this to access or manipulate the error information.
|
||||
::
|
||||
::
|
||||
::
|
||||
|
||||
210
docs/content/3.forms/2.input-menu.md
Normal file
210
docs/content/3.forms/2.input-menu.md
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
title: InputMenu
|
||||
description: Display an autocomplete input with real-time suggestions.
|
||||
links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/InputMenu.vue
|
||||
- label: 'Combobox'
|
||||
icon: i-simple-icons-headlessui
|
||||
to: 'https://headlessui.com/vue/combobox'
|
||||
navigation:
|
||||
badge: New
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
The `InputMenu` component renders by default an [Input](/forms/input) component and is based on the `ui.input` preset. You can use most of the `Input` props to configure the display such as [color](/forms/input#style), [variant](/forms/input#style), [size](/forms/input#size), [placeholder](/forms/input#placeholder), [icon](/forms/input#icon), [disabled](/forms/input#disabled), etc.
|
||||
|
||||
You can use the `ui` prop like the `Input` component to override the default config. The `uiMenu` prop can be used to override the default menu config.
|
||||
|
||||
Pass an array of strings or objects to the `options` prop to display in the menu.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-basic'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-exclamation-triangle"}
|
||||
This component does not support multiple values. Use the [SelectMenu](/forms/select-menu#multiple) component instead.
|
||||
::
|
||||
|
||||
### Objects
|
||||
|
||||
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `option-attribute` prop that defaults to `label`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-objects'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
Use the `search-attributes` prop with an array of property names to search on each option object. Nested attributes can be accessed using `dot.notation`. When the property value is an array or object, these are cast to string so these can be searched within.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-search-attributes'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
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`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-objects-value-attribute'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### Icon
|
||||
|
||||
The `InputMenu` has a button on the right to toggle the menu. Use the `trailing-icon` prop to set a different icon or change it globally in `ui.inputMenu.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
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:
|
||||
trailingIcon: 'i-heroicons-chevron-up-down-20-solid'
|
||||
excludedProps:
|
||||
- trailingIcon
|
||||
---
|
||||
::
|
||||
|
||||
Use the `selected-icon` prop to set a different icon or change it globally in `ui.inputMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
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:
|
||||
selectedIcon: 'i-heroicons-hand-thumb-up-solid'
|
||||
excludedProps:
|
||||
- selectedIcon
|
||||
---
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
Learn how to customize icons from the [Input](/forms/input#icon) component.
|
||||
::
|
||||
|
||||
## Searchable
|
||||
|
||||
### Attributes
|
||||
|
||||
Use the `search-attributes` prop with an array of property names to search on each option object. Nested attributes can be accessed using `dot.notation`. When the property value is an array or object, these are cast to string so these can be searched within.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-search-attributes'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### Control the query
|
||||
|
||||
Use a `v-model:query` to control the search query.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-search-query'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### Async search
|
||||
|
||||
Pass a function to the `search` prop to customize the search behavior and filter options according to your needs. The function will receive the query as its first argument and should return an array.
|
||||
|
||||
Use the `debounce` prop to adjust the delay of the function.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-search-async'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
## Popper
|
||||
|
||||
Use the `popper` prop to customize the popper instance.
|
||||
|
||||
### Arrow
|
||||
|
||||
:component-example{component="input-menu-example-popper-arrow"}
|
||||
|
||||
### Placement
|
||||
|
||||
:component-example{component="input-menu-example-popper-placement"}
|
||||
|
||||
### Offset
|
||||
|
||||
:component-example{component="input-menu-example-popper-offset"}
|
||||
|
||||
## Slots
|
||||
|
||||
### `option`
|
||||
|
||||
Use the `#option` slot to customize the option content. You will have access to the `option`, `active` and `selected` properties in the slot scope.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-option-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### `option-empty`
|
||||
|
||||
Use the `#option-empty` slot to customize the content displayed when the `searchable` prop is `true` and there is no options. You will have access to the `query` property in the slot scope.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-option-empty-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### `empty`
|
||||
|
||||
Use the `#empty` slot to customize the content displayed when there is no options. Defaults to `No options.`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-empty-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Config
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
Use the `ui` prop to override the input config and the `uiMenu` prop to override the menu config.
|
||||
::
|
||||
|
||||
::tabs{:selectedIndex="1"}
|
||||
:component-preset{label="Input (ui)" slug="Input"}
|
||||
:component-preset{label="InputMenu (uiMenu)"}
|
||||
::
|
||||
@@ -203,7 +203,7 @@ Use the `#leading` slot to set the content of the leading icon.
|
||||
::component-card
|
||||
---
|
||||
slots:
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5" />
|
||||
baseProps:
|
||||
options:
|
||||
- 'United States'
|
||||
@@ -213,7 +213,7 @@ baseProps:
|
||||
---
|
||||
|
||||
#leading
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5"}
|
||||
::
|
||||
|
||||
### `trailing`
|
||||
|
||||
@@ -22,7 +22,7 @@ Like the `Select` component, you can use the `options` prop to pass an array of
|
||||
---
|
||||
component: 'select-menu-example-basic'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -34,7 +34,7 @@ You can use the `multiple` prop to select multiple values.
|
||||
---
|
||||
component: 'select-menu-example-multiple'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -46,7 +46,7 @@ You can pass an array of objects to `options` and either compare on the whole ob
|
||||
---
|
||||
component: 'select-menu-example-objects'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -56,7 +56,7 @@ If you only want to select a single object property rather than the whole object
|
||||
---
|
||||
component: 'select-menu-example-objects-value-attribute'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -67,7 +67,7 @@ Use the `selected-icon` prop to set a different icon or change it globally in `u
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'w-full lg:w-40'
|
||||
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:
|
||||
@@ -81,7 +81,7 @@ excludedProps:
|
||||
Learn how to customize icons from the [Select](/forms/select#icon) component.
|
||||
::
|
||||
|
||||
### Search
|
||||
## Searchable
|
||||
|
||||
Use the `searchable` prop to enable search.
|
||||
|
||||
@@ -92,7 +92,7 @@ This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) compon
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'w-full lg:w-40'
|
||||
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:
|
||||
@@ -101,28 +101,28 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
#### Search Attributes
|
||||
### Attributes
|
||||
|
||||
Use the `search-attributes` with an array of property names to search on each option object.
|
||||
|
||||
Nested attributes can be accessed using `dot.notation`. When the property value is an array or object, these are cast to string so these can be searched within.
|
||||
Use the `search-attributes` prop with an array of property names to search on each option object. Nested attributes can be accessed using `dot.notation`. When the property value is an array or object, these are cast to string so these can be searched within.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-search-attributes'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-96'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
#### Clear on close
|
||||
### Clear on close
|
||||
|
||||
By default, the search query will be kept after the menu is closed. To clear it on close, set the `clear-search-on-close` prop.
|
||||
|
||||
You can also configure this globally through the `ui.selectMenu.default.clearSearchOnClose` config. Defaults to `false`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
placeholder: 'Select a person'
|
||||
searchable: true
|
||||
searchablePlaceholder: 'Search a person...'
|
||||
@@ -132,6 +132,18 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Control the query :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use a `v-model:query` to control the search query.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-search-query'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### Async search
|
||||
|
||||
Pass a function to the `searchable` prop to customize the search behavior and filter options according to your needs. The function will receive the query as its first argument and should return an array.
|
||||
@@ -140,13 +152,13 @@ Use the `debounce` prop to adjust the delay of the function.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-async-search'
|
||||
component: 'select-menu-example-search-async'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### Create option
|
||||
## Creatable
|
||||
|
||||
Use the `creatable` prop to enable the creation of new options when the search doesn't return any results (only works with `searchable`).
|
||||
|
||||
@@ -156,7 +168,21 @@ Try to search for something that doesn't exist in the example below.
|
||||
---
|
||||
component: 'select-menu-example-creatable'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
However, if you want to create options despite search query (apart from exact match), you can set the `show-create-option-when` prop to `'always'`.
|
||||
|
||||
You can also configure this globally through the `ui.selectMenu.default.showCreateOptionWhen` config. Defaults to `empty`.
|
||||
|
||||
Try to search for something that exists in the example below, but not an exact match.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-creatable-always'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -186,7 +212,7 @@ You can override the `#label` slot and handle the display yourself.
|
||||
---
|
||||
component: 'select-menu-example-multiple-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -198,7 +224,7 @@ You can also override the `#default` slot entirely.
|
||||
---
|
||||
component: 'select-menu-example-button'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -210,7 +236,7 @@ Use the `#option` slot to customize the option content. You will have access to
|
||||
---
|
||||
component: 'select-menu-example-option-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -222,7 +248,7 @@ Use the `#option-empty` slot to customize the content displayed when the `search
|
||||
---
|
||||
component: 'select-menu-example-option-empty-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -234,6 +260,18 @@ Use the `#option-create` slot to customize the content displayed when the `creat
|
||||
An example is available in the [Create option](#create-option) section.
|
||||
::
|
||||
|
||||
### `empty` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `#empty` slot to customize the content displayed when there is no options. Defaults to `No options.`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-empty-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
@@ -248,5 +286,3 @@ Use the `ui` prop to override the select config and the `uiMenu` prop to overrid
|
||||
:component-preset{label="Select (ui)" slug="Select"}
|
||||
:component-preset{label="SelectMenu (uiMenu)"}
|
||||
::
|
||||
|
||||
:component-preset
|
||||
|
||||
@@ -60,7 +60,7 @@ Use the `disabled` prop to disable the RadioGroup.
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
options: [{ value: 'email', label: 'Email' }, { value: 'sms', label: 'Phone (SMS)' }, { value: 'push', label: 'Push notification' }]
|
||||
options: [{ value: 'email', label: 'Email' }, { value: 'sms', label: 'Phone (SMS)' }, { value: 'push', label: 'Push notification', disabled: true }]
|
||||
modelValue: 'sms'
|
||||
props:
|
||||
disabled: true
|
||||
@@ -68,7 +68,7 @@ props:
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
This prop also work on the Radio component.
|
||||
This prop also work on the Radio component and you can set the `disabled` field in the `options` to disable a specific Radio.
|
||||
::
|
||||
|
||||
### Label
|
||||
|
||||
@@ -6,6 +6,10 @@ links:
|
||||
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/data/Table.vue
|
||||
---
|
||||
|
||||
::callout{icon="i-heroicons-puzzle-piece" to="/getting-started/examples#table"}
|
||||
Check out an example of a Table with advanced features like sorting, pagination, search, etc.
|
||||
::
|
||||
|
||||
## Usage
|
||||
|
||||
Use the `rows` prop to set the data to display in the table. By default, the table will display all the fields of the rows.
|
||||
@@ -28,6 +32,7 @@ Use the `columns` prop to configure which columns to display. It's an array of o
|
||||
- `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.
|
||||
- `sort` - Pass your own `sort` function. Defaults to a simple _greater than_ / _less than_ comparison.
|
||||
|
||||
::component-example{class="grid"}
|
||||
---
|
||||
@@ -53,6 +58,8 @@ componentProps:
|
||||
|
||||
You can make the columns sortable by setting the `sortable` property to `true` in the column configuration.
|
||||
|
||||
You may specify the default direction of each column through the `direction` property. It can be either `asc` or `desc`, but it will default to `asc`.
|
||||
|
||||
::component-example{class="grid"}
|
||||
---
|
||||
padding: false
|
||||
@@ -62,17 +69,84 @@ componentProps:
|
||||
---
|
||||
::
|
||||
|
||||
You may specify the default direction of each column through the `direction` property. It can be either `asc` or `desc`, but it will default to `asc`.
|
||||
#### Default sorting
|
||||
|
||||
You can specify a default sort for the table through the `sort` prop. It's an object with the following properties:
|
||||
|
||||
- `column` - The column to sort by.
|
||||
- `direction` - The sort direction. Can be either `asc` or `desc` and defaults to `asc`.
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
This will set the default sort and will work even if no column is set as `sortable`.
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const sort = ref({
|
||||
column: 'name',
|
||||
direction: 'desc'
|
||||
})
|
||||
|
||||
const columns = [{
|
||||
label: 'Name',
|
||||
key: 'name',
|
||||
sortable: true
|
||||
}]
|
||||
|
||||
const people = [{
|
||||
id: 1,
|
||||
name: 'Lindsay Walton',
|
||||
title: 'Front-end Developer',
|
||||
email: 'lindsay.walton@example.com',
|
||||
role: 'Member'
|
||||
}, {
|
||||
id: 2,
|
||||
name: 'Courtney Henry',
|
||||
title: 'Designer',
|
||||
email: 'courtney.henry@example.com',
|
||||
role: 'Admin'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :sort="sort" :columns="columns" :rows="people" />
|
||||
</template>
|
||||
```
|
||||
|
||||
#### Reactive sorting
|
||||
|
||||
You can use a `v-model:sort` to make the sorting reactive. You may also use `@update:sort` to call your own function with the sorting data.
|
||||
|
||||
When fetching data from an API, we can take advantage of the [`useFetch`](https://nuxt.com/docs/api/composables/use-fetch) or [`useAsyncData`](https://nuxt.com/docs/api/composables/use-async-data) composables to fetch the data based on the sorting column and direction every time the `sort` reactive element changes.
|
||||
|
||||
When doing so, you might want to set the `sort-mode` prop to `manual` to disable the automatic sorting and return the rows as is. :u-badge{label="New" class="!rounded-full" variant="subtle"}
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// Ensure it uses `ref` instead of `reactive`.
|
||||
const sort = ref({
|
||||
column: 'name',
|
||||
direction: 'desc'
|
||||
})
|
||||
|
||||
const columns = [{
|
||||
label: 'Name',
|
||||
key: 'name',
|
||||
sortable: true
|
||||
}]
|
||||
|
||||
const { data, pending } = await useLazyFetch(() => `/api/users?orderBy=${sort.value.column}&order=${sort.value.direction}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable v-model:sort="sort" :loading="pending" :columns="columns" :rows="data" sort-mode="manual" />
|
||||
</template>
|
||||
```
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb" to="https://nuxt.com/docs/api/composables/use-fetch#params" target="_blank"}
|
||||
We pass a function to `useLazyFetch` here make the url reactive but you can use the `query` / `params` options alongside `watch`.
|
||||
::
|
||||
|
||||
#### Custom sorting
|
||||
|
||||
Use the `sort-button` prop to customize the sort button in the header. You can pass all the props of the [Button](/elements/button) component to customize it through this prop or globally through `ui.table.default.sortButton`. Its icon defaults to `i-heroicons-arrows-up-down-20-solid`.
|
||||
|
||||
::component-card{class="grid"}
|
||||
@@ -151,43 +225,6 @@ Use the `sort-desc-icon` prop to set a different icon or change it globally in `
|
||||
You can also customize the entire header cell, read more in the [Slots](#slots) section.
|
||||
::
|
||||
|
||||
#### Reactive sorting
|
||||
|
||||
Sometimes you will want to fetch new data depending on the sorted column and direction. You can use the `v-model:sort` to automatically update the `ref` reactive element every time the sorting changes on the Table. You may also use `@update:sort` to call your own function with the sorting data.
|
||||
|
||||
For example, we can take advantage of `useLazyRefresh` computed URL to automatically fetch the data depending on the sorting column and direction every time the `sort` reactive element changes.
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// Ensure it uses `ref` instead of `reactive`.
|
||||
const sort = ref({
|
||||
column: 'name',
|
||||
direction: 'desc'
|
||||
})
|
||||
|
||||
const columns = [...]
|
||||
|
||||
const { data, pending } = useLazyFetch(() => {
|
||||
return `/api/users?orderBy=${sort.value.column}&order=${sort.value.direction}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable v-model:sort="sort" :loading="pending" :columns="columns" :rows="data" />
|
||||
</template>
|
||||
```
|
||||
|
||||
The initial value of `sort` will be respected as the initial sort column and direction, as well as each column default sorting direction.
|
||||
|
||||
::component-example{class="grid"}
|
||||
---
|
||||
padding: false
|
||||
component: 'table-example-reactive-sorting'
|
||||
componentProps:
|
||||
class: 'flex-1'
|
||||
---
|
||||
::
|
||||
|
||||
### Selectable
|
||||
|
||||
Use a `v-model` to make the table selectable. The `v-model` will be an array of the selected rows.
|
||||
|
||||
@@ -12,8 +12,9 @@ links:
|
||||
Pass an array to the `links` prop of the VerticalNavigation component. Each link can have the following properties:
|
||||
|
||||
- `label` - The label of the link.
|
||||
- `labelClass` - The class of the link label. :u-badge{label="New" class="!rounded-full" variant="subtle"}
|
||||
- `icon` - The icon of the link.
|
||||
- `iconClass` - The class of the icon link.
|
||||
- `iconClass` - The class of the link icon.
|
||||
- `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.
|
||||
@@ -26,6 +27,12 @@ You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/com
|
||||
Learn how to build a Tailwind like vertical navigation in the [Examples](/getting-started/examples#verticalnavigation) page.
|
||||
::
|
||||
|
||||
## Sections
|
||||
|
||||
Group your navigation links into distinct sections, separated by a divider. You can do this by passing an array of arrays to the `links` prop of the VerticalNavigation component.
|
||||
|
||||
:component-example{component="vertical-navigation-example-sections"}
|
||||
|
||||
## Slots
|
||||
|
||||
You can use slots to customize links display.
|
||||
|
||||
@@ -46,6 +46,22 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Disabled :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `disabled` prop to disable all the buttons.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
modelValue: 1
|
||||
total: 100
|
||||
showLast: true
|
||||
showFirst: true
|
||||
props:
|
||||
disabled: true
|
||||
---
|
||||
::
|
||||
|
||||
### Active / Inactive
|
||||
|
||||
Use the `active-button` and `inactive-button` props to customize the active and inactive buttons of the Pagination.
|
||||
|
||||
@@ -8,8 +8,9 @@ description: A list of links that indicate the current page's location within a
|
||||
Pass an array to the `links` prop of the Breadcrumb component. Each link can have the following properties:
|
||||
|
||||
- `label` - The label of the link.
|
||||
- `labelClass` - The class of the link label. :u-badge{label="New" class="!rounded-full" variant="subtle"}
|
||||
- `icon` - The icon of the link.
|
||||
- `iconClass` - The class of the icon link.
|
||||
- `iconClass` - The class of the link icon.
|
||||
|
||||
You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Use the `mode` prop to switch between `click` and `hover` modes.
|
||||
|
||||
### Manual
|
||||
|
||||
Use the `open` prop to manually control showing the panel.
|
||||
Use a `v-model:open` to manually control the state. In this example, press :shortcut{value="O"} to toggle the popover.
|
||||
|
||||
:component-example{component="popover-example-open"}
|
||||
|
||||
@@ -29,10 +29,6 @@ Use the `open` prop to manually control showing the panel.
|
||||
|
||||
:component-example{component="popover-example-overlay"}
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
Clicking on the `overlay` emits `update:open`. If you are manually controlling the `open` prop, you will need to use a [`v-model` argument](https://vuejs.org/guide/components/v-model.html#v-model-arguments) (`v-model:open`).
|
||||
::
|
||||
|
||||
## Popper
|
||||
|
||||
Use the `popper` prop to customize the popper instance.
|
||||
|
||||
@@ -133,7 +133,7 @@ excludedProps:
|
||||
|
||||
### Timeout
|
||||
|
||||
Use the `timeout` prop to configure how long the Notification will remain. Set it to `0` to disable the timeout.
|
||||
Use the `timeout` prop to configure how long the Notification will remain. The default value is `5000`, set it to `0` to disable the timeout.
|
||||
|
||||
You will see a progress bar at the bottom of the Notification which will indicate the remaining time. When hovering the Notification, the progress bar will be paused.
|
||||
|
||||
|
||||
@@ -67,8 +67,8 @@ sections:
|
||||
color: white
|
||||
size: lg
|
||||
trailingIcon: i-heroicons-arrow-right-20-solid
|
||||
- title: 'A collection of <span class="text-primary">30+</span> components'
|
||||
description: 'Get access to 30+ beautifully designed and fully customizable components built for Nuxt. These components<br class="hidden lg:block"> are updated regularly to ensure that you always have the latest features and functionalities.'
|
||||
- title: 'A collection of <span class="text-primary">40+</span> components'
|
||||
description: 'Get access to 40+ beautifully designed and fully customizable components built for Nuxt. These components<br class="hidden lg:block"> are updated regularly to ensure that you always have the latest features and functionalities.'
|
||||
class: 'dark:bg-gradient-to-b from-gray-950/50 to-gray-900'
|
||||
slot: categories
|
||||
links:
|
||||
@@ -82,12 +82,12 @@ sections:
|
||||
to: /elements/dropdown
|
||||
image:
|
||||
path: /illustrations/elements
|
||||
badge: 9
|
||||
badge: 15
|
||||
- label: Forms
|
||||
to: /forms/form
|
||||
image:
|
||||
path: /illustrations/forms
|
||||
badge: 10
|
||||
badge: 12
|
||||
- label: Data
|
||||
to: /data/table
|
||||
image:
|
||||
@@ -97,17 +97,17 @@ sections:
|
||||
to: /navigation/command-palette
|
||||
image:
|
||||
path: /illustrations/navigation
|
||||
badge: 4
|
||||
badge: 5
|
||||
- label: Overlays
|
||||
to: /overlays/modal
|
||||
image:
|
||||
path: /illustrations/overlays
|
||||
badge: 6
|
||||
badge: 7
|
||||
- label: Layout
|
||||
to: /layout/card
|
||||
image:
|
||||
path: /illustrations/layout
|
||||
badge: 3
|
||||
badge: 4
|
||||
cta:
|
||||
title: Trusted and supported by our<br class="hidden lg:block"> amazing community
|
||||
pro:
|
||||
|
||||
@@ -17,8 +17,8 @@ export default defineNuxtConfig({
|
||||
].filter(Boolean),
|
||||
modules: [
|
||||
'@nuxt/content',
|
||||
'@nuxt/image',
|
||||
'nuxt-og-image',
|
||||
// '@nuxt/devtools',
|
||||
// '@nuxthq/studio',
|
||||
module,
|
||||
'@nuxtjs/fontaine',
|
||||
@@ -86,7 +86,8 @@ export default defineNuxtConfig({
|
||||
'/api/search.json',
|
||||
'/api/releases.json',
|
||||
'/api/pulls.json'
|
||||
]
|
||||
],
|
||||
ignore: !process.env.NUXT_UI_PRO_PATH && !process.env.NUXT_GITHUB_TOKEN ? ['/pro'] : []
|
||||
}
|
||||
},
|
||||
componentMeta: {
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
"@nuxt/ui": "workspace:latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/heroicons": "^1.1.15",
|
||||
"@iconify-json/simple-icons": "^1.1.82",
|
||||
"@nuxt/content": "^2.9.0",
|
||||
"@nuxt/devtools": "^1.0.4",
|
||||
"@iconify-json/heroicons": "^1.1.19",
|
||||
"@iconify-json/simple-icons": "^1.1.87",
|
||||
"@nuxt/content": "^2.10.0",
|
||||
"@nuxt/devtools": "^1.0.6",
|
||||
"@nuxt/eslint-config": "^0.2.0",
|
||||
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@0.5.0-28367445.03c79ba",
|
||||
"@nuxthq/studio": "^1.0.5",
|
||||
"@nuxt/image": "^1.1.0",
|
||||
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@0.6.1-28413612.408e456",
|
||||
"@nuxthq/studio": "^1.0.6",
|
||||
"@nuxtjs/fontaine": "^0.4.1",
|
||||
"@nuxtjs/google-fonts": "^3.1.0",
|
||||
"@nuxtjs/mdc": "^0.2.8",
|
||||
"@nuxtjs/google-fonts": "^3.1.3",
|
||||
"@nuxtjs/plausible": "^0.2.4",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@vueuse/nuxt": "^10.7.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"eslint": "^8.55.0",
|
||||
"@vueuse/nuxt": "^10.7.1",
|
||||
"date-fns": "^3.1.0",
|
||||
"eslint": "^8.56.0",
|
||||
"joi": "^17.11.0",
|
||||
"nuxt": "^3.8.2",
|
||||
"nuxt": "^3.9.1",
|
||||
"nuxt-cloudflare-analytics": "^1.0.8",
|
||||
"nuxt-component-meta": "^0.6.0",
|
||||
"nuxt-og-image": "^2.2.4",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier": "^3.1.1",
|
||||
"typescript": "^5.3.3",
|
||||
"ufo": "^1.3.2",
|
||||
"v-calendar": "^3.1.2",
|
||||
"valibot": "^0.21.0",
|
||||
"yup": "^1.3.2",
|
||||
"valibot": "^0.25.0",
|
||||
"yup": "^1.3.3",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
<UDivider v-if="page.body?.toc?.links?.length" type="dashed" />
|
||||
|
||||
<UPageLinks title="Community" :links="links" />
|
||||
|
||||
<UDivider type="dashed" />
|
||||
|
||||
<div class="space-y-3">
|
||||
<AdsPro />
|
||||
<AdsCarbon />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UDocsToc>
|
||||
|
||||
@@ -41,6 +41,8 @@ const { data: pulls } = await useLazyFetch('/api/pulls.json', { default: () => [
|
||||
|
||||
const dates = computed(() => {
|
||||
const first = releases.value[releases.value.length - 1]
|
||||
if (!first) return []
|
||||
|
||||
const days = eachDayOfInterval({ start: new Date(first.published_at), end: new Date() })
|
||||
|
||||
return days.reverse().map(day => {
|
||||
|
||||
51
package.json
51
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nuxt/ui",
|
||||
"version": "2.11.1",
|
||||
"version": "2.12.0",
|
||||
"repository": "nuxt/ui",
|
||||
"homepage": "https://ui.nuxt.com",
|
||||
"license": "MIT",
|
||||
@@ -32,51 +32,54 @@
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@egoist/tailwindcss-icons": "^1.4.0",
|
||||
"@egoist/tailwindcss-icons": "^1.7.2",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@headlessui/vue": "^1.7.16",
|
||||
"@iconify-json/heroicons": "^1.1.15",
|
||||
"@nuxt/kit": "^3.8.2",
|
||||
"@iconify-json/heroicons": "^1.1.19",
|
||||
"@nuxt/kit": "^3.9.1",
|
||||
"@nuxtjs/color-mode": "^3.3.2",
|
||||
"@nuxtjs/tailwindcss": "^6.10.1",
|
||||
"@nuxtjs/tailwindcss": "^6.10.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@vueuse/core": "^10.7.0",
|
||||
"@vueuse/integrations": "^10.7.0",
|
||||
"@vueuse/math": "^10.7.0",
|
||||
"defu": "^6.1.3",
|
||||
"@vueuse/core": "^10.7.1",
|
||||
"@vueuse/integrations": "^10.7.1",
|
||||
"@vueuse/math": "^10.7.1",
|
||||
"defu": "^6.1.4",
|
||||
"fuse.js": "^6.6.2",
|
||||
"nuxt-icon": "^0.6.7",
|
||||
"nuxt-icon": "^0.6.8",
|
||||
"ohash": "^1.1.3",
|
||||
"pathe": "^1.1.1",
|
||||
"scule": "^1.1.1",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.3.6"
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss": "^3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint-config": "^0.2.0",
|
||||
"@nuxt/module-builder": "^0.5.4",
|
||||
"@nuxt/module-builder": "^0.5.5",
|
||||
"@nuxt/test-utils": "^3.9.0",
|
||||
"@release-it/conventional-changelog": "^8.0.1",
|
||||
"eslint": "^8.55.0",
|
||||
"@vue/test-utils": "^2.4.3",
|
||||
"eslint": "^8.56.0",
|
||||
"happy-dom": "^12.10.3",
|
||||
"joi": "^17.11.0",
|
||||
"nuxt": "^3.8.2",
|
||||
"nuxt-vitest": "^0.11.5",
|
||||
"release-it": "^17.0.0",
|
||||
"nuxt": "^3.9.1",
|
||||
"release-it": "^17.0.1",
|
||||
"typescript": "^5.3.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"valibot": "^0.21.0",
|
||||
"vitest": "^0.33.0",
|
||||
"vue-tsc": "^1.8.25",
|
||||
"yup": "^1.3.2",
|
||||
"valibot": "^0.25.0",
|
||||
"vitest": "^1.1.3",
|
||||
"vitest-environment-nuxt": "^1.0.0",
|
||||
"vue-tsc": "^1.8.27",
|
||||
"yup": "^1.3.3",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@nuxt/kit": "3.8.2",
|
||||
"@nuxt/schema": "3.8.2",
|
||||
"vue": "3.3.8"
|
||||
"@nuxt/kit": "3.9.1",
|
||||
"@nuxt/schema": "3.9.1",
|
||||
"vue": "3.3.13",
|
||||
"tailwindcss": "3.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
6
playground/app.config.ts
Normal file
6
playground/app.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
primary: 'green',
|
||||
gray: 'cool'
|
||||
}
|
||||
})
|
||||
6
playground/tailwind.config.ts
Normal file
6
playground/tailwind.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
export default <Partial<Config>>{
|
||||
theme: {
|
||||
}
|
||||
}
|
||||
4987
pnpm-lock.yaml
generated
4987
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -143,7 +143,7 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
tailwindConfig.safelist.push(...generateSafelist(options.safelistColors || [], colors))
|
||||
|
||||
tailwindConfig.plugins = tailwindConfig.plugins || []
|
||||
tailwindConfig.plugins.push(iconsPlugin(Array.isArray(options.icons) || options.icons === 'all' ? { collections: getIconCollections(options.icons) } : typeof options.icons === 'object' ? options.icons as IconsPluginOptions : {}))
|
||||
tailwindConfig.plugins.push(iconsPlugin(Array.isArray(options.icons) ? { collections: getIconCollections(options.icons) } : typeof options.icons === 'object' ? options.icons as IconsPluginOptions : {}))
|
||||
})
|
||||
|
||||
createTemplates(nuxt)
|
||||
|
||||
@@ -67,15 +67,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, defineComponent, toRaw, toRef } from 'vue'
|
||||
import { computed, defineComponent, toRaw, toRef } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { upperFirst } from 'scule'
|
||||
import { defu } from 'defu'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import UButton from '../elements/Button.vue'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import UCheckbox from '../forms/Checkbox.vue'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
import { mergeConfig, omit, get } from '../../utils'
|
||||
import { mergeConfig, get } from '../../utils'
|
||||
import type { Strategy, Button } from '../../types'
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
@@ -87,6 +88,18 @@ function defaultComparator<T> (a: T, z: T): boolean {
|
||||
return a === z
|
||||
}
|
||||
|
||||
function defaultSort (a: any, b: any, direction: 'asc' | 'desc') {
|
||||
if (a === b) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (direction === 'asc') {
|
||||
return a < b ? -1 : 1
|
||||
} else {
|
||||
return a > b ? -1 : 1
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UButton,
|
||||
@@ -104,11 +117,11 @@ export default defineComponent({
|
||||
default: () => defaultComparator
|
||||
},
|
||||
rows: {
|
||||
type: Array as PropType<{ [key: string]: any, click?: Function }[]>,
|
||||
type: Array as PropType<{ [key: string]: any }[]>,
|
||||
default: () => []
|
||||
},
|
||||
columns: {
|
||||
type: Array as PropType<{ key: string, sortable?: boolean, direction?: 'asc' | 'desc', class?: string, [key: string]: any }[]>,
|
||||
type: Array as PropType<{ key: string, sortable?: boolean, sort?: (a: any, b: any, direction: 'asc' | 'desc') => number, direction?: 'asc' | 'desc', class?: string, [key: string]: any }[]>,
|
||||
default: null
|
||||
},
|
||||
columnAttribute: {
|
||||
@@ -119,6 +132,10 @@ export default defineComponent({
|
||||
type: Object as PropType<{ column: string, direction: 'asc' | 'desc' }>,
|
||||
default: () => ({})
|
||||
},
|
||||
sortMode: {
|
||||
type: String as PropType<'manual' | 'auto'>,
|
||||
default: 'auto'
|
||||
},
|
||||
sortButton: {
|
||||
type: Object as PropType<Button>,
|
||||
default: () => config.default.sortButton as Button
|
||||
@@ -156,14 +173,14 @@ export default defineComponent({
|
||||
setup (props, { emit, attrs: $attrs }) {
|
||||
const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const columns = computed(() => props.columns ?? Object.keys(omit(props.rows[0] ?? {}, ['click'])).map((key) => ({ key, label: upperFirst(key), sortable: false })))
|
||||
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: upperFirst(key), sortable: false, class: undefined, sort: defaultSort })))
|
||||
|
||||
const sort = ref(defu({}, props.sort, { column: null, direction: 'asc' }))
|
||||
const sort = useVModel(props, 'sort', emit, { passive: true, defaultValue: defu({}, props.sort, { column: null, direction: 'asc' }) })
|
||||
|
||||
const defaultSort = { column: sort.value.column, direction: null }
|
||||
const savedSort = { column: sort.value.column, direction: null }
|
||||
|
||||
const rows = computed(() => {
|
||||
if (!sort.value?.column) {
|
||||
if (!sort.value?.column || props.sortMode === 'manual') {
|
||||
return props.rows
|
||||
}
|
||||
|
||||
@@ -173,15 +190,9 @@ export default defineComponent({
|
||||
const aValue = get(a, column)
|
||||
const bValue = get(b, column)
|
||||
|
||||
if (aValue === bValue) {
|
||||
return 0
|
||||
}
|
||||
const sort = columns.value.find((col) => col.key === column)?.sort ?? defaultSort
|
||||
|
||||
if (direction === 'asc') {
|
||||
return aValue < bValue ? -1 : 1
|
||||
} else {
|
||||
return aValue > bValue ? -1 : 1
|
||||
}
|
||||
return sort(aValue, bValue, direction)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -227,15 +238,13 @@ export default defineComponent({
|
||||
const direction = !column.direction || column.direction === 'asc' ? 'desc' : 'asc'
|
||||
|
||||
if (sort.value.direction === direction) {
|
||||
sort.value = defu({}, defaultSort, { column: null, direction: 'asc' })
|
||||
sort.value = defu({}, savedSort, { column: null, direction: 'asc' })
|
||||
} else {
|
||||
sort.value.direction = sort.value.direction === 'asc' ? 'desc' : 'asc'
|
||||
sort.value = { column: sort.value.column, direction: sort.value.direction === 'asc' ? 'desc' : 'asc' }
|
||||
}
|
||||
} else {
|
||||
sort.value = { column: column.key, direction: column.direction || 'asc' }
|
||||
}
|
||||
|
||||
emit('update:sort', sort.value)
|
||||
}
|
||||
|
||||
function onSelect (row) {
|
||||
@@ -267,7 +276,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
function getRowData (row: Object, rowKey: string | string[], defaultValue: any = 'Failed to get cell value') {
|
||||
function getRowData (row: Object, rowKey: string | string[], defaultValue: any = '') {
|
||||
return get(row, rowKey, defaultValue)
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,8 @@ export default defineComponent({
|
||||
})
|
||||
}
|
||||
|
||||
function onEnter (el: HTMLElement, done) {
|
||||
function onEnter (_el: Element, done: () => void) {
|
||||
const el = _el as HTMLElement
|
||||
el.style.height = '0'
|
||||
el.offsetHeight // Trigger a reflow, flushing the CSS changes
|
||||
el.style.height = el.scrollHeight + 'px'
|
||||
@@ -127,16 +128,19 @@ export default defineComponent({
|
||||
el.addEventListener('transitionend', done, { once: true })
|
||||
}
|
||||
|
||||
function onBeforeLeave (el: HTMLElement) {
|
||||
function onBeforeLeave (_el: Element) {
|
||||
const el = _el as HTMLElement
|
||||
el.style.height = el.scrollHeight + 'px'
|
||||
el.offsetHeight // Trigger a reflow, flushing the CSS changes
|
||||
}
|
||||
|
||||
function onAfterEnter (el: HTMLElement) {
|
||||
function onAfterEnter (_el: Element) {
|
||||
const el = _el as HTMLElement
|
||||
el.style.height = 'auto'
|
||||
}
|
||||
|
||||
function onLeave (el: HTMLElement, done) {
|
||||
function onLeave (_el: Element, done: () => void) {
|
||||
const el = _el as HTMLElement
|
||||
el.style.height = '0'
|
||||
|
||||
el.addEventListener('transitionend', done, { once: true })
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
</p>
|
||||
|
||||
<div v-if="(description || $slots.description) && actions.length" :class="ui.actions">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="action.click" />
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="closeButton || (!description && !$slots.description && actions.length)" :class="twMerge(ui.actions, 'mt-0')">
|
||||
<template v-if="!description && !$slots.description && actions.length">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="action.click" />
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
|
||||
</template>
|
||||
|
||||
<UButton v-if="closeButton" aria-label="Close" v-bind="{ ...(ui.default.closeButton || {}), ...closeButton }" @click.stop="$emit('close')" />
|
||||
@@ -39,7 +39,7 @@ import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import UButton from '../elements/Button.vue'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
import type { Avatar, Button, AlertColor, AlertVariant, Strategy } from '../../types'
|
||||
import type { Avatar, Button, AlertColor, AlertVariant, AlertAction, Strategy } from '../../types'
|
||||
import { mergeConfig } from '../../utils'
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
@@ -76,7 +76,7 @@ export default defineComponent({
|
||||
default: () => config.default.closeButton as unknown as Button
|
||||
},
|
||||
actions: {
|
||||
type: Array as PropType<(Button & { click?: Function })[]>,
|
||||
type: Array as PropType<AlertAction[]>,
|
||||
default: () => []
|
||||
},
|
||||
color: {
|
||||
@@ -121,11 +121,18 @@ export default defineComponent({
|
||||
), props.class)
|
||||
})
|
||||
|
||||
function onAction (action: AlertAction) {
|
||||
if (action.click) {
|
||||
action.click()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
attrs,
|
||||
alertClass,
|
||||
onAction,
|
||||
twMerge
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<!-- eslint-disable-next-line vue/no-template-shadow -->
|
||||
<HMenu v-slot="{ open }" as="div" :class="ui.wrapper" v-bind="attrs" @mouseleave="onMouseLeave">
|
||||
<HMenuButton
|
||||
ref="trigger"
|
||||
@@ -19,23 +20,24 @@
|
||||
<Transition appear v-bind="ui.transition">
|
||||
<div>
|
||||
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(ui.arrow)" />
|
||||
|
||||
<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">
|
||||
<NuxtLink v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ href, target, rel, navigate, isExternal }" v-bind="omit(item, ['label', 'slot', 'icon', 'iconClass', 'avatar', 'shortcuts', 'disabled', 'click'])" custom>
|
||||
<NuxtLink v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ href, target, rel, navigate, isExternal }" v-bind="omit(item, ['label', 'labelClass', 'slot', 'icon', 'iconClass', 'avatar', 'shortcuts', 'disabled', 'class', 'click'])" custom>
|
||||
<HMenuItem v-slot="{ active, disabled: itemDisabled, close }" :disabled="item.disabled">
|
||||
<component
|
||||
:is="!!href ? 'a' : 'button'"
|
||||
:href="!itemDisabled ? href : undefined"
|
||||
:rel="rel"
|
||||
:target="target"
|
||||
:class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]"
|
||||
:class="twMerge(twJoin(ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled), item.class)"
|
||||
@click="onClick($event, item, { href, navigate, close, isExternal })"
|
||||
>
|
||||
<slot :name="item.slot || 'item'" :item="item">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="[ui.item.icon.base, active ? ui.item.icon.active : ui.item.icon.inactive, item.iconClass]" />
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="twMerge(twJoin(ui.item.icon.base, active ? ui.item.icon.active : ui.item.icon.inactive), item.iconClass)" />
|
||||
<UAvatar v-else-if="item.avatar" v-bind="{ size: ui.item.avatar.size, ...item.avatar }" :class="ui.item.avatar.base" />
|
||||
|
||||
<span :class="ui.item.label">{{ item.label }}</span>
|
||||
<span :class="twMerge(ui.item.label, item.labelClass)">{{ item.label }}</span>
|
||||
|
||||
<span v-if="item.shortcuts?.length" :class="ui.item.shortcuts">
|
||||
<UKbd v-for="shortcut of item.shortcuts" :key="shortcut">{{ shortcut }}</UKbd>
|
||||
@@ -53,10 +55,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, toRef, onMounted, resolveComponent } from 'vue'
|
||||
import { defineComponent, ref, computed, watch, toRef, onMounted, resolveComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue'
|
||||
import { defu } from 'defu'
|
||||
import { twMerge, twJoin } from 'tailwind-merge'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import UKbd from '../elements/Kbd.vue'
|
||||
@@ -91,6 +94,10 @@ export default defineComponent({
|
||||
default: 'click',
|
||||
validator: (value: string) => ['click', 'hover'].includes(value)
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -116,7 +123,8 @@ export default defineComponent({
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
emits: ['update:open'],
|
||||
setup (props, { emit }) {
|
||||
const { ui, attrs } = useUI('dropdown', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
|
||||
@@ -130,21 +138,46 @@ export default defineComponent({
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const menuProvides = trigger.value?.$.provides
|
||||
if (!menuProvides) {
|
||||
return
|
||||
}
|
||||
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
|
||||
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
|
||||
}, 200)
|
||||
// @ts-expect-error internals
|
||||
const menuProvides = trigger.value?.$.provides
|
||||
if (!menuProvides) {
|
||||
return
|
||||
}
|
||||
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
|
||||
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
|
||||
|
||||
if (props.open) {
|
||||
menuApi.value?.openMenu()
|
||||
}
|
||||
})
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8
|
||||
if (props.mode !== 'hover') {
|
||||
return {}
|
||||
}
|
||||
|
||||
return props.mode === 'hover' ? { paddingTop: `${offsetDistance}px`, paddingBottom: `${offsetDistance}px` } : {}
|
||||
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8
|
||||
const placement = popper.value.placement?.split('-')[0]
|
||||
const padding = `${offsetDistance}px`
|
||||
|
||||
if (placement === 'top' || placement === 'bottom') {
|
||||
return {
|
||||
paddingTop: padding,
|
||||
paddingBottom: padding
|
||||
}
|
||||
} else if (placement === 'left' || placement === 'right') {
|
||||
return {
|
||||
paddingLeft: padding,
|
||||
paddingRight: padding
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
paddingTop: padding,
|
||||
paddingBottom: padding,
|
||||
paddingLeft: padding,
|
||||
paddingRight: padding
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function onMouseOver () {
|
||||
@@ -199,6 +232,23 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.open, (newValue: boolean, oldValue: boolean) => {
|
||||
if (!menuApi.value) return
|
||||
if (oldValue === undefined || newValue === oldValue) return
|
||||
|
||||
if (newValue) {
|
||||
menuApi.value.openMenu()
|
||||
} else {
|
||||
menuApi.value.closeMenu()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => menuApi.value?.menuState, (newValue: number, oldValue: number) => {
|
||||
if (oldValue === undefined || newValue === oldValue) return
|
||||
|
||||
emit('update:open', newValue === 0)
|
||||
})
|
||||
|
||||
const NuxtLink = resolveComponent('NuxtLink')
|
||||
|
||||
return {
|
||||
@@ -214,6 +264,8 @@ export default defineComponent({
|
||||
onMouseLeave,
|
||||
onClick,
|
||||
omit,
|
||||
twMerge,
|
||||
twJoin,
|
||||
NuxtLink
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
:role="disabled ? 'link' : undefined"
|
||||
:rel="rel"
|
||||
:target="target"
|
||||
:class="active ? activeClass : resolveLinkClass(route, $route, { isActive, isExactActive })"
|
||||
:class="active !== undefined ? (active ? activeClass : inactiveClass) : resolveLinkClass(route, $route, { isActive, isExactActive })"
|
||||
@click="(e) => !isExternal && navigate(e)"
|
||||
>
|
||||
<slot v-bind="{ isActive: exact ? isExactActive : isActive }" />
|
||||
@@ -48,7 +48,7 @@ export default defineComponent({
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: undefined
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { provide, ref, type PropType, defineComponent } from 'vue'
|
||||
import { provide, ref, type PropType, defineComponent, onUnmounted, onMounted } from 'vue'
|
||||
import { useEventBus } from '@vueuse/core'
|
||||
import type { ZodSchema } from 'zod'
|
||||
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
|
||||
@@ -51,10 +51,16 @@ export default defineComponent({
|
||||
setup (props, { expose, emit }) {
|
||||
const bus = useEventBus<FormEvent>(`form-${uid()}`)
|
||||
|
||||
bus.on(async (event) => {
|
||||
if (event.type !== 'submit' && props.validateOn?.includes(event.type)) {
|
||||
await validate(event.path, { silent: true })
|
||||
}
|
||||
onMounted(() => {
|
||||
bus.on(async (event) => {
|
||||
if (event.type !== 'submit' && props.validateOn?.includes(event.type)) {
|
||||
await validate(event.path, { silent: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
bus.reset()
|
||||
})
|
||||
|
||||
const errors = ref<FormError[]>([])
|
||||
@@ -104,7 +110,8 @@ export default defineComponent({
|
||||
return props.state
|
||||
}
|
||||
|
||||
async function onSubmit (event: SubmitEvent) {
|
||||
async function onSubmit (payload: Event) {
|
||||
const event = payload as SubmitEvent
|
||||
try {
|
||||
if (props.validateOn?.includes('submit')) {
|
||||
await validate()
|
||||
@@ -143,6 +150,9 @@ export default defineComponent({
|
||||
errors.value = errs
|
||||
}
|
||||
},
|
||||
async submit () {
|
||||
await onSubmit(new Event('submit'))
|
||||
},
|
||||
getErrors (path?: string) {
|
||||
if (path) {
|
||||
return errors.value.filter((err) => err.path === path)
|
||||
@@ -151,7 +161,7 @@ export default defineComponent({
|
||||
},
|
||||
clear (path?: string) {
|
||||
if (path) {
|
||||
errors.value = errors.value.filter((err) => err.path === path)
|
||||
errors.value = errors.value.filter((err) => err.path !== path)
|
||||
} else {
|
||||
errors.value = []
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:type="type"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled || loading"
|
||||
:disabled="disabled"
|
||||
:class="inputClass"
|
||||
v-bind="attrs"
|
||||
@input="onInput"
|
||||
|
||||
444
src/runtime/components/forms/InputMenu.vue
Normal file
444
src/runtime/components/forms/InputMenu.vue
Normal file
@@ -0,0 +1,444 @@
|
||||
<template>
|
||||
<HCombobox
|
||||
v-slot="{ open }"
|
||||
:by="by"
|
||||
:name="name"
|
||||
:model-value="modelValue"
|
||||
:disabled="disabled"
|
||||
as="div"
|
||||
:class="ui.wrapper"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<div :class="uiMenu.trigger">
|
||||
<HComboboxInput
|
||||
:id="inputId"
|
||||
:name="name"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="inputClass"
|
||||
autocomplete="off"
|
||||
v-bind="attrs"
|
||||
:display-value="() => query ? query : ['string', 'number'].includes(typeof modelValue) ? modelValue : modelValue[optionAttribute]"
|
||||
@change="onChange"
|
||||
/>
|
||||
|
||||
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
|
||||
<slot name="leading" :disabled="disabled" :loading="loading">
|
||||
<UIcon :name="leadingIconName" :class="leadingIconClass" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<HComboboxButton v-if="(isTrailing && trailingIconName) || $slots.trailing" ref="trigger" :class="trailingWrapperIconClass">
|
||||
<slot name="trailing" :disabled="disabled" :loading="loading">
|
||||
<UIcon :name="trailingIconName" :class="trailingIconClass" />
|
||||
</slot>
|
||||
</HComboboxButton>
|
||||
</div>
|
||||
|
||||
<div v-if="open" ref="container" :class="[uiMenu.container, uiMenu.width]">
|
||||
<Transition appear v-bind="uiMenu.transition">
|
||||
<div>
|
||||
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(uiMenu.arrow)" />
|
||||
|
||||
<HComboboxOptions static :class="[uiMenu.base, uiMenu.ring, uiMenu.rounded, uiMenu.shadow, uiMenu.background, uiMenu.padding, uiMenu.height]">
|
||||
<HComboboxOption
|
||||
v-for="(option, index) in filteredOptions"
|
||||
v-slot="{ active, selected, disabled: optionDisabled }"
|
||||
:key="index"
|
||||
as="template"
|
||||
:value="valueAttribute ? option[valueAttribute] : option"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
<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="[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: uiMenu.option.avatar.size, ...option.avatar }"
|
||||
:class="uiMenu.option.avatar.base"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span v-else-if="option.chip" :class="uiMenu.option.chip.base" :style="{ background: `#${option.chip}` }" />
|
||||
|
||||
<span class="truncate">{{ ['string', 'number'].includes(typeof option) ? option : option[optionAttribute] }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</HComboboxOption>
|
||||
|
||||
<p v-if="query && !filteredOptions.length" :class="uiMenu.option.empty">
|
||||
<slot name="option-empty" :query="query">
|
||||
No results for "{{ query }}".
|
||||
</slot>
|
||||
</p>
|
||||
<p v-else-if="!filteredOptions.length" :class="uiMenu.empty">
|
||||
<slot name="empty" :query="query">
|
||||
No options.
|
||||
</slot>
|
||||
</p>
|
||||
</HComboboxOptions>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</HCombobox>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, toRef, watch, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import {
|
||||
Combobox as HCombobox,
|
||||
ComboboxButton as HComboboxButton,
|
||||
ComboboxOptions as HComboboxOptions,
|
||||
ComboboxOption as HComboboxOption,
|
||||
ComboboxInput as HComboboxInput
|
||||
} from '@headlessui/vue'
|
||||
import { computedAsync, useDebounceFn } from '@vueuse/core'
|
||||
import { defu } from 'defu'
|
||||
import { twMerge, twJoin } from 'tailwind-merge'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import { useFormGroup } from '../../composables/useFormGroup'
|
||||
import { get, mergeConfig } from '../../utils'
|
||||
import { useInjectButtonGroup } from '../../composables/useButtonGroup'
|
||||
import type { InputSize, InputColor, InputVariant, PopperOptions, Strategy } from '../../types'
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
import { input, inputMenu } from '#ui/ui.config'
|
||||
|
||||
const config = mergeConfig<typeof input>(appConfig.ui.strategy, appConfig.ui.input, input)
|
||||
|
||||
const configMenu = mergeConfig<typeof inputMenu>(appConfig.ui.strategy, appConfig.ui.inputMenu, inputMenu)
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
HCombobox,
|
||||
HComboboxButton,
|
||||
HComboboxOptions,
|
||||
HComboboxOption,
|
||||
HComboboxInput,
|
||||
UIcon,
|
||||
UAvatar
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
by: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[] | string[]>,
|
||||
default: () => []
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => config.default.loadingIcon
|
||||
},
|
||||
leadingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailingIcon: {
|
||||
type: String,
|
||||
default: () => configMenu.default.trailingIcon
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedIcon: {
|
||||
type: String,
|
||||
default: () => configMenu.default.selectedIcon
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
padded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<InputSize>,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys(config.size).includes(value)
|
||||
}
|
||||
},
|
||||
color: {
|
||||
type: String as PropType<InputColor>,
|
||||
default: () => config.default.color,
|
||||
validator (value: string) {
|
||||
return [...appConfig.ui.colors, ...Object.keys(config.color)].includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String as PropType<InputVariant>,
|
||||
default: () => config.default.variant,
|
||||
validator (value: string) {
|
||||
return [
|
||||
...Object.keys(config.variant),
|
||||
...Object.values(config.color).flatMap(value => Object.keys(value))
|
||||
].includes(value)
|
||||
}
|
||||
},
|
||||
optionAttribute: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
valueAttribute: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
search: {
|
||||
type: Function as PropType<((query: string) => Promise<any[]> | any[])>,
|
||||
default: undefined
|
||||
},
|
||||
searchAttributes: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
debounce: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
popper: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
inputClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
},
|
||||
uiMenu: {
|
||||
type: Object as PropType<Partial<typeof configMenu> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'update:query', 'open', 'close', 'change'],
|
||||
setup (props, { emit, slots }) {
|
||||
const { ui, attrs } = useUI('input', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const { ui: uiMenu } = useUI('inputMenu', toRef(props, 'uiMenu'), configMenu)
|
||||
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, uiMenu.value.popper as PopperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popper.value)
|
||||
|
||||
const { size: sizeButtonGroup, rounded } = useInjectButtonGroup({ ui, props })
|
||||
const { emitFormBlur, emitFormChange, inputId, color, size: sizeFormGroup, name } = useFormGroup(props, config)
|
||||
|
||||
const size = computed(() => sizeButtonGroup.value || sizeFormGroup.value)
|
||||
|
||||
const internalQuery = ref('')
|
||||
const query = computed({
|
||||
get () {
|
||||
return props.query ?? internalQuery.value
|
||||
},
|
||||
set (value) {
|
||||
internalQuery.value = value
|
||||
emit('update:query', value)
|
||||
}
|
||||
})
|
||||
|
||||
const inputClass = computed(() => {
|
||||
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
|
||||
return twMerge(twJoin(
|
||||
ui.value.base,
|
||||
ui.value.form,
|
||||
rounded.value,
|
||||
ui.value.placeholder,
|
||||
ui.value.size[size.value],
|
||||
props.padded ? ui.value.padding[size.value] : 'p-0',
|
||||
variant?.replaceAll('{color}', color.value),
|
||||
(isLeading.value || slots.leading) && ui.value.leading.padding[size.value],
|
||||
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[size.value]
|
||||
), props.inputClass)
|
||||
})
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
|
||||
})
|
||||
|
||||
const leadingIconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.leadingIcon || props.icon
|
||||
})
|
||||
|
||||
const trailingIconName = computed(() => {
|
||||
if (props.loading && !isLeading.value) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.trailingIcon || props.icon
|
||||
})
|
||||
|
||||
const leadingWrapperIconClass = computed(() => {
|
||||
return twJoin(
|
||||
ui.value.icon.leading.wrapper,
|
||||
ui.value.icon.leading.pointer,
|
||||
ui.value.icon.leading.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return twJoin(
|
||||
ui.value.icon.base,
|
||||
color.value && appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && ui.value.icon.loading
|
||||
)
|
||||
})
|
||||
|
||||
const trailingWrapperIconClass = computed(() => {
|
||||
return twJoin(
|
||||
ui.value.icon.trailing.wrapper,
|
||||
ui.value.icon.trailing.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return twJoin(
|
||||
ui.value.icon.base,
|
||||
color.value && appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && !isLeading.value && ui.value.icon.loading
|
||||
)
|
||||
})
|
||||
|
||||
const debouncedSearch = props.search && typeof props.search === 'function' ? useDebounceFn(props.search, props.debounce) : undefined
|
||||
|
||||
const filteredOptions = computedAsync(async () => {
|
||||
if (debouncedSearch) {
|
||||
return await debouncedSearch(query.value)
|
||||
}
|
||||
|
||||
if (query.value === '') {
|
||||
return props.options
|
||||
}
|
||||
|
||||
return (props.options as any[]).filter((option: any) => {
|
||||
return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => {
|
||||
if (['string', 'number'].includes(typeof option)) {
|
||||
return String(option).search(new RegExp(query.value, 'i')) !== -1
|
||||
}
|
||||
|
||||
const child = get(option, searchAttribute)
|
||||
|
||||
return child !== null && child !== undefined && String(child).search(new RegExp(query.value, 'i')) !== -1
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
watch(container, (value) => {
|
||||
if (value) {
|
||||
emit('open')
|
||||
} else {
|
||||
emit('close')
|
||||
emitFormBlur()
|
||||
}
|
||||
})
|
||||
|
||||
function onUpdate (event: any) {
|
||||
query.value = ''
|
||||
emit('update:modelValue', event)
|
||||
emit('change', event)
|
||||
emitFormChange()
|
||||
}
|
||||
|
||||
function onChange (event: any) {
|
||||
query.value = event.target.value
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
uiMenu,
|
||||
attrs,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
name,
|
||||
inputId,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
popper,
|
||||
trigger,
|
||||
container,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
inputClass,
|
||||
leadingIconName,
|
||||
leadingIconClass,
|
||||
leadingWrapperIconClass,
|
||||
trailingIconName,
|
||||
trailingIconClass,
|
||||
trailingWrapperIconClass,
|
||||
filteredOptions,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
query,
|
||||
onUpdate,
|
||||
onChange
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -12,7 +12,7 @@
|
||||
:label="option.label"
|
||||
:model-value="modelValue"
|
||||
:value="option.value"
|
||||
:disabled="disabled"
|
||||
:disabled="option.disabled || disabled"
|
||||
:ui="uiRadio"
|
||||
@change="onUpdate(option.value)"
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:name="name"
|
||||
:value="modelValue"
|
||||
:required="required"
|
||||
:disabled="disabled || loading"
|
||||
:disabled="disabled"
|
||||
:class="selectClass"
|
||||
v-bind="attrs"
|
||||
@input="onInput"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:name="name"
|
||||
:model-value="modelValue"
|
||||
:multiple="multiple"
|
||||
:disabled="disabled || loading"
|
||||
:disabled="disabled"
|
||||
as="div"
|
||||
:class="ui.wrapper"
|
||||
@update:model-value="onUpdate"
|
||||
@@ -28,7 +28,7 @@
|
||||
:class="uiMenu.trigger"
|
||||
>
|
||||
<slot :open="open" :disabled="disabled" :loading="loading">
|
||||
<button :id="inputId" :class="selectClass" :disabled="disabled || loading" type="button" v-bind="attrs">
|
||||
<button :id="inputId" :class="selectClass" :disabled="disabled" 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" />
|
||||
@@ -58,14 +58,13 @@
|
||||
<component :is="searchable ? 'HComboboxOptions' : 'HListboxOptions'" static :class="[uiMenu.base, uiMenu.ring, uiMenu.rounded, uiMenu.shadow, uiMenu.background, uiMenu.padding, uiMenu.height]">
|
||||
<HComboboxInput
|
||||
v-if="searchable"
|
||||
ref="searchInput"
|
||||
:display-value="() => query"
|
||||
name="q"
|
||||
:placeholder="searchablePlaceholder"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
:class="uiMenu.input"
|
||||
@change="query = $event.target.value"
|
||||
@change="onChange"
|
||||
/>
|
||||
<component
|
||||
:is="searchable ? 'HComboboxOption' : 'HListboxOption'"
|
||||
@@ -98,11 +97,11 @@
|
||||
</li>
|
||||
</component>
|
||||
|
||||
<component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" as="template">
|
||||
<component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && createOption" v-slot="{ active, selected }" :value="createOption" as="template">
|
||||
<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="uiMenu.option.create">Create "{{ queryOption[optionAttribute] }}"</span>
|
||||
<slot name="option-create" :option="createOption" :active="active" :selected="selected">
|
||||
<span :class="uiMenu.option.create">Create "{{ createOption[optionAttribute] }}"</span>
|
||||
</slot>
|
||||
</div>
|
||||
</li>
|
||||
@@ -112,6 +111,11 @@
|
||||
No results for "{{ query }}".
|
||||
</slot>
|
||||
</p>
|
||||
<p v-else-if="!filteredOptions.length" :class="uiMenu.empty">
|
||||
<slot name="empty" :query="query">
|
||||
No options.
|
||||
</slot>
|
||||
</p>
|
||||
</component>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -121,7 +125,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, toRef, watch, defineComponent } from 'vue'
|
||||
import type { PropType, ComponentPublicInstance } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import {
|
||||
Combobox as HCombobox,
|
||||
ComboboxButton as HComboboxButton,
|
||||
@@ -172,6 +176,10 @@ export default defineComponent({
|
||||
type: [String, Number, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
by: {
|
||||
type: String,
|
||||
default: undefined
|
||||
@@ -242,7 +250,7 @@ export default defineComponent({
|
||||
},
|
||||
clearSearchOnClose: {
|
||||
type: Boolean,
|
||||
default: () => configMenu.default.clearOnClose
|
||||
default: () => configMenu.default.clearSearchOnClose
|
||||
},
|
||||
debounce: {
|
||||
type: Number,
|
||||
@@ -252,6 +260,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showCreateOptionWhen: {
|
||||
type: String as PropType<'always' | 'empty'>,
|
||||
default: () => configMenu.default.showCreateOptionWhen
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
@@ -317,7 +329,7 @@ export default defineComponent({
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'open', 'close', 'change'],
|
||||
emits: ['update:modelValue', 'update:query', 'open', 'close', 'change'],
|
||||
setup (props, { emit, slots }) {
|
||||
const { ui, attrs } = useUI('select', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
@@ -332,8 +344,16 @@ export default defineComponent({
|
||||
|
||||
const size = computed(() => sizeButtonGroup.value || sizeFormGroup.value)
|
||||
|
||||
const query = ref('')
|
||||
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
|
||||
const internalQuery = ref('')
|
||||
const query = computed({
|
||||
get () {
|
||||
return props.query ?? internalQuery.value
|
||||
},
|
||||
set (value) {
|
||||
internalQuery.value = value
|
||||
emit('update:query', value)
|
||||
}
|
||||
})
|
||||
|
||||
const selectClass = computed(() => {
|
||||
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
@@ -433,8 +453,21 @@ export default defineComponent({
|
||||
})
|
||||
})
|
||||
|
||||
const queryOption = computed(() => {
|
||||
return query.value === '' ? null : { [props.optionAttribute]: query.value }
|
||||
const createOption = computed(() => {
|
||||
if (query.value === '') {
|
||||
return null
|
||||
}
|
||||
if (props.showCreateOptionWhen === 'empty' && filteredOptions.value.length) {
|
||||
return null
|
||||
}
|
||||
if (props.showCreateOptionWhen === 'always') {
|
||||
const existingOption = filteredOptions.value.find(option => ['string', 'number'].includes(typeof option) ? option === query.value : option[props.optionAttribute] === query.value)
|
||||
if (existingOption) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return ['string', 'number'].includes(typeof props.modelValue) ? query.value : { [props.optionAttribute]: query.value }
|
||||
})
|
||||
|
||||
function clearOnClose () {
|
||||
@@ -454,17 +487,15 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
function onUpdate (event: any) {
|
||||
if (query.value && searchInput.value?.$el) {
|
||||
query.value = ''
|
||||
// explicitly set input text because `ComboboxInput` `displayValue` is not reactive
|
||||
searchInput.value.$el.value = ''
|
||||
}
|
||||
|
||||
emit('update:modelValue', event)
|
||||
emit('change', event)
|
||||
emitFormChange()
|
||||
}
|
||||
|
||||
function onChange (event: any) {
|
||||
query.value = event.target.value
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
@@ -489,9 +520,11 @@ export default defineComponent({
|
||||
trailingIconClass,
|
||||
trailingWrapperIconClass,
|
||||
filteredOptions,
|
||||
queryOption,
|
||||
createOption,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
query,
|
||||
onUpdate
|
||||
onUpdate,
|
||||
onChange
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -89,7 +89,7 @@ export default defineComponent({
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup (props, { emit }) {
|
||||
const { ui, attrs } = useUI('toggle', toRef(props, 'ui'), config)
|
||||
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
<ULink
|
||||
as="span"
|
||||
:class="[ui.base, index === links.length - 1 ? ui.active : !!link.to ? ui.inactive : '']"
|
||||
v-bind="omit(link, ['label', 'icon', 'iconClass'])"
|
||||
v-bind="omit(link, ['label', 'labelClass', 'icon', 'iconClass'])"
|
||||
:aria-current="index === links.length - 1 ? 'page' : undefined"
|
||||
>
|
||||
<slot name="icon" :link="link" :index="index" :is-active="index === links.length - 1">
|
||||
<UIcon
|
||||
v-if="link.icon"
|
||||
:name="link.icon"
|
||||
:class="[ui.icon.base, index === links.length - 1 ? ui.icon.active : !!link.to ? ui.icon.inactive : '', link.iconClass]"
|
||||
:class="twMerge(twJoin(ui.icon.base, index === links.length - 1 ? ui.icon.active : !!link.to ? ui.icon.inactive : ''), link.iconClass)"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<slot :link="link" :index="index" :is-active="index === links.length - 1">
|
||||
{{ link.label }}
|
||||
<span v-if="link.label" :class="twMerge(ui.label, link.labelClass)">{{ link.label }}</span>
|
||||
</slot>
|
||||
</ULink>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRef } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { twMerge, twJoin } from 'tailwind-merge'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import ULink from '../elements/Link.vue'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
@@ -77,7 +78,9 @@ export default defineComponent({
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
attrs,
|
||||
omit
|
||||
omit,
|
||||
twMerge,
|
||||
twJoin
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<UButton
|
||||
v-if="firstButton && showFirst"
|
||||
:size="size"
|
||||
:disabled="!canGoFirstOrPrev"
|
||||
:disabled="!canGoFirstOrPrev || disabled"
|
||||
:class="[ui.base, ui.rounded]"
|
||||
v-bind="{ ...(ui.default.firstButton || {}), ...firstButton }"
|
||||
:ui="{ rounded: '' }"
|
||||
@@ -17,7 +17,7 @@
|
||||
<UButton
|
||||
v-if="prevButton"
|
||||
:size="size"
|
||||
:disabled="!canGoFirstOrPrev"
|
||||
:disabled="!canGoFirstOrPrev || disabled"
|
||||
:class="[ui.base, ui.rounded]"
|
||||
v-bind="{ ...(ui.default.prevButton || {}), ...prevButton }"
|
||||
:ui="{ rounded: '' }"
|
||||
@@ -30,6 +30,7 @@
|
||||
v-for="(page, index) of displayedPages"
|
||||
:key="`${page}-${index}`"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
:label="`${page}`"
|
||||
v-bind="page === currentPage ? { ...(ui.default.activeButton || {}), ...activeButton } : { ...(ui.default.inactiveButton || {}), ...inactiveButton }"
|
||||
:class="[{ 'pointer-events-none': typeof page === 'string', 'z-[1]': page === currentPage }, ui.base, ui.rounded]"
|
||||
@@ -41,7 +42,7 @@
|
||||
<UButton
|
||||
v-if="nextButton"
|
||||
:size="size"
|
||||
:disabled="!canGoLastOrNext"
|
||||
:disabled="!canGoLastOrNext || disabled"
|
||||
:class="[ui.base, ui.rounded]"
|
||||
v-bind="{ ...(ui.default.nextButton || {}), ...nextButton }"
|
||||
:ui="{ rounded: '' }"
|
||||
@@ -54,7 +55,7 @@
|
||||
<UButton
|
||||
v-if="lastButton && showLast"
|
||||
:size="size"
|
||||
:disabled="!canGoLastOrNext"
|
||||
:disabled="!canGoLastOrNext || disabled"
|
||||
:class="[ui.base, ui.rounded]"
|
||||
v-bind="{ ...(ui.default.lastButton || {}), ...lastButton }"
|
||||
:ui="{ rounded: '' }"
|
||||
@@ -105,6 +106,10 @@ export default defineComponent({
|
||||
return value >= 5 && value < Number.MAX_VALUE
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<ButtonSize>,
|
||||
default: () => config.default.size,
|
||||
|
||||
@@ -1,48 +1,58 @@
|
||||
<template>
|
||||
<nav :class="ui.wrapper" v-bind="attrs">
|
||||
<ULink
|
||||
v-for="(link, index) of links"
|
||||
v-slot="{ isActive }"
|
||||
:key="index"
|
||||
v-bind="omit(link, ['label', 'icon', 'iconClass', 'avatar', 'badge', 'click'])"
|
||||
:class="[ui.base, ui.padding, ui.width, ui.ring, ui.rounded, ui.font, ui.size]"
|
||||
:active-class="ui.active"
|
||||
:inactive-class="ui.inactive"
|
||||
@click="link.click"
|
||||
@keyup.enter="$event.target.blur()"
|
||||
>
|
||||
<slot name="avatar" :link="link" :is-active="isActive">
|
||||
<UAvatar
|
||||
v-if="link.avatar"
|
||||
v-bind="{ size: ui.avatar.size, ...link.avatar }"
|
||||
:class="[ui.avatar.base]"
|
||||
/>
|
||||
</slot>
|
||||
<slot name="icon" :link="link" :is-active="isActive">
|
||||
<UIcon
|
||||
v-if="link.icon"
|
||||
:name="link.icon"
|
||||
:class="[ui.icon.base, isActive ? ui.icon.active : ui.icon.inactive, link.iconClass]"
|
||||
/>
|
||||
</slot>
|
||||
<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">
|
||||
<span v-if="link.badge" :class="[ui.badge.base, isActive ? ui.badge.active : ui.badge.inactive]">
|
||||
{{ link.badge }}
|
||||
</span>
|
||||
</slot>
|
||||
</ULink>
|
||||
<ul v-for="(section, sectionIndex) of linkSections" :key="`linkSection${sectionIndex}`">
|
||||
<li v-for="(link, index) of section" :key="`linkSection${sectionIndex}-${index}`">
|
||||
<ULink
|
||||
v-slot="{ isActive }"
|
||||
v-bind="omit(link, ['label', 'labelClass', 'icon', 'iconClass', 'avatar', 'badge', 'click'])"
|
||||
:class="[ui.base, ui.padding, ui.width, ui.ring, ui.rounded, ui.font, ui.size]"
|
||||
:active-class="ui.active"
|
||||
:inactive-class="ui.inactive"
|
||||
@click="link.click"
|
||||
@keyup.enter="$event.target.blur()"
|
||||
>
|
||||
<slot name="avatar" :link="link" :is-active="isActive">
|
||||
<UAvatar
|
||||
v-if="link.avatar"
|
||||
v-bind="{ size: ui.avatar.size, ...link.avatar }"
|
||||
:class="[ui.avatar.base]"
|
||||
/>
|
||||
</slot>
|
||||
<slot name="icon" :link="link" :is-active="isActive">
|
||||
<UIcon
|
||||
v-if="link.icon"
|
||||
:name="link.icon"
|
||||
:class="twMerge(twJoin(ui.icon.base, isActive ? ui.icon.active : ui.icon.inactive), link.iconClass)"
|
||||
/>
|
||||
</slot>
|
||||
<slot :link="link" :is-active="isActive">
|
||||
<span v-if="link.label" :class="twMerge(ui.label, link.labelClass)">
|
||||
<span v-if="isActive" class="sr-only">
|
||||
Current page:
|
||||
</span>
|
||||
{{ link.label }}
|
||||
</span>
|
||||
</slot>
|
||||
<slot name="badge" :link="link" :is-active="isActive">
|
||||
<span v-if="link.badge" :class="[ui.badge.base, isActive ? ui.badge.active : ui.badge.inactive]">
|
||||
{{ link.badge }}
|
||||
</span>
|
||||
</slot>
|
||||
</ULink>
|
||||
</li>
|
||||
<UDivider v-if="sectionIndex < linkSections.length - 1" :ui="ui.divider" />
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { toRef, defineComponent } from 'vue'
|
||||
import { toRef, defineComponent, computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { twMerge, twJoin } from 'tailwind-merge'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import ULink from '../elements/Link.vue'
|
||||
import UDivider from '../layout/Divider.vue'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
import { mergeConfig, omit } from '../../utils'
|
||||
import type { VerticalNavigationLink, Strategy } from '../../types'
|
||||
@@ -56,12 +66,13 @@ export default defineComponent({
|
||||
components: {
|
||||
UIcon,
|
||||
UAvatar,
|
||||
ULink
|
||||
ULink,
|
||||
UDivider
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
links: {
|
||||
type: Array as PropType<VerticalNavigationLink[]>,
|
||||
type: Array as PropType<VerticalNavigationLink[][] | VerticalNavigationLink[]>,
|
||||
default: () => []
|
||||
},
|
||||
class: {
|
||||
@@ -76,11 +87,18 @@ export default defineComponent({
|
||||
setup (props) {
|
||||
const { ui, attrs } = useUI('verticalNavigation', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const linkSections = computed(() => {
|
||||
return (Array.isArray(props.links[0]) ? props.links : [props.links]) as VerticalNavigationLink[][]
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
attrs,
|
||||
omit
|
||||
omit,
|
||||
linkSections,
|
||||
twMerge,
|
||||
twJoin
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<HPopover ref="popover" v-slot="{ open: headlessOpen, close }" :class="ui.wrapper" v-bind="attrs" @mouseleave="onMouseLeave">
|
||||
<!-- eslint-disable-next-line vue/no-template-shadow -->
|
||||
<HPopover ref="popover" v-slot="{ open, close }" :class="ui.wrapper" v-bind="attrs" @mouseleave="onMouseLeave">
|
||||
<HPopoverButton
|
||||
ref="trigger"
|
||||
as="div"
|
||||
@@ -8,7 +9,7 @@
|
||||
role="button"
|
||||
@mouseover="onMouseOver"
|
||||
>
|
||||
<slot :open="(open !== undefined) ? open : headlessOpen" :close="close">
|
||||
<slot :open="open" :close="close">
|
||||
<button :disabled="disabled">
|
||||
Open
|
||||
</button>
|
||||
@@ -16,16 +17,16 @@
|
||||
</HPopoverButton>
|
||||
|
||||
<Transition v-if="overlay" appear v-bind="ui.overlay.transition">
|
||||
<div v-if="(open !== undefined) ? open : headlessOpen" :class="[ui.overlay.base, ui.overlay.background]" @click="$emit('update:open')" />
|
||||
<div v-if="open" :class="[ui.overlay.base, ui.overlay.background]" />
|
||||
</Transition>
|
||||
|
||||
<div v-if="(open !== undefined) ? open : headlessOpen" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseover="onMouseOver">
|
||||
<div v-if="open" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseover="onMouseOver">
|
||||
<Transition appear v-bind="ui.transition">
|
||||
<div>
|
||||
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(ui.arrow)" />
|
||||
|
||||
<HPopoverPanel :class="[ui.base, ui.background, ui.ring, ui.rounded, ui.shadow]" static>
|
||||
<slot name="panel" :open="(open !== undefined) ? open : headlessOpen" :close="close" />
|
||||
<slot name="panel" :open="open" :close="close" />
|
||||
</HPopoverPanel>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -34,7 +35,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ref, toRef, onMounted, defineComponent } from 'vue'
|
||||
import { computed, ref, toRef, onMounted, defineComponent, watch } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { Popover as HPopover, PopoverButton as HPopoverButton, PopoverPanel as HPopoverPanel } from '@headlessui/vue'
|
||||
@@ -95,15 +96,15 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
emits: ['update:open'],
|
||||
setup (props) {
|
||||
setup (props, { emit }) {
|
||||
const { ui, attrs } = useUI('popover', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popper.value)
|
||||
|
||||
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/popover/popover.ts#L151
|
||||
const popover = ref<any>(null)
|
||||
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/popover/popover.ts#L151
|
||||
const popoverApi = ref<any>(null)
|
||||
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
@@ -116,18 +117,39 @@ export default defineComponent({
|
||||
}
|
||||
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
|
||||
popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
|
||||
|
||||
if (props.open) {
|
||||
popoverApi.value?.togglePopover()
|
||||
}
|
||||
})
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
if (props.mode !== 'hover') {
|
||||
return {}
|
||||
}
|
||||
|
||||
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8
|
||||
const placement = popper.value.placement?.split('-')[0]
|
||||
const padding = `${offsetDistance}px`
|
||||
|
||||
return props.mode === 'hover' ? {
|
||||
paddingTop: padding,
|
||||
paddingBottom: padding,
|
||||
paddingLeft: padding,
|
||||
paddingRight: padding
|
||||
} : {}
|
||||
if (placement === 'top' || placement === 'bottom') {
|
||||
return {
|
||||
paddingTop: padding,
|
||||
paddingBottom: padding
|
||||
}
|
||||
} else if (placement === 'left' || placement === 'right') {
|
||||
return {
|
||||
paddingLeft: padding,
|
||||
paddingRight: padding
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
paddingTop: padding,
|
||||
paddingBottom: padding,
|
||||
paddingLeft: padding,
|
||||
paddingRight: padding
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function onMouseOver () {
|
||||
@@ -170,6 +192,24 @@ export default defineComponent({
|
||||
}, props.closeDelay)
|
||||
}
|
||||
|
||||
watch(() => props.open, (newValue: boolean, oldValue: boolean) => {
|
||||
if (!popoverApi.value) return
|
||||
if (oldValue === undefined || newValue === oldValue) return
|
||||
|
||||
if (newValue) {
|
||||
// No `openPopover` method and `popoverApi.value.togglePopover` won't work because of the `watch` below
|
||||
popoverApi.value.popoverState = 0
|
||||
} else {
|
||||
popoverApi.value.closePopover()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => popoverApi.value?.popoverState, (newValue: number, oldValue: number) => {
|
||||
if (oldValue === undefined || newValue === oldValue) return
|
||||
|
||||
emit('update:open', newValue === 0)
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
|
||||
@@ -9,7 +9,10 @@ export const _useShortcuts = () => {
|
||||
|
||||
const activeElement = useActiveElement()
|
||||
const usingInput = computed(() => {
|
||||
const usingInput = !!(activeElement.value?.tagName === 'INPUT' || activeElement.value?.tagName === 'TEXTAREA' || activeElement.value?.contentEditable === 'true')
|
||||
const tagName = activeElement.value?.tagName
|
||||
const contentEditable = activeElement.value?.contentEditable
|
||||
|
||||
const usingInput = !!(tagName === 'INPUT' || tagName === 'TEXTAREA' || contentEditable === 'true' || contentEditable === 'plaintext-only')
|
||||
|
||||
if (usingInput) {
|
||||
return ((activeElement.value as any)?.name as string) || true
|
||||
|
||||
5
src/runtime/types/alert.d.ts
vendored
5
src/runtime/types/alert.d.ts
vendored
@@ -1,7 +1,12 @@
|
||||
import { alert } from '../ui.config'
|
||||
import type { NestedKeyOf, ExtractDeepKey, ExtractDeepObject } from '.'
|
||||
import type { Button } from './button'
|
||||
import colors from '#ui-colors'
|
||||
import type { AppConfig } from 'nuxt/schema'
|
||||
|
||||
export type AlertColor = keyof typeof alert.color | ExtractDeepKey<AppConfig, ['ui', 'alert', 'color']> | typeof colors[number]
|
||||
export type AlertVariant = keyof typeof alert.variant | ExtractDeepKey<AppConfig, ['ui', 'alert', 'variant']> | NestedKeyOf<typeof alert.color> | NestedKeyOf<ExtractDeepObject<AppConfig, ['ui', 'alert', 'color']>>
|
||||
|
||||
export interface AlertAction extends Button {
|
||||
click?: Function
|
||||
}
|
||||
|
||||
1
src/runtime/types/breadcrumb.d.ts
vendored
1
src/runtime/types/breadcrumb.d.ts
vendored
@@ -2,6 +2,7 @@ import type { Link } from './link'
|
||||
|
||||
export interface BreadcrumbLink extends Link {
|
||||
label: string
|
||||
labelClass?: string
|
||||
icon?: string
|
||||
iconClass?: string
|
||||
// FIXME: This is a workaround for `link.to` not being resolved although it extends `NuxtLinkProps`
|
||||
|
||||
2
src/runtime/types/dropdown.d.ts
vendored
2
src/runtime/types/dropdown.d.ts
vendored
@@ -3,11 +3,13 @@ import type { Avatar } from './avatar'
|
||||
|
||||
export interface DropdownItem extends NuxtLinkProps {
|
||||
label: string
|
||||
labelClass?: string
|
||||
slot?: string
|
||||
icon?: string
|
||||
iconClass?: string
|
||||
avatar?: Avatar
|
||||
shortcuts?: string[]
|
||||
disabled?: boolean
|
||||
class?: string
|
||||
click?: Function
|
||||
}
|
||||
|
||||
1
src/runtime/types/form.d.ts
vendored
1
src/runtime/types/form.d.ts
vendored
@@ -15,6 +15,7 @@ export interface Form<T> {
|
||||
errors: Ref<FormError[]>
|
||||
setErrors(errs: FormError[], path?: string): void
|
||||
getErrors(path?: string): FormError[]
|
||||
submit(): Promise<void>
|
||||
}
|
||||
|
||||
export type FormSubmitEvent<T> = SubmitEvent & { data: T }
|
||||
|
||||
1
src/runtime/types/vertical-navigation.d.ts
vendored
1
src/runtime/types/vertical-navigation.d.ts
vendored
@@ -3,6 +3,7 @@ import type { Avatar } from './avatar'
|
||||
|
||||
export interface VerticalNavigationLink extends Link {
|
||||
label: string
|
||||
labelClass?: string
|
||||
icon?: string
|
||||
iconClass?: string
|
||||
avatar?: Avatar
|
||||
|
||||
@@ -76,24 +76,24 @@ export default {
|
||||
wrapper: 'absolute inset-y-0 start-0 flex items-center',
|
||||
pointer: 'pointer-events-none',
|
||||
padding: {
|
||||
'2xs': 'ps-2',
|
||||
xs: 'ps-2.5',
|
||||
sm: 'ps-2.5',
|
||||
md: 'ps-3',
|
||||
lg: 'ps-3.5',
|
||||
xl: 'ps-3.5'
|
||||
'2xs': 'px-2',
|
||||
xs: 'px-2.5',
|
||||
sm: 'px-2.5',
|
||||
md: 'px-3',
|
||||
lg: 'px-3.5',
|
||||
xl: 'px-3.5'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
wrapper: 'absolute inset-y-0 end-0 flex items-center',
|
||||
pointer: 'pointer-events-none',
|
||||
padding: {
|
||||
'2xs': 'pe-2',
|
||||
xs: 'pe-2.5',
|
||||
sm: 'pe-2.5',
|
||||
md: 'pe-3',
|
||||
lg: 'pe-3.5',
|
||||
xl: 'pe-3.5'
|
||||
'2xs': 'px-2',
|
||||
xs: 'px-2.5',
|
||||
sm: 'px-2.5',
|
||||
md: 'px-3',
|
||||
lg: 'px-3.5',
|
||||
xl: 'px-3.5'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
63
src/runtime/ui.config/forms/inputMenu.ts
Normal file
63
src/runtime/ui.config/forms/inputMenu.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { arrow } from '../popper'
|
||||
|
||||
export default {
|
||||
container: 'z-20 group',
|
||||
trigger: 'inline-flex w-full',
|
||||
width: 'w-full',
|
||||
height: 'max-h-60',
|
||||
base: 'relative focus:outline-none overflow-y-auto scroll-py-1',
|
||||
background: 'bg-white dark:bg-gray-800',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
padding: 'p-1',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
|
||||
option: {
|
||||
base: 'cursor-default select-none relative flex items-center justify-between gap-1',
|
||||
rounded: 'rounded-md',
|
||||
padding: 'px-2 py-1.5',
|
||||
size: 'text-sm',
|
||||
color: 'text-gray-900 dark:text-white',
|
||||
container: 'flex items-center gap-2 min-w-0',
|
||||
active: 'bg-gray-100 dark:bg-gray-900',
|
||||
inactive: '',
|
||||
selected: 'pe-7',
|
||||
disabled: 'cursor-not-allowed opacity-50',
|
||||
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 h-4 w-4',
|
||||
active: 'text-gray-900 dark:text-white',
|
||||
inactive: 'text-gray-400 dark:text-gray-500'
|
||||
},
|
||||
selectedIcon: {
|
||||
wrapper: 'absolute inset-y-0 end-0 flex items-center',
|
||||
padding: 'pe-2',
|
||||
base: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
size: '3xs' as const
|
||||
},
|
||||
chip: {
|
||||
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
|
||||
}
|
||||
},
|
||||
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
|
||||
transition: {
|
||||
leaveActiveClass: 'transition ease-in duration-100',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0'
|
||||
},
|
||||
popper: {
|
||||
placement: 'bottom-end'
|
||||
},
|
||||
default: {
|
||||
selectedIcon: 'i-heroicons-check-20-solid',
|
||||
trailingIcon: 'i-heroicons-chevron-down-20-solid'
|
||||
},
|
||||
arrow: {
|
||||
...arrow,
|
||||
ring: 'before:ring-1 before:ring-gray-200 dark:before:ring-gray-700',
|
||||
background: 'before:bg-white dark:before:bg-gray-700'
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,15 @@
|
||||
import { arrow } from '../popper'
|
||||
import inputMenu from './inputMenu'
|
||||
|
||||
export default {
|
||||
container: 'z-20 group',
|
||||
trigger: 'inline-flex w-full',
|
||||
...inputMenu,
|
||||
select: 'inline-flex items-center text-left cursor-default',
|
||||
width: 'w-full',
|
||||
height: 'max-h-60',
|
||||
base: 'relative focus:outline-none overflow-y-auto scroll-py-1',
|
||||
background: 'bg-white dark:bg-gray-800',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
padding: 'p-1',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||
input: 'block w-[calc(100%+0.5rem)] focus:ring-transparent text-sm px-3 py-1.5 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border-0 border-b border-gray-200 dark:border-gray-700 focus:border-inherit sticky -top-1 -mt-1 mb-1 -mx-1 z-10 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none',
|
||||
input: 'block w-[calc(100%+0.5rem)] focus:ring-transparent text-sm px-3 py-1.5 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border-0 border-b border-gray-200 dark:border-gray-700 sticky -top-1 -mt-1 mb-1 -mx-1 z-10 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none',
|
||||
required: 'absolute inset-0 w-px opacity-0 cursor-default',
|
||||
label: 'block truncate',
|
||||
option: {
|
||||
base: 'cursor-default select-none relative flex items-center justify-between gap-1',
|
||||
rounded: 'rounded-md',
|
||||
padding: 'px-2 py-1.5',
|
||||
size: 'text-sm',
|
||||
color: 'text-gray-900 dark:text-white',
|
||||
container: 'flex items-center gap-2 min-w-0',
|
||||
active: 'bg-gray-100 dark:bg-gray-900',
|
||||
inactive: '',
|
||||
selected: 'pe-7',
|
||||
disabled: 'cursor-not-allowed opacity-50',
|
||||
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
|
||||
create: 'block truncate',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 h-4 w-4',
|
||||
active: 'text-gray-900 dark:text-white',
|
||||
inactive: 'text-gray-400 dark:text-gray-500'
|
||||
},
|
||||
selectedIcon: {
|
||||
wrapper: 'absolute inset-y-0 end-0 flex items-center',
|
||||
padding: 'pe-2',
|
||||
base: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
size: '3xs' as const
|
||||
},
|
||||
chip: {
|
||||
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
|
||||
}
|
||||
...inputMenu.option,
|
||||
create: 'block truncate'
|
||||
},
|
||||
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
|
||||
transition: {
|
||||
@@ -57,7 +22,8 @@ export default {
|
||||
},
|
||||
default: {
|
||||
selectedIcon: 'i-heroicons-check-20-solid',
|
||||
clearOnClose: false
|
||||
clearSearchOnClose: false,
|
||||
showCreateOptionWhen: 'empty'
|
||||
},
|
||||
arrow: {
|
||||
...arrow,
|
||||
|
||||
@@ -18,6 +18,7 @@ export { default as meterGroup } from './elements/meterGroup'
|
||||
|
||||
// Forms
|
||||
export { default as input } from './forms/input'
|
||||
export { default as inputMenu } from './forms/inputMenu'
|
||||
export { default as formGroup } from './forms/formGroup'
|
||||
export { default as textarea } from './forms/textarea'
|
||||
export { default as select } from './forms/select'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
base: 'overflow-hidden',
|
||||
base: '',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
divide: 'divide-y divide-gray-200 dark:divide-gray-800',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
|
||||
@@ -20,4 +20,4 @@ export default {
|
||||
background: '',
|
||||
padding: 'px-4 py-4 sm:px-6'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export default {
|
||||
ol: 'flex items-center gap-x-1.5',
|
||||
li: 'flex items-center gap-x-1.5 text-gray-500 dark:text-gray-400 text-sm',
|
||||
base: 'flex items-center gap-x-1.5 group font-semibold',
|
||||
label: '',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 w-4 h-4',
|
||||
active: '',
|
||||
|
||||
@@ -23,5 +23,10 @@ export default {
|
||||
base: 'relative ms-auto inline-block py-0.5 px-2 text-xs rounded-md -me-1 -my-0.5',
|
||||
active: 'bg-white dark:bg-gray-900',
|
||||
inactive: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white group-hover:bg-white dark:group-hover:bg-gray-900'
|
||||
},
|
||||
divider: {
|
||||
wrapper: {
|
||||
base: 'p-2'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ export const arrow = {
|
||||
rounded: 'before:rounded-sm',
|
||||
background: 'before:bg-gray-200 dark:before:bg-gray-800',
|
||||
shadow: 'before:shadow',
|
||||
placement: 'group-data-[popper-placement*="right"]:-left-1 group-data-[popper-placement*="left"]:-right-1 group-data-[popper-placement*="top"]:-bottom-1 group-data-[popper-placement*="bottom"]:-top-1'
|
||||
// eslint-disable-next-line quotes
|
||||
placement: `group-data-[popper-placement*='right']:-left-1 group-data-[popper-placement*='left']:-right-1 group-data-[popper-placement*='top']:-bottom-1 group-data-[popper-placement*='bottom']:-top-1`
|
||||
}
|
||||
|
||||
@@ -2,14 +2,22 @@ import { defu, createDefu } from 'defu'
|
||||
import { extendTailwindMerge } from 'tailwind-merge'
|
||||
import type { Strategy } from '../types'
|
||||
|
||||
const customTwMerge = extendTailwindMerge({
|
||||
classGroups: {
|
||||
icons: [(classPart: string) => /^i-/.test(classPart)]
|
||||
const customTwMerge = extendTailwindMerge<string, string>({
|
||||
extend: {
|
||||
classGroups: {
|
||||
icons: [(classPart: string) => /^i-/.test(classPart)]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const defuTwMerge = createDefu((obj, key, value, namespace) => {
|
||||
if (namespace !== 'default' && !namespace.startsWith('default.') && typeof obj[key] === 'string' && typeof value === 'string' && obj[key] && value) {
|
||||
if (namespace === 'default' || namespace.startsWith('default.')) {
|
||||
return false
|
||||
}
|
||||
if (namespace.endsWith('avatar') && key === 'size') {
|
||||
return false
|
||||
}
|
||||
if (typeof obj[key] === 'string' && typeof value === 'string' && obj[key] && value) {
|
||||
// @ts-ignore
|
||||
obj[key] = customTwMerge(obj[key], value)
|
||||
return true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
let _id = 0
|
||||
|
||||
export function uid () {
|
||||
return `nuid-${_id++}`
|
||||
_id = (_id + 1) % Number.MAX_SAFE_INTEGER
|
||||
return `nuid-${_id}`
|
||||
}
|
||||
|
||||
@@ -1,46 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import module from '../src/module'
|
||||
import { defu } from 'defu'
|
||||
import { join } from 'pathe'
|
||||
import { loadNuxt } from '@nuxt/kit'
|
||||
import { join } from 'path'
|
||||
import type { NuxtConfig } from '@nuxt/schema'
|
||||
import type * as tailwindcss from 'tailwindcss'
|
||||
type TWConfig = tailwindcss.Config;
|
||||
import type resolveConfig from 'tailwindcss/resolveConfig'
|
||||
|
||||
// TODO: fix these anys
|
||||
async function getTailwindCSSConfig (overrides: any = {}): Promise<[any, any]> {
|
||||
overrides.modules = [module]
|
||||
overrides.ssr = overrides.ssr ?? false
|
||||
overrides.hooks = overrides.hooks ?? {}
|
||||
return new Promise((resolve) => {
|
||||
overrides.hooks['tailwindcss:resolvedConfig'] = async (config: any) => {
|
||||
resolve([config, nuxt])
|
||||
}
|
||||
const nuxt = loadNuxt({
|
||||
cwd: join(process.cwd(), 'fixtures', 'empty'),
|
||||
dev: false,
|
||||
overrides
|
||||
})
|
||||
async function getTailwindCSSConfig (overrides: Partial<NuxtConfig> = {}) {
|
||||
let tailwindConfig: ReturnType<typeof resolveConfig<TWConfig>>
|
||||
const nuxt = await loadNuxt({
|
||||
ready: true,
|
||||
cwd: join(process.cwd(), 'fixtures', 'empty'),
|
||||
dev: false,
|
||||
overrides: defu(overrides, {
|
||||
ssr: false,
|
||||
modules: ['../../src/module'],
|
||||
hooks: {
|
||||
'tailwindcss:resolvedConfig' (config) {
|
||||
tailwindConfig = config
|
||||
}
|
||||
}
|
||||
} satisfies NuxtConfig) as NuxtConfig
|
||||
})
|
||||
const nuxtOptions = structuredClone({
|
||||
plugins: nuxt.options.plugins.map(p => typeof p !== 'string' && ({ src: p.src, mode: p.mode })),
|
||||
_requiredModules: nuxt.options._requiredModules,
|
||||
appConfig: nuxt.options.appConfig
|
||||
})
|
||||
await nuxt.close()
|
||||
|
||||
return {
|
||||
nuxtOptions,
|
||||
tailwindConfig
|
||||
}
|
||||
}
|
||||
|
||||
describe('nuxt', () => {
|
||||
it('should add plugins and modules to nuxt', async () => {
|
||||
const [, lnuxt] = await getTailwindCSSConfig()
|
||||
await lnuxt.then((nuxt: { options: { plugins: any; _requiredModules: any; appConfig: { ui: any } }; close: () => void }) => {
|
||||
expect(nuxt.options.plugins).toContainEqual(
|
||||
expect.objectContaining({
|
||||
src: expect.stringContaining('plugins/colors'),
|
||||
mode: 'all'
|
||||
})
|
||||
)
|
||||
expect(nuxt.options._requiredModules).toContain({
|
||||
'@nuxtjs/color-mode': true,
|
||||
'@nuxtjs/tailwindcss': true
|
||||
const { nuxtOptions } = await getTailwindCSSConfig()
|
||||
expect(nuxtOptions.plugins).toContainEqual(
|
||||
expect.objectContaining({
|
||||
src: expect.stringContaining('plugins/colors'),
|
||||
mode: 'all'
|
||||
})
|
||||
// default values in appConfig
|
||||
expect(nuxt.options.appConfig.ui).toContain({
|
||||
primary: 'green',
|
||||
gray: 'cool'
|
||||
})
|
||||
// TODO: this should be done inside getTailwindCSSConfig
|
||||
nuxt.close()
|
||||
)
|
||||
expect(nuxtOptions._requiredModules).toMatchObject({
|
||||
'@nuxtjs/color-mode': true,
|
||||
'@nuxtjs/tailwindcss': true
|
||||
})
|
||||
// default values in appConfig
|
||||
expect(nuxtOptions.appConfig.ui).toMatchObject({
|
||||
primary: 'green',
|
||||
gray: 'cool'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -68,7 +80,7 @@ describe('tailwindcss config', () => {
|
||||
['bg-(plainBlue|primary)-50', '!', /orange/] // the word "orange" should _not_ be found in any safelist pattern
|
||||
]
|
||||
])('%s', async (_description, tailwindcss, safelistColors, safelistPatterns) => {
|
||||
const [config, _nuxt] = await getTailwindCSSConfig({
|
||||
const { tailwindConfig } = await getTailwindCSSConfig({
|
||||
ui: {
|
||||
safelistColors
|
||||
},
|
||||
@@ -105,19 +117,15 @@ describe('tailwindcss config', () => {
|
||||
continue
|
||||
}
|
||||
if (negate) {
|
||||
expect(config.safelist).not.toContainEqual({
|
||||
expect(tailwindConfig.safelist).not.toContainEqual({
|
||||
pattern: expect.toBeRegExp(safelistPattern)
|
||||
})
|
||||
} else {
|
||||
expect(config.safelist).toContainEqual({
|
||||
expect(tailwindConfig.safelist).toContainEqual({
|
||||
pattern: expect.toBeRegExp(safelistPattern)
|
||||
})
|
||||
}
|
||||
negate = false
|
||||
}
|
||||
|
||||
await _nuxt.then((n: { close: () => void }) => {
|
||||
n.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { mountSuspended } from 'nuxt-vitest/utils'
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
||||
import path from 'path'
|
||||
|
||||
export default async function (nameOrHtml: string, options: any, component: any) {
|
||||
let html
|
||||
let html: string
|
||||
const name = path.parse(component.__file).name
|
||||
if (options === undefined) {
|
||||
const app = {
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Button > renders <UButton icon="i-heroicons-pencil-square" size="sm" color="primary" square variant="solid" /> correctly 1`] = `
|
||||
"<button type=\\"button\\" class=\\"focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 p-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center\\"><span class=\\"i-heroicons-pencil-square flex-shrink-0 h-5 w-5\\" aria-hidden=\\"true\\"></span>
|
||||
"<button type="button" class="focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 p-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center"><span class="i-heroicons-pencil-square flex-shrink-0 h-5 w-5" aria-hidden="true"></span>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</button>"
|
||||
`;
|
||||
|
||||
exports[`Button > renders basic case correctly 1`] = `
|
||||
"<button type=\\"button\\" class=\\"focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center\\">
|
||||
"<button type="button" class="focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center">
|
||||
<!--v-if-->label
|
||||
<!--v-if-->
|
||||
</button>"
|
||||
`;
|
||||
|
||||
exports[`Button > renders black solid correctly 1`] = `
|
||||
"<button type=\\"button\\" class=\\"focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-gray-900 hover:bg-gray-800 disabled:bg-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:disabled:bg-white focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 inline-flex items-center\\">
|
||||
"<button type="button" class="focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-gray-900 hover:bg-gray-800 disabled:bg-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:disabled:bg-white focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 inline-flex items-center">
|
||||
<!--v-if-->label
|
||||
<!--v-if-->
|
||||
</button>"
|
||||
`;
|
||||
|
||||
exports[`Button > renders leading icon correctly 1`] = `
|
||||
"<button type=\\"button\\" class=\\"focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center\\"><span class=\\"heroicons-check flex-shrink-0 h-5 w-5\\" aria-hidden=\\"true\\"></span>label
|
||||
"<button type="button" class="focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center"><span class="heroicons-check flex-shrink-0 h-5 w-5" aria-hidden="true"></span>label
|
||||
<!--v-if-->
|
||||
</button>"
|
||||
`;
|
||||
|
||||
exports[`Button > renders rounded full correctly 1`] = `
|
||||
"<button type=\\"button\\" class=\\"focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-full text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center\\">
|
||||
"<button type="button" class="focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-full text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center">
|
||||
<!--v-if-->label
|
||||
<!--v-if-->
|
||||
</button>"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import Skeleton from '../../../src/runtime/components/layout/Skeleton.vue'
|
||||
import { USkeleton } from '#components'
|
||||
import type { TypeOf } from 'zod'
|
||||
import ComponentRender from '../component-render'
|
||||
|
||||
@@ -7,9 +7,8 @@ describe('Skeleton', () => {
|
||||
it.each([
|
||||
[ 'basic case', { } ],
|
||||
[ '<USkeleton class="h-12 w-12" :ui="{ rounded: \'rounded-full\' }" />' ]
|
||||
// @ts-ignore
|
||||
])('renders %s correctly', async (nameOrHtml: string, options: TypeOf<typeof Skeleton.props>) => {
|
||||
const html = await ComponentRender(nameOrHtml, options, Skeleton)
|
||||
])('renders %s correctly', async (nameOrHtml: string, options?: TypeOf<typeof USkeleton.props>) => {
|
||||
const html = await ComponentRender(nameOrHtml, options, USkeleton)
|
||||
expect(html).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Skeleton > renders <USkeleton class="h-12 w-12" :ui="{ rounded: 'rounded-full' }" /> correctly 1`] = `"<div class=\\"animate-pulse bg-gray-100 dark:bg-gray-800 rounded-full h-12 w-12\\"></div>"`;
|
||||
exports[`Skeleton > renders <USkeleton class="h-12 w-12" :ui="{ rounded: 'rounded-full' }" /> correctly 1`] = `"<div class="animate-pulse bg-gray-100 dark:bg-gray-800 rounded-full h-12 w-12"></div>"`;
|
||||
|
||||
exports[`Skeleton > renders basic case correctly 1`] = `"<div class=\\"animate-pulse bg-gray-100 dark:bg-gray-800 rounded-md\\"></div>"`;
|
||||
exports[`Skeleton > renders basic case correctly 1`] = `"<div class="animate-pulse bg-gray-100 dark:bg-gray-800 rounded-md"></div>"`;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/// <reference types="vitest" />
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineVitestConfig } from 'nuxt-vitest/config'
|
||||
import { defineVitestConfig } from '@nuxt/test-utils/config'
|
||||
|
||||
export default defineVitestConfig({
|
||||
// @ts-ignore
|
||||
test: {
|
||||
testTimeout: 20000,
|
||||
globals: true,
|
||||
Reference in New Issue
Block a user