Compare commits

..

43 Commits

Author SHA1 Message Date
Benjamin Canac
60eea0e46b chore(release): 2.3.0 2023-06-05 12:12:48 +02:00
Benjamin Canac
5e50eb9eb8 fix: use cloneVNode when altering props in render functions
Resolves #252

https://vuejs.org/api/render-function.html#clonevnode
2023-06-05 11:17:31 +02:00
Dominik Opyd
af65683123 docs(github): support for various file extensions (#250) 2023-06-02 11:35:31 +02:00
Benjamin Canac
2c673f5377 fix(CommandPalette): override of closeButton and emptyState props 2023-06-01 17:15:04 +02:00
Benjamin Canac
192b0e6301 fix(Table): override of sortButton and emptyState props 2023-06-01 17:14:45 +02:00
Benjamin Canac
71edb91c4f fix(Avatar): placeholder font size 2023-06-01 16:47:23 +02:00
Benjamin Canac
f9b935f5f5 fix(Badge): remove console.log in validator 2023-06-01 16:24:19 +02:00
Benjamin Canac
23833e92cb chore(Badge): handle color override like buttons 2023-06-01 16:06:07 +02:00
Benjamin Canac
241df7f05e docs: fix toc scroll when duplicated names 2023-06-01 15:41:23 +02:00
Benjamin Canac
130a1f2c54 docs: improve slots sections 2023-06-01 15:29:02 +02:00
Benjamin Canac
c63981e31c docs: :component-card now handle slots 2023-06-01 15:28:39 +02:00
Benjamin Canac
687f0c6f63 docs: improve inline code blocks inside Alert component 2023-06-01 15:28:12 +02:00
Benjamin Canac
f59a92ca15 chore(Input)!: move pointer class inside its own preset class 2023-06-01 15:27:53 +02:00
Benjamin Canac
01fa85c7a3 fix(defineShortcuts): err with input autocomplete that triggers keydown 2023-06-01 15:26:46 +02:00
Benjamin Canac
3434bc7f2b chore(deps): bump @nuxthq/studio 2023-06-01 11:43:47 +02:00
Benjamin Canac
9b1aacb1da docs: move slots sections as h2 2023-06-01 11:39:07 +02:00
Benjamin Canac
8951923a11 fix(SelectMenu): disable on loading 2023-06-01 11:08:16 +02:00
Benjamin Canac
e200d4cc74 chore(package): remove preinstall script 2023-06-01 10:49:36 +02:00
Benjamin Canac
e05619f8c8 chore: add leading and trailing slots
Resolves #246
2023-05-31 23:53:31 +02:00
Benjamin Canac
5ea43ab4e4 chore: uniformize icons in Button / Input / Select / SelectMenu
Also adds `loading` to `Select` and `SelectMenu`
2023-05-31 23:30:52 +02:00
Benjamin Canac
ba44c58a80 chore(SelectMenu)!: remove inline-flex from wrapper to behave like other form elements 2023-05-31 23:22:58 +02:00
Benjamin Canac
490025a981 docs: add Table icons in theming icons section 2023-05-31 18:31:15 +02:00
Benjamin Canac
2966373a86 chore(Table): handle empty-state
Resolves #243
2023-05-31 18:30:49 +02:00
Sylvain Marroufin
8bdb8c45f7 chore(Dropdown): hover mode with padding instead of offset + improve docs (#242)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-05-31 14:28:14 +02:00
Sylvain Marroufin
9827de0b58 docs(textarea): improve props documentation (#241) 2023-05-31 12:49:47 +02:00
Benjamin Canac
23f01fde41 chore(Table): rename preset container to base 2023-05-31 12:11:28 +02:00
Benjamin Canac
f680318e44 docs: fix overflow in table page
Fixes #244
2023-05-31 12:11:12 +02:00
Benjamin Canac
cd2d1eb1fa docs(table): add alert on sort prop without sortable 2023-05-30 18:06:03 +02:00
Benjamin Canac
3ba0aedcba fix(Table): type sort prop 2023-05-30 18:03:24 +02:00
Benjamin Canac
40b6884424 chore(Table): handle default sort and default column direction 2023-05-30 17:59:30 +02:00
Benjamin Canac
a2638c6057 chore(Table): split container divide 2023-05-30 16:23:17 +02:00
Benjamin Canac
6bd5142a37 fix(Table): add missing text-left in th.base 2023-05-30 16:21:54 +02:00
Benjamin Canac
bc1d653857 chore(Table): split preset for th and td 2023-05-30 16:17:17 +02:00
Benjamin Canac
6c215e07a6 chore(deps): bump 2023-05-30 12:18:05 +02:00
Benjamin Canac
272af9d24c fix(Table): missing ref import from vue 2023-05-30 12:17:59 +02:00
Benjamin Canac
cce000ab2b feat: add Table component (#237) 2023-05-30 12:13:57 +02:00
Benjamin Canac
4a99d6a7bb docs: fix notification preset for closeButton and actionButton 2023-05-29 22:45:33 +02:00
Benjamin Canac
4458656be5 chore(Notification)!: rename to closeButton and actionButton for consistency 2023-05-29 21:59:59 +02:00
Benjamin Canac
daca46371c chore(CommandPalette)!: rename props to emptyState and closeButton for consistency 2023-05-29 21:55:08 +02:00
Benjamin Canac
8ee2ac10e7 chore(Toggle)!: rename icons to onIcon / offIcon for consistency 2023-05-29 21:38:51 +02:00
Benjamin Canac
1ebaa5aa00 fix(Button): invalid padding when using square prop 2023-05-29 21:36:28 +02:00
Benjamin Canac
cb43548305 chore(SelectMenu): handle multiple default display + specific placeholder 2023-05-29 11:52:02 +02:00
Benjamin Canac
360084af7c chore(Toggle): improve component
- allow `iconOn` / `iconOff` default values from preset
- `bg-gray-900` on dark mode inside of `bg-white`
- added `name` prop for form control
2023-05-27 22:27:31 +02:00
62 changed files with 2275 additions and 430 deletions

View File

@@ -2,6 +2,43 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.3.0](https://github.com/nuxtlabs/ui/compare/v2.2.1...v2.3.0) (2023-06-05)
### ⚠ BREAKING CHANGES
* **Input:** move pointer class inside its own preset class
* **SelectMenu:** remove `inline-flex` from wrapper to behave like other form elements
* **Notification:** rename to `closeButton` and `actionButton` for consistency
* **CommandPalette:** rename props to `emptyState` and `closeButton` for consistency
* **Toggle:** rename icons to `onIcon` / `offIcon` for consistency
### Features
* add `Table` component ([#237](https://github.com/nuxtlabs/ui/issues/237)) ([cce000a](https://github.com/nuxtlabs/ui/commit/cce000ab2b2af1079216e0e79769703fc4d9933e))
### Bug Fixes
* **Avatar:** placeholder font size ([71edb91](https://github.com/nuxtlabs/ui/commit/71edb91c4ff17a258d6229ed6c6fa6a4b54bdd53))
* **Badge:** remove `console.log` in validator ([f9b935f](https://github.com/nuxtlabs/ui/commit/f9b935f5f59b872fd952a2739d305d6574bf7cf8))
* **Button:** invalid padding when using `square` prop ([1ebaa5a](https://github.com/nuxtlabs/ui/commit/1ebaa5aa00752cd276f7c754d64ac7f85b14dc26))
* **CommandPalette:** override of `closeButton` and `emptyState` props ([2c673f5](https://github.com/nuxtlabs/ui/commit/2c673f5377dbbcdefa6b57eddba2c19d065d5f1f))
* **defineShortcuts:** err with input autocomplete that triggers `keydown` ([01fa85c](https://github.com/nuxtlabs/ui/commit/01fa85c7a3e476d4f710ed3a36c1e815fc986a94))
* **SelectMenu:** disable on loading ([8951923](https://github.com/nuxtlabs/ui/commit/8951923a11d533ebf53dbec5f852800555af253c))
* **Table:** add missing `text-left` in `th.base` ([6bd5142](https://github.com/nuxtlabs/ui/commit/6bd5142a377694599952e0f9b53fde0d0132b61b))
* **Table:** missing `ref` import from `vue` ([272af9d](https://github.com/nuxtlabs/ui/commit/272af9d24c7cda8341e66b57f76acdb9f46ea23e))
* **Table:** override of `sortButton` and `emptyState` props ([192b0e6](https://github.com/nuxtlabs/ui/commit/192b0e63018ae73e8acaa8b4b1771cda2b59bdb6))
* **Table:** type `sort` prop ([3ba0aed](https://github.com/nuxtlabs/ui/commit/3ba0aedcba578350e2fdd9c180505ed8920e0404))
* use `cloneVNode` when altering props in render functions ([5e50eb9](https://github.com/nuxtlabs/ui/commit/5e50eb9eb82571d22e0a2f1a2fe985addf7efe18)), closes [#252](https://github.com/nuxtlabs/ui/issues/252)
* **CommandPalette:** rename props to `emptyState` and `closeButton` for consistency ([daca463](https://github.com/nuxtlabs/ui/commit/daca46371cab1344bd87ffb0abe0f7e9cdb08609))
* **Input:** move pointer class inside its own preset class ([f59a92c](https://github.com/nuxtlabs/ui/commit/f59a92ca1533a44e17fbc8b7945bdaa9a83e805a))
* **Notification:** rename to `closeButton` and `actionButton` for consistency ([4458656](https://github.com/nuxtlabs/ui/commit/4458656be5547fc9505a5c4758bea4818ada408b))
* **SelectMenu:** remove `inline-flex` from wrapper to behave like other form elements ([ba44c58](https://github.com/nuxtlabs/ui/commit/ba44c58a80252a4394fcf2f84611ea2696883120))
* **Toggle:** rename icons to `onIcon` / `offIcon` for consistency ([8ee2ac1](https://github.com/nuxtlabs/ui/commit/8ee2ac10e7eda4c54418f613a5ef87dd89e1f7eb))
### [2.2.1](https://github.com/nuxtlabs/ui/compare/v2.2.0...v2.2.1) (2023-05-27)

View File

@@ -2,7 +2,7 @@
<component
:is="to ? NuxtLink : 'div'"
:to="to"
class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm leading-6 my-5 last:mb-0 font-normal group relative"
class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm leading-6 my-5 last:mb-0 font-normal group relative prose-code:bg-white dark:prose-code:bg-gray-900"
:class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed hover:text-gray-800 dark:hover:text-gray-200' : '']"
>
<UIcon v-if="!!to" name="i-heroicons-link-20-solid" class="w-3 h-3 absolute right-2 top-2 text-gray-400 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400" />

View File

@@ -2,11 +2,11 @@
<div>
<div v-if="propsToSelect.length" class="relative flex border border-gray-200 dark:border-gray-700 rounded-t-md overflow-hidden not-prose">
<div v-for="prop in propsToSelect" :key="prop.name" class="flex flex-col gap-0.5 justify-between py-1.5 font-medium bg-gray-50 dark:bg-gray-800 border-r border-r-gray-200 dark:border-r-gray-700">
<label :for="prop.name" class="block text-xs px-3 font-medium text-gray-400 dark:text-gray-500 -my-px">{{ prop.label }}</label>
<label :for="`prop-${prop.name}`" class="block text-xs px-3 font-medium text-gray-400 dark:text-gray-500 -my-px">{{ prop.label }}</label>
<UCheckbox
v-if="prop.type === 'boolean'"
v-model="componentProps[prop.name]"
:name="prop.name"
:name="`prop-${prop.name}`"
variant="none"
class="justify-center"
/>
@@ -14,7 +14,7 @@
v-else-if="prop.type === 'string' && prop.options.length"
v-model="componentProps[prop.name]"
:options="prop.options"
:name="prop.name"
:name="`prop-${prop.name}`"
:label="componentProps[prop.name]"
variant="none"
class="inline-flex"
@@ -26,7 +26,7 @@
v-else
:model-value="componentProps[prop.name]"
:type="prop.type === 'number' ? 'number' : 'text'"
:name="prop.name"
:name="`prop-${prop.name}`"
variant="none"
autocomplete="off"
:ui="{ custom: '!py-0' }"
@@ -35,9 +35,15 @@
</div>
</div>
<div class="flex border border-b-0 border-gray-200 dark:border-gray-700 relative not-prose" :class="[{ 'p-4': padding }, propsToSelect.length ? 'border-t-0' : 'rounded-t-md', backgroundClass]">
<div class="flex border border-b-0 border-gray-200 dark:border-gray-700 relative not-prose" :class="[{ 'p-4': padding }, propsToSelect.length ? 'border-t-0' : 'rounded-t-md', backgroundClass, overflowClass]">
<component :is="name" v-model="vModel" v-bind="fullProps">
<ContentSlot v-if="$slots.default" :use="$slots.default" />
<template v-for="slot in Object.keys(slots || {})" :key="slot" #[slot]>
<ClientOnly>
<ContentSlot v-if="$slots[slot]" :use="$slots[slot]" />
</ClientOnly>
</template>
</component>
</div>
@@ -67,6 +73,10 @@ const props = defineProps({
type: String,
default: null
},
slots: {
type: Object,
default: null
},
baseProps: {
type: Object,
default: () => ({})
@@ -86,6 +96,10 @@ const props = defineProps({
backgroundClass: {
type: String,
default: 'bg-white dark:bg-gray-900'
},
overflowClass: {
type: String,
default: ''
}
})
@@ -150,7 +164,14 @@ const code = computed(() => {
code += ` ${(prop?.type === 'boolean' && value !== true) || typeof value === 'object' ? ':' : ''}${key === 'modelValue' ? 'value' : useKebabCase(key)}${prop?.type === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}`
}
if (props.code) {
if (props.slots) {
code += `>
${Object.entries(props.slots).map(([key, value]) => `<template #${key}>
${value}
</template>`).join('\n ')}
</${name}>`
} else if (props.code) {
const lineBreaks = (props.code.match(/\n/g) || []).length
if (lineBreaks > 1) {
code += `>
@@ -183,7 +204,7 @@ function renderObject (obj: any) {
return obj
}
const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify(componentProps)}`, () => transformContent('content:_markdown.md', code.value, {
const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify(props)}`, () => transformContent('content:_markdown.md', code.value, {
highlight: {
theme: {
light: 'material-lighter',

View File

@@ -1,6 +1,6 @@
<template>
<div class="[&>div>pre]:!rounded-t-none">
<div class="flex border border-gray-200 dark:border-gray-700 relative not-prose rounded-t-md" :class="[{ 'p-4': padding, 'rounded-b-md': !$slots.code, 'border-b-0': !!$slots.code }, backgroundClass]">
<div class="flex border border-gray-200 dark:border-gray-700 relative not-prose rounded-t-md" :class="[{ 'p-4': padding, 'rounded-b-md': !$slots.code, 'border-b-0': !!$slots.code }, backgroundClass, overflowClass]">
<ContentSlot v-if="$slots.default" :use="$slots.default" />
</div>
@@ -17,6 +17,10 @@ defineProps({
backgroundClass: {
type: String,
default: 'bg-white dark:bg-gray-900'
},
overflowClass: {
type: String,
default: ''
}
})
</script>

View File

@@ -0,0 +1,16 @@
<script setup>
const items = [
[{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
}]
]
</script>
<template>
<UDropdown :items="items" mode="hover" :popper="{ placement: 'bottom-start' }">
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
</UDropdown>
</template>

View File

@@ -0,0 +1,18 @@
<template>
<UInput v-model="q" name="q" placeholder="Search..." icon="i-heroicons-magnifying-glass-20-solid" :ui="{ icon: { trailing: { pointer: '' } } }">
<template #trailing>
<UButton
v-show="q !== ''"
color="gray"
variant="link"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
@click="q = ''"
/>
</template>
</UInput>
</template>
<script setup lang="ts">
const q = ref('')
</script>

View File

@@ -6,10 +6,10 @@ const selected = ref(people[3])
<template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton>
<UButton color="gray">
{{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="[open && 'transform rotate-90']" />
</UButton>
</USelectMenu>
</template>

View File

@@ -5,10 +5,5 @@ const selected = ref([])
</script>
<template>
<USelectMenu v-model="selected" :options="people" multiple>
<template #label>
<span v-if="selected.length" class="font-medium truncate">{{ selected.join(', ') }}</span>
<span v-else class="block truncate text-gray-400 dark:text-gray-500">Select people</span>
</template>
</USelectMenu>
<USelectMenu v-model="selected" :options="people" multiple placeholder="Select people" />
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref([])
</script>
<template>
<USelectMenu v-model="selected" :options="people" multiple>
<template #label>
<span v-if="selected.length" class="truncate">{{ selected.join(', ') }}</span>
<span v-else>Select people</span>
</template>
</USelectMenu>
</template>

View File

@@ -0,0 +1,43 @@
<script setup>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :rows="people" />
</template>

View File

@@ -0,0 +1,59 @@
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'User name'
}, {
key: 'title',
label: 'Job position'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :columns="columns" :rows="people" />
</template>

View File

@@ -0,0 +1,68 @@
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}]
const selectedColumns = ref([...columns])
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<div>
<div class="flex px-3 py-3.5 border-b border-gray-200 dark:border-gray-700">
<USelectMenu v-model="selectedColumns" :options="columns" multiple placeholder="Columns" />
</div>
<UTable :columns="selectedColumns" :rows="people" />
</div>
</template>

View File

@@ -0,0 +1,64 @@
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name',
sortable: true
}, {
key: 'title',
label: 'Title',
sortable: true
}, {
key: 'email',
label: 'Email',
sortable: true,
direction: 'desc'
}, {
key: 'role',
label: 'Role'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :columns="columns" :rows="people" :sort="{ column: 'title' }" />
</template>

View File

@@ -0,0 +1,80 @@
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
const q = ref('')
const filteredRows = computed(() => {
if (!q.value) {
return people
}
return people.filter((person) => {
return Object.values(person).some((value) => {
return String(value).toLowerCase().includes(q.value.toLowerCase())
})
})
})
</script>
<template>
<div>
<div class="flex px-3 py-3.5 border-b border-gray-200 dark:border-gray-700">
<UInput v-model="q" placeholder="Filter people..." />
</div>
<UTable :rows="filteredRows" :columns="columns" />
</div>
</template>

View File

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

View File

@@ -0,0 +1,91 @@
<script setup>
const columns = [{
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}, {
key: 'actions'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
const items = (row) => [
[{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
click: () => console.log('Edit', row.id)
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid'
}], [{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'i-heroicons-arrow-right-circle-20-solid'
}], [{
label: 'Delete',
icon: 'i-heroicons-trash-20-solid'
}]
]
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" :columns="columns">
<template #name-data="{ row }">
<span :class="[selected.find(person => person.id === row.id) && 'text-primary-500 dark:text-primary-400']">{{ row.name }}</span>
</template>
<template #actions-data="{ row }">
<UDropdown :items="items(row)">
<UButton color="gray" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
</UDropdown>
</template>
</UTable>
</template>

View File

@@ -17,8 +17,8 @@ const groups = computed(() => navigation.value.map(item => ({
}))
})))
const close = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-x-mark', color: 'black', variant: 'ghost', size: 'lg', padded: false }) : null)
const empty = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-magnifying-glass', queryLabel: 'No results' }) : ({ icon: '', label: 'No recent searches' }))
const closeButton = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-x-mark', color: 'black', variant: 'ghost', size: 'lg', padded: false }) : null)
const emptyState = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-magnifying-glass', queryLabel: 'No results' }) : ({ icon: '', label: 'No recent searches' }))
const ui = {
wrapper: 'flex flex-col flex-1 min-h-0 bg-gray-50 dark:bg-gray-800',
@@ -50,7 +50,7 @@ const ui = {
}
}
},
empty: {
emptyState: {
wrapper: 'flex flex-col items-center justify-center flex-1 py-9',
label: 'text-sm text-center text-gray-500 dark:text-gray-400',
queryLabel: 'text-lg text-center text-gray-900 dark:text-white',
@@ -64,8 +64,8 @@ const ui = {
ref="commandPaletteRef"
:groups="groups"
:ui="ui"
:close="close"
:empty="empty"
:close-button="closeButton"
:empty-state="emptyState"
:autoselect="false"
command-attribute="title"
:fuse="{

View File

@@ -22,7 +22,7 @@
label="GitHub"
icon="i-simple-icons-github"
color="white"
:to="`https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/${page._dir}/${page.title.replace(' ', '')}.vue`"
:to="`https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/${page._dir}/${page.title.replace(' ', '')}${page.github.suffix || '.vue'}`"
/>
</div>
</div>

View File

@@ -166,6 +166,7 @@ export default defineAppConfig({
},
select: {
default: {
loadingIcon: 'i-octicon-sync-24',
trailingIcon: 'i-octicon-chevron-down-24'
}
},
@@ -176,7 +177,7 @@ export default defineAppConfig({
},
notification: {
default: {
close: {
closeButton: {
icon: 'i-octicon-x-24'
}
}
@@ -186,10 +187,22 @@ export default defineAppConfig({
icon: 'i-octicon-search-24',
loadingIcon: 'i-octicon-sync-24',
selectedIcon: 'i-octicon-check-24',
empty: {
emptyState: {
icon: 'i-octicon-search-24'
}
}
},
table: {
default: {
sortAscIcon: 'i-octicon-sort-asc-24',
sortDescIcon: 'i-octicon-sort-desc-24',
sortButton: {
icon: 'i-octicon-arrow-switch-24'
},
emptyState: {
icon: 'i-octicon-database-24'
}
}
}
}
})

View File

@@ -54,6 +54,7 @@ If there's an `alt` prop initials will be displayed on top of the background, cu
---
props:
alt: 'Benjamin Canac'
size: 'sm'
---
::

View File

@@ -275,6 +275,48 @@ code: |
:u-button{icon="i-heroicons-chevron-down-20-solid" color="gray"}
::
## Slots
### `leading`
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" />
baseProps:
color: 'gray'
props:
label: Button
color: 'gray'
excludedProps:
- color
---
#leading
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
::
### `trailing`
Use the `#trailing` slot to set the content of the trailing icon.
::component-card
---
slots:
trailing: <UIcon name="i-heroicons-arrow-right-20-solid" />
props:
label: Button
color: 'gray'
excludedProps:
- color
---
#trailing
:u-icon{name="i-heroicons-arrow-right-20-solid"}
::
## Props
:component-props

View File

@@ -8,9 +8,20 @@ headlessui:
## Usage
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.
- `icon` - The icon of the item.
- `avatar` - The avatar of the item. You can pass all the props of the [Avatar](/elements/avatar) component.
- `shortcuts` - The shortcuts of the item.
- `disabled` - Whether the item is disabled.
- `click` - The click handler of the item.
You can also pass properties from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
::component-example
#default
:dropdown-example
:dropdown-example-basic
#code
```vue
@@ -55,6 +66,35 @@ const items = [
```
::
### Mode
Use the `mode` prop to switch between `click` and `hover` modes.
::component-example
#default
:dropdown-example-mode
#code
```vue
<script setup>
const items = [
[{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
}]
]
</script>
<template>
<UDropdown :items="items" mode="hover" :popper="{ placement: 'bottom-start' }">
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
</UDropdown>
</template>
```
::
## Props
:component-props

View File

@@ -142,6 +142,75 @@ excludedProps:
---
::
## Slots
### `leading`
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" />
baseProps:
name: 'input'
placeholder: 'Search...'
---
#leading
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
::
### `trailing`
Use the `#trailing` slot to set the content of the trailing icon.
::component-card
---
slots:
trailing: <span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
baseProps:
name: 'input'
placeholder: 'Search...'
---
#trailing
[EUR]{class="text-gray-500 dark:text-gray-400 text-xs"}
::
You can for example create a clearable Input by injecting a [Button](/elements/button) in the `trailing` slot that displays when some text is entered.
::component-example
#default
:input-example-clearable
#code
```vue
<template>
<UInput v-model="q" name="q" placeholder="Search..." icon="i-heroicons-magnifying-glass-20-solid" :ui="{ icon: { trailing: { pointer: '' } } }">
<template #trailing>
<UButton
v-show="q !== ''"
color="gray"
variant="link"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
@click="q = ''"
/>
</template>
</UInput>
</template>
<script setup lang="ts">
const q = ref('')
</script>
```
::
::alert{icon="i-heroicons-exclamation-triangle-20-solid"}
As leading and trailing icons are wrapped around a `pointer-events-none` class, if you inject a clickable element in the slot, you need to remove this class to make it clickable by adding `:ui="{ icon: { trailing: { pointer: '' } } }"` to the Input.
::
## Props
:component-props

View File

@@ -85,6 +85,20 @@ props:
---
::
### Rows
Use the `rows` prop to set the number of rows of the Textarea.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
rows: 1
---
::
### Disabled
Use the `disabled` prop to disable the Textarea.
@@ -99,6 +113,35 @@ props:
---
::
### Autoresize
Use the `autoresize` prop to enable the autoresize. Writing more lines than the `rows` prop will make the Textarea grow up.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
modelValue: 'Here is an autoresize Textarea, write new lines to make the Textarea grow up...'
props:
autoresize: true
---
::
### Resize
Use the `resize` prop to enable the resize control.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
resize: true
---
::
## Props
:component-props

View File

@@ -157,6 +157,69 @@ props:
---
::
### Loading
Use the `loading` prop to show a loading icon and disable the Input.
Use the `loading-icon` prop to set a different icon or change it globally in `ui.select.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
placeholder: 'Search...'
props:
loading: true
icon: 'i-heroicons-magnifying-glass-20-solid'
excludedProps:
- icon
---
::
## Slots
### `leading`
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" />
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
placeholder: 'Search...'
---
#leading
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
::
### `trailing`
Use the `#trailing` slot to set the content of the trailing icon.
::component-card
---
slots:
trailing: <UIcon name="i-heroicons-arrows-up-down-20-solid" />
baseProps:
name: 'input'
placeholder: 'Search...'
---
#trailing
:u-icon{name="i-heroicons-arrows-up-down-20-solid"}
::
## Props
:component-props

View File

@@ -10,6 +10,8 @@ headlessui:
The SelectMenu component renders by default a [Select](/forms/select) component and is based on the `ui.select` preset. You can use most of the Select props to configure the display if you don't want to override the default slot such as [color](/forms/select#style), [variant](/forms/select#style), [size](/forms/select#size), [placeholder](/forms/select#placeholder), [icon](/forms/select#icon), [disabled](/forms/select#disabled), etc.
### Options
Like the Select component, you can use the `options` prop to pass an array of strings or objects.
::component-example
@@ -30,7 +32,9 @@ const selected = ref(people[0])
```
::
You can use the `multiple` prop to select multiple values but you have to override the `#label` slot and handle the display yourself.
### Multiple
You can use the `multiple` prop to select multiple values.
::component-example
#default
@@ -39,47 +43,18 @@ You can use the `multiple` prop to select multiple values but you have to overri
#code
```vue
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const people = [...]
const selected = ref([])
</script>
<template>
<USelectMenu v-model="selected" :options="people" multiple>
<template #label>
<span v-if="selected.length" class="font-medium truncate">{{ selected.join(', ') }}</span>
<span v-else class="block truncate text-gray-400 dark:text-gray-500">Select people</span>
</template>
</USelectMenu>
<USelectMenu v-model="selected" :options="people" multiple placeholder="Select people" />
</template>
```
::
You can also override the default slot entirely.
::component-example
#default
:select-menu-example-button{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[3])
</script>
<template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton>
{{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
</UButton>
</USelectMenu>
</template>
```
::
### 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`.
@@ -134,10 +109,6 @@ const selected = ref(people[0])
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `trailing-icon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
Use the `selected-icon` prop to set a different icon or change it globally in `ui.selectMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
::component-card
@@ -147,16 +118,16 @@ baseProps:
placeholder: 'Select a person'
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
icon: 'i-heroicons-magnifying-glass-20-solid'
color: 'white'
extraColors:
- white
- gray
selectedIcon: 'i-heroicons-hand-thumb-up-solid'
excludedProps:
- icon
- selectedIcon
---
::
::alert{icon="i-heroicons-light-bulb"}
Learn how to customize icons from the [Select](/forms/select#icon) component.
::
### Search
Use the `searchable` prop to enable search.
@@ -177,6 +148,63 @@ props:
---
::
## Slots
### `label`
You can override the `#label` slot and handle the display yourself.
::component-example
#default
:select-menu-example-multiple-slot{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const people = [...]
const selected = ref([])
</script>
<template>
<USelectMenu v-model="selected" :options="people" multiple>
<template #label>
<span v-if="selected.length" class="truncate">{{ selected.join(', ') }}</span>
<span v-else>Select people</span>
</template>
</USelectMenu>
</template>
```
::
### `default`
You can also override the `#default` slot entirely.
::component-example
#default
:select-menu-example-button{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const people = [...]
const selected = ref(people[3])
</script>
<template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton color="gray">
{{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
</UButton>
</USelectMenu>
</template>
```
::
## Props
:component-props

View File

@@ -13,16 +13,16 @@ headlessui:
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon-on` and `icon-off` props by using this pattern: `i-{collection_name}-{icon_name}`.
Use any icon from [Iconify](https://icones.js.org) by setting the `on-icon` and `off-icon` props by using this pattern: `i-{collection_name}-{icon_name}` or change it globally in `ui.toggle.default.onIcon` and `ui.toggle.default.offIcon`.
::component-card
---
props:
iconOn: 'i-heroicons-check-20-solid'
iconOff: 'i-heroicons-x-mark-20-solid'
onIcon: 'i-heroicons-check-20-solid'
offIcon: 'i-heroicons-x-mark-20-solid'
excludedProps:
- iconOn
- iconOff
- onIcon
- offIcon
---
::

View File

@@ -1,5 +1,6 @@
---
github: true
github:
suffix: .ts
description: Display a label and additional informations around a form element.
---

View File

@@ -0,0 +1,491 @@
---
github: true
description: 'Display data in a table.'
---
## 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.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-basic{class="flex-1"}
#code
```vue
<script setup>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :rows="people" />
</template>
```
::
### Columns
Use the `columns` prop to configure which columns to display. It's an array of objects with the following properties:
- `label` - The label to display in the table header. Can be changed through the `column-attribute` prop.
- `key` - The field to display from the row data.
- `sortable` - Whether the column is sortable. Defaults to `false`.
- `direction` - The sort direction to use on first click. Defaults to `asc`.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-columns{class="flex-1"}
#code
```vue
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'User name'
}, {
key: 'title',
label: 'Job position'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role'
}]
const people = [...]
</script>
<template>
<UTable :columns="columns" :rows="people" />
</template>
```
::
You can easily use the [SelectMenu](/forms/select-menu) component to change the columns to display.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-columns-selectable{class="flex-1"}
#code
```vue
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}]
const selectedColumns = ref([...columns])
const people = [...]
</script>
<template>
<div>
<USelectMenu v-model="selectedColumns" :options="columns" multiple placeholder="Columns" />
<UTable :columns="selectedColumns" :rows="people" />
</div>
</template>
```
::
### Sortable
You can make the columns sortable by setting the `sortable` property to `true` in the column configuration.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-columns-sortable{class="flex-1"}
#code
```vue
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name',
sortable: true
}, {
key: 'title',
label: 'Title',
sortable: true
}, {
key: 'email',
label: 'Email',
sortable: true,
direction: 'desc'
}, {
key: 'role',
label: 'Role'
}]
const people = [...]
</script>
<template>
<UTable :columns="columns" :rows="people" :sort="{ column: 'title' }" />
</template>
```
::
You can specify the default direction of each column through the `direction` property. It can be either `asc` or `desc` and defaults to `asc`.
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`.
::alert{icon="i-heroicons-light-bulb"}
This will set the default sort and will work even if no column is set as `sortable`.
::
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"}
---
padding: false
overflowClass: 'overflow-x-auto'
baseProps:
class: 'w-full'
columns:
- key: 'id'
label: 'ID'
- key: 'name'
label: 'Name'
sortable: true
- key: 'title'
label: 'Title'
sortable: true
- key: 'email'
label: 'Email'
sortable: true
- key: 'role'
label: 'Role'
rows:
- id: 1
name: 'Lindsay Walton'
title: 'Front-end Developer'
email: 'lindsay.walton@example.com'
role: 'Member'
- id: 2
name: 'Courtney Henry'
title: 'Designer'
email: 'courtney.henry@example.com'
role: 'Admin'
- id: 3
name: 'Tom Cook'
title: 'Director of Product'
email: 'tom.cook@example.com'
role: 'Member'
- id: 4
name: 'Whitney Francis'
title: 'Copywriter'
email: 'whitney.francis@example.com'
role: 'Admin'
- id: 5
name: 'Leonard Krasner'
title: 'Senior Designer'
email: 'leonard.krasner@example.com'
role: 'Owner'
- id: 6
name: 'Floyd Miles'
title: 'Principal Designer'
email: 'floyd.miles@example.com'
role: 'Member'
props:
sortAscIcon: 'i-heroicons-arrow-up-20-solid'
sortDescIcon: 'i-heroicons-arrow-down-20-solid'
sortButton:
icon: 'i-heroicons-sparkles-20-solid'
color: 'primary'
variant: 'outline'
size: '2xs'
square: false
ui:
rounded: 'rounded-full'
excludedProps:
- sortButton
- sortAscIcon
- sortDescIcon
---
::
Use the `sort-asc-icon` prop to set a different icon or change it globally in `ui.table.default.sortAscIcon`. Defaults to `i-heroicons-bars-arrow-up-20-solid`.
Use the `sort-desc-icon` prop to set a different icon or change it globally in `ui.table.default.sortDescIcon`. Defaults to `i-heroicons-bars-arrow-down-20-solid`.
::alert{icon="i-heroicons-light-bulb"}
You can also customize the entire header cell, read more in the [Slots](#slots) section.
::
### Selectable
Use a `v-model` to make the table selectable. The `v-model` will be an array of the selected rows.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-selectable{class="flex-1"}
#code
```vue
<script setup>
const people = [...]
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" />
</template>
```
::
::alert{icon="i-heroicons-light-bulb"}
You can use the `by` prop to compare objects by a field instead of comparing object instances. We've replicated the behavior of Headless UI [Combobox](https://headlessui.com/vue/combobox#binding-objects-as-values).
::
### Searchable
You can easily use the [Input](/forms/input) component to filter the rows.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-searchable{class="flex-1"}
#code
```vue
<script setup>
const people = [...]
const q = ref('')
const filteredRows = computed(() => {
if (!q.value) {
return people
}
return people.filter((person) => {
return Object.values(person).some((value) => {
return String(value).toLowerCase().includes(q.value.toLowerCase())
})
})
})
</script>
<template>
<div>
<UInput v-model="q" placeholder="Filter people..." />
<UTable :rows="filteredRows" />
</div>
</template>
```
::
### Empty
Use the `empty-state` prop to display a message when there are no results.
You can pass an `object` through the `empty-state` prop or globally through `ui.table.default.emptyState`.
You can also set it to `null` to hide the empty state.
::component-card
---
padding: false
overflowClass: 'overflow-x-auto'
baseProps:
class: 'w-full'
columns:
- key: 'id'
label: 'ID'
- key: 'name'
label: 'Name'
- key: 'title'
label: 'Title'
- key: 'email'
label: 'Email'
- key: 'role'
label: 'Role'
props:
emptyState:
icon: 'i-heroicons-circle-stack-20-solid'
label: "No items."
excludedProps:
- emptyState
---
::
## Slots
You can use slots to customize the header and data cells of the table.
### `<column>-header`
Use the `#<column>-header` slot to customize the header cell of a column. You will have access to the `column`, `sort` and `on-sort` properties in the slot scope.
The `sort` property is an object with the following properties:
- `field` - The field to sort by.
- `direction` - The direction to sort by. Can be `asc` or `desc`.
The `on-sort` property is a function that you can call to sort the table and accepts the column as parameter.
::alert{icon="i-heroicons-light-bulb"}
Even though you can customize the sort button as mentioned in the [Sortable](#sortable) section, you can use this slot to completely override its behavior, with a custom dropdown for example.
::
### `<column>-data`
Use the `#<column>-data` slot to customize the data cell of a column. You will have access to the `row` and `column` properties in the slot scope.
You can for example create an extra column for actions with a [Dropdown](/elements/dropdown) component inside or change the color of the rows based on a selection.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-slots{class="flex-1"}
#code
```vue
<script setup>
const columns = [..., {
key: 'actions'
}]
const people = [...]
const items = (row) => [
[{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
click: () => console.log('Edit', row.id)
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid'
}], [{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'i-heroicons-arrow-right-circle-20-solid'
}], [{
label: 'Delete',
icon: 'i-heroicons-trash-20-solid'
}]
]
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" :columns="columns">
<template #name-data="{ row }">
<span :class="[selected.find(person => person.id === row.id) && 'text-primary-500 dark:text-primary-400']">{{ row.name }}</span>
</template>
<template #actions-data="{ row }">
<UDropdown :items="items(row)">
<UButton color="gray" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
</UDropdown>
</template>
</UTable>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -166,7 +166,7 @@ Use the `selected-icon` prop to set a different icon or change it globally in `u
---
padding: false
baseProps:
empty: null
emptyState: null
props:
icon: 'i-heroicons-command-line'
excludedProps:
@@ -184,7 +184,7 @@ Use the `loading-icon` prop to set a different icon or change it globally in `ui
---
padding: false
baseProps:
empty: null
emptyState: null
props:
loading: true
excludedProps:
@@ -200,7 +200,7 @@ Use the `placeholder` prop to change the input placeholder
---
padding: false
baseProps:
empty: null
emptyState: null
props:
placeholder: 'Type a command...'
excludedProps:
@@ -210,33 +210,33 @@ excludedProps:
### Close
Use the `close` prop to display a close button on the right side of the input.
Use the `close-button` prop to display a close button on the right side of the input.
You can pass all the props of the [Button](/elements/button) component to customize it through the `close` prop or globally through `ui.commandPalette.default.close`.
You can pass all the props of the [Button](/elements/button) component to customize it through the `close-button` prop or globally through `ui.commandPalette.default.closeButton`.
::component-card
---
padding: false
baseProps:
empty: null
emptyState: null
props:
close:
closeButton:
icon: 'i-heroicons-x-mark-20-solid'
color: 'gray'
variant: 'link'
padded: false
excludedProps:
- close
- closeButton
---
::
### Empty
Use the `empty` prop to display a message when there are no results.
Use the `empty-state` prop to display a message when there are no results.
You can pass an `object` through the `empty` prop or globally through `ui.commandPalette.default.empty`. Here is the default:
You can pass an `object` through the `empty-state` prop or globally through `ui.commandPalette.default.emptyState`.
You can also set it to `null` to hide the empty label.
You can also set it to `null` to hide the empty state.
::component-card
---
@@ -244,12 +244,12 @@ padding: false
baseProps:
placeholder: 'Type something to see the empty label change'
props:
empty:
emptyState:
icon: 'i-heroicons-magnifying-glass-20-solid'
label: "We couldn't find any items."
queryLabel: "We couldn't find any items with that term. Please try again."
excludedProps:
- empty
- emptyState
---
::
@@ -257,7 +257,7 @@ excludedProps:
The CommandPalette component takes care of the full-text search for you with [Fuse.js](https://fusejs.io). You can pass all the options of Fuse.js through the `fuse` prop.
When searching for a command, the component will look for a `label` property on the command by default. You can customize this behaviour by overriding the `command-attribute` prop. This will also affect the display of the command.
When searching for a command, the component will look for a `label` property on the command by default. You can customize this behavior by overriding the `command-attribute` prop. This will also affect the display of the command.
You can also highlight the matches in the command by setting the `fuse.fuseOptions.includeMatches` to `true`. The CommandPalette component automatically takes care of the highlighting for you.
@@ -320,7 +320,7 @@ const groups = computed(() => {
::
::alert{icon="i-heroicons-light-bulb"}
The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behaviour by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`.
The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behavior by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`.
::
## Themes

View File

@@ -36,7 +36,7 @@ const toast = useToast()
```
::
This component will render by default the notifications at the bottom right of the screen. You can configure its behaviour in the `app.config.ts` through `ui.notifications`:
This component will render by default the notifications at the bottom right of the screen. You can configure its behavior in the `app.config.ts` through `ui.notifications`:
```ts [app.config.ts]
export default defineAppConfig({
@@ -187,9 +187,9 @@ function onCallback () {
### Close
Use the `close` prop to hide or customize the close button on the Notification.
Use the `close-button` prop to hide or customize the close button on the Notification.
You can pass all the props of the [Button](/elements/button) component to customize it through the `close` prop or globally through `ui.notifications.default.close`.
You can pass all the props of the [Button](/elements/button) component to customize it through the `close-button` prop or globally through `ui.notification.default.closeButton`.
::component-card
---
@@ -198,7 +198,7 @@ baseProps:
title: 'Notification'
timeout: 0
props:
close:
closeButton:
icon: 'i-heroicons-archive-box-x-mark'
color: 'primary'
variant: 'outline'
@@ -207,7 +207,7 @@ props:
ui:
rounded: 'rounded-full'
excludedProps:
- close
- closeButton
---
::
@@ -230,7 +230,7 @@ const toast = useToast()
```
::
Like for `close`, you can pass all the props of the [Button](/elements/button) component inside the action or globally through `ui.notifications.default.action`.
Like for `closeButton`, you can pass all the props of the [Button](/elements/button) component inside the action or globally through `ui.notification.default.actionButton`.
::component-card
---

View File

@@ -27,10 +27,6 @@ description: Display a card for content with a header, body and footer.
:component-props
## Slots
:component-slots
## Preset
:component-preset

View File

@@ -50,7 +50,7 @@ export default <Partial<Config>> {
borderRadius: '0.375rem',
border: '1px solid var(--tw-prose-pre-border)',
whiteSpace: 'pre-wrap',
wordBreak: 'break-words'
wordBreak: 'break-word'
},
code: {
backgroundColor: 'var(--tw-prose-pre-bg)',

View File

@@ -1,6 +1,6 @@
{
"name": "@nuxthq/ui",
"version": "2.2.1",
"version": "2.3.0",
"repository": "https://github.com/nuxtlabs/ui",
"license": "MIT",
"exports": {
@@ -18,7 +18,6 @@
"node": ">=16.14.0"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "nuxt-module-build",
"prepack": "pnpm build",
"dev": "nuxi dev docs",
@@ -32,7 +31,7 @@
"@egoist/tailwindcss-icons": "^1.0.7",
"@headlessui/vue": "1.7.10",
"@iconify-json/heroicons": "^1.1.10",
"@nuxt/kit": "^3.5.1",
"@nuxt/kit": "^3.5.2",
"@nuxtjs/color-mode": "^3.2.0",
"@nuxtjs/tailwindcss": "^6.7.0",
"@popperjs/core": "^2.11.8",
@@ -48,18 +47,18 @@
"tailwindcss": "^3.3.2"
},
"devDependencies": {
"@iconify-json/simple-icons": "^1.1.54",
"@iconify-json/simple-icons": "^1.1.55",
"@nuxt/content": "^2.6.0",
"@nuxt/devtools": "^0.5.5",
"@nuxt/eslint-config": "^0.1.1",
"@nuxt/module-builder": "^0.4.0",
"@nuxthq/studio": "^0.12.1",
"@nuxthq/studio": "^0.13.2",
"@nuxtjs/plausible": "^0.2.1",
"@types/lodash-es": "^4.17.7",
"@types/node": "^20.2.4",
"@types/node": "^20.2.5",
"@vueuse/nuxt": "^10.1.2",
"eslint": "^8.41.0",
"nuxt": "^3.5.1",
"nuxt": "^3.5.2",
"nuxt-component-meta": "^0.5.3",
"nuxt-lodash": "^2.4.1",
"standard-version": "^9.5.0",

422
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -203,6 +203,12 @@ export default defineNuxtModule<ModuleOptions>({
global: options.global,
watch: false
})
addComponentsDir({
path: resolve(runtimeDir, 'components', 'data'),
prefix: options.prefix,
global: options.global,
watch: false
})
addComponentsDir({
path: resolve(runtimeDir, 'components', 'layout'),
prefix: options.prefix,

View File

@@ -1,20 +1,69 @@
// Data
const table = {
wrapper: 'relative',
base: 'min-w-full table-fixed',
divide: 'divide-y divide-gray-300 dark:divide-gray-700',
thead: '',
tbody: 'divide-y divide-gray-200 dark:divide-gray-800',
tr: {
base: '',
selected: 'bg-gray-50 dark:bg-gray-800/50'
},
th: {
base: 'text-left',
padding: 'px-3 py-3.5',
color: 'text-gray-900 dark:text-white',
font: 'font-semibold',
size: 'text-sm'
},
td: {
base: 'whitespace-nowrap',
padding: 'px-3 py-4',
color: 'text-gray-500 dark:text-gray-400',
font: '',
size: 'text-sm'
},
emptyState: {
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
label: 'text-sm text-center text-gray-900 dark:text-white',
icon: 'w-6 h-6 mx-auto text-gray-400 dark:text-gray-500 mb-4'
},
default: {
sortAscIcon: 'i-heroicons-bars-arrow-up-20-solid',
sortDescIcon: 'i-heroicons-bars-arrow-down-20-solid',
sortButton: {
icon: 'i-heroicons-arrows-up-down-20-solid',
trailing: true,
square: true,
color: 'gray',
variant: 'ghost',
class: '-m-1.5'
},
emptyState: {
icon: 'i-heroicons-circle-stack-20-solid',
label: 'No items.'
}
}
}
// Elements
const avatar = {
wrapper: 'relative inline-flex items-center justify-center',
background: 'bg-gray-100 dark:bg-gray-800',
rounded: 'rounded-full',
placeholder: 'text-xs font-medium leading-none text-gray-900 dark:text-white truncate',
placeholder: 'font-medium leading-none text-gray-900 dark:text-white truncate',
size: {
'3xs': 'h-4 w-4 text-xs',
'2xs': 'h-5 w-5 text-xs',
xs: 'h-6 w-6 text-xs',
sm: 'h-8 w-8 text-sm',
md: 'h-10 w-10 text-md',
lg: 'h-12 w-12 text-lg',
xl: 'h-14 w-14 text-xl',
'2xl': 'h-16 w-16 text-2xl',
'3xl': 'h-20 w-20 text-3xl'
'3xs': 'h-4 w-4 text-[8px]',
'2xs': 'h-5 w-5 text-[10px]',
xs: 'h-6 w-6 text-[11px]',
sm: 'h-8 w-8 text-xs',
md: 'h-10 w-10 text-sm',
lg: 'h-12 w-12 text-base',
xl: 'h-14 w-14 text-lg',
'2xl': 'h-16 w-16 text-xl',
'3xl': 'h-20 w-20 text-2xl'
},
chip: {
base: 'absolute block rounded-full ring-1 ring-white dark:ring-gray-900',
@@ -60,6 +109,7 @@ const badge = {
md: 'text-sm px-2 py-1',
lg: 'text-sm px-2.5 py-1.5'
},
color: {},
variant: {
solid: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-10 dark:ring-opacity-20'
},
@@ -99,11 +149,11 @@ const button = {
xl: 'px-4 py-3'
},
square: {
'2xs': 'p-[5px]',
'2xs': 'p-1',
xs: 'p-1.5',
sm: 'p-2',
sm: 'p-1.5',
md: 'p-2',
lg: 'p-2.5',
lg: 'p-2',
xl: 'p-3'
},
color: {
@@ -290,7 +340,8 @@ const input = {
xl: 'h-6 w-6'
},
leading: {
wrapper: 'absolute inset-y-0 left-0 flex items-center pointer-events-none',
wrapper: 'absolute inset-y-0 left-0 flex items-center',
pointer: 'pointer-events-none',
padding: {
'2xs': 'pl-2',
xs: 'pl-2.5',
@@ -301,7 +352,8 @@ const input = {
}
},
trailing: {
wrapper: 'absolute inset-y-0 right-0 flex items-center pointer-events-none',
wrapper: 'absolute inset-y-0 right-0 flex items-center',
pointer: 'pointer-events-none',
padding: {
'2xs': 'pr-2',
xs: 'pr-2.5',
@@ -345,16 +397,18 @@ const textarea = {
const select = {
...input,
placeholder: 'text-gray-900 dark:text-white',
default: {
size: 'sm',
color: 'white',
variant: 'outline',
loadingIcon: 'i-heroicons-arrow-path-20-solid',
trailingIcon: 'i-heroicons-chevron-down-20-solid'
}
}
const selectMenu = {
wrapper: 'relative inline-flex',
wrapper: 'relative',
container: 'z-20',
width: 'w-full',
height: 'max-h-60',
@@ -410,7 +464,7 @@ const selectMenu = {
const radio = {
wrapper: 'relative flex items-start',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent dark:indeterminate:bg-current dark:indeterminate:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
label: 'font-medium text-gray-700 dark:text-gray-200',
required: 'text-red-500 dark:text-red-400',
help: 'text-gray-500 dark:text-gray-400'
@@ -426,7 +480,7 @@ const toggle = {
active: 'bg-primary-500 dark:bg-primary-400',
inactive: 'bg-gray-200 dark:bg-gray-700',
container: {
base: 'pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
base: 'pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white dark:bg-gray-900 shadow transform ring-0 transition ease-in-out duration-200',
active: 'translate-x-4',
inactive: 'translate-x-0'
},
@@ -436,6 +490,10 @@ const toggle = {
inactive: 'opacity-0 ease-out duration-100',
on: 'h-3 w-3 text-primary-500 dark:text-primary-400',
off: 'h-3 w-3 text-gray-400 dark:text-gray-500'
},
default: {
onIcon: null,
offIcon: null
}
}
@@ -521,9 +579,9 @@ const commandPalette = {
size: 'h-4 w-4',
padding: 'pl-10'
},
close: 'absolute right-4'
closeButton: 'absolute right-4'
},
empty: {
emptyState: {
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
label: 'text-sm text-center text-gray-900 dark:text-white',
queryLabel: 'text-sm text-center text-gray-900 dark:text-white',
@@ -565,12 +623,12 @@ const commandPalette = {
default: {
icon: 'i-heroicons-magnifying-glass-20-solid',
loadingIcon: 'i-heroicons-arrow-path-20-solid',
empty: {
emptyState: {
icon: 'i-heroicons-magnifying-glass-20-solid',
label: 'We couldn\'t find any items.',
queryLabel: 'We couldn\'t find any items with that term. Please try again.'
},
close: null,
closeButton: null,
selectedIcon: 'i-heroicons-check-20-solid'
}
}
@@ -739,13 +797,13 @@ const notification = {
default: {
color: 'primary',
icon: null,
close: {
closeButton: {
icon: 'i-heroicons-x-mark-20-solid',
color: 'gray',
variant: 'link',
padded: false
},
action: {
actionButton: {
size: 'xs',
color: 'white'
}
@@ -761,6 +819,7 @@ const notifications = {
export default {
ui: {
table,
avatar,
avatarGroup,
badge,

View File

@@ -0,0 +1,192 @@
<template>
<div :class="ui.wrapper">
<table :class="[ui.base, ui.divide]">
<thead :class="ui.thead">
<tr :class="ui.tr.base">
<th v-if="modelValue" scope="col" class="pl-4">
<UCheckbox :checked="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" @change="selected = $event.target.checked ? rows : []" />
</th>
<th v-for="(column, index) in columns" :key="index" scope="col" :class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size]">
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
<UButton
v-if="column.sortable"
v-bind="{ ...ui.default.sortButton, ...sortButton }"
:icon="(!sort.column || sort.column !== column.key) ? sortButton.icon : sort.direction === 'asc' ? sortAscIcon : sortDescIcon"
:label="column[columnAttribute]"
@click="onSort(column)"
/>
<span v-else>{{ column[columnAttribute] }}</span>
</slot>
</th>
</tr>
</thead>
<tbody :class="ui.tbody">
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected]">
<td v-if="modelValue" class="pl-4">
<UCheckbox v-model="selected" :value="row" />
</td>
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]">
<slot :name="`${column.key}-data`" :column="column" :row="row">
{{ row[column.key] }}
</slot>
</td>
</tr>
<tr v-if="emptyState && !rows.length">
<td :colspan="columns.length">
<div :class="ui.emptyState.wrapper">
<UIcon v-if="emptyState.icon" :name="emptyState.icon" :class="ui.emptyState.icon" aria-hidden="true" />
<p :class="ui.emptyState.label">
{{ emptyState.label }}
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts">
import { ref, computed, defineComponent, toRaw } from 'vue'
import type { PropType } from 'vue'
import { capitalize, orderBy } from 'lodash-es'
import { defu } from 'defu'
import type { Button } from '../../types/button'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
function defaultComparator<T>(a: T, z: T): boolean {
return a === z
}
export default defineComponent({
props: {
modelValue: {
type: Array,
default: null
},
by: {
type: [String, Function],
default: () => defaultComparator
},
rows: {
type: Array as PropType<{ [key: string]: any }[]>,
default: () => []
},
columns: {
type: Array as PropType<{ key: string, sortable?: boolean, [key: string]: any }[]>,
default: null
},
columnAttribute: {
type: String,
default: 'label'
},
sort: {
type: Object as PropType<{ column: string, direction: 'asc' | 'desc' }>,
default: () => ({})
},
sortButton: {
type: Object as PropType<Partial<Button>>,
default: () => appConfig.ui.table.default.sortButton
},
sortAscIcon: {
type: String,
default: () => appConfig.ui.table.default.sortAscIcon
},
sortDescIcon: {
type: String,
default: () => appConfig.ui.table.default.sortDescIcon
},
emptyState: {
type: Object as PropType<{ icon: string, label: string }>,
default: () => appConfig.ui.table.default.emptyState
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.table>>,
default: () => appConfig.ui.table
}
},
emits: ['update:modelValue'],
setup (props, { emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.table>>(() => defu({}, props.ui, appConfig.ui.table))
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: capitalize(key), sortable: false })))
const sort = ref(defu({}, props.sort, { column: null, direction: 'asc' }))
const rows = computed(() => {
if (!sort.value?.column) {
return props.rows
}
const { column, direction } = sort.value
return orderBy(props.rows, column, direction)
})
const selected = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const indeterminate = computed(() => selected.value && selected.value.length > 0 && selected.value.length < props.rows.length)
const emptyState = computed(() => ({ ...ui.value.default.emptyState, ...props.emptyState }))
function compare (a: any, z: any) {
if (typeof props.by === 'string') {
const property = props.by as unknown as any
return a?.[property] === z?.[property]
}
return props.by(a, z)
}
function isSelected (row) {
if (!props.modelValue) {
return false
}
return selected.value.some((item) => compare(toRaw(item), toRaw(row)))
}
function onSort (column) {
if (sort.value.column === column.key) {
sort.value.direction = sort.value.direction === 'asc' ? 'desc' : 'asc'
} else {
sort.value = { column: column.key, direction: column.direction || 'asc' }
}
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
// eslint-disable-next-line vue/no-dupe-keys
sort,
// eslint-disable-next-line vue/no-dupe-keys
columns,
// eslint-disable-next-line vue/no-dupe-keys
rows,
selected,
indeterminate,
// eslint-disable-next-line vue/no-dupe-keys
emptyState,
isSelected,
onSort
}
}
})
</script>

View File

@@ -1,4 +1,4 @@
import { h, computed, defineComponent } from 'vue'
import { h, cloneVNode, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { classNames, getSlotsChildren } from '../../utils'
@@ -39,18 +39,20 @@ export default defineComponent({
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max)
const clones = computed(() => children.value.map((node, index) => {
const vProps: any = {}
if (!props.max || (max.value && index < max.value)) {
if (props.size) {
node.props.size = props.size
vProps.size = props.size
}
node.props.class = node.props.class || ''
node.props.class += ` ${classNames(
vProps.class = node.props.class || ''
vProps.class += ` ${classNames(
ui.value.ring,
ui.value.margin
)}`
return node
return cloneVNode(node, vProps)
}
if (max.value !== undefined && index === max.value) {

View File

@@ -29,14 +29,17 @@ export default defineComponent({
type: String,
default: () => appConfig.ui.badge.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.badge.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.badge.default.variant,
validator (value: string) {
return Object.keys(appConfig.ui.badge.variant).includes(value)
return [
...Object.keys(appConfig.ui.badge.variant),
...Object.values(appConfig.ui.badge.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
label: {
@@ -55,12 +58,14 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defu({}, props.ui, appConfig.ui.badge))
const badgeClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames(
ui.value.base,
ui.value.font,
ui.value.rounded,
ui.value.size[props.size],
ui.value.variant[props.variant]?.replaceAll('{color}', props.color)
variant?.replaceAll('{color}', props.color)
)
})

View File

@@ -5,13 +5,19 @@
:aria-label="ariaLabel"
v-bind="buttonProps"
>
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
</slot>
<slot>
<span v-if="label" :class="[truncate ? 'text-left break-all line-clamp-1' : '']">
{{ label }}
</span>
</slot>
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</slot>
</component>
</template>

View File

@@ -1,4 +1,4 @@
import { h, computed, defineComponent } from 'vue'
import { h, cloneVNode, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { getSlotsChildren } from '../../utils'
@@ -44,28 +44,30 @@ export default defineComponent({
}[ui.value.rounded]))
const clones = computed(() => children.value.map((node, index) => {
const vProps: any = {}
if (props.size) {
node.props.size = props.size
vProps.size = props.size
}
node.props.class = node.props.class || ''
node.props.class += ' !shadow-none'
node.props.ui = node.props.ui || {}
node.props.ui.rounded = ''
vProps.class = node.props.class || ''
vProps.class += ' !shadow-none'
vProps.ui = node.props.ui || {}
vProps.ui.rounded = ''
if (index === 0) {
node.props.ui.rounded = rounded.value.left
vProps.ui.rounded = rounded.value.left
}
if (index > 0) {
node.props.class += ' -ml-px'
vProps.class += ' -ml-px'
}
if (index === children.value.length - 1) {
node.props.ui.rounded = rounded.value.right
vProps.ui.rounded = rounded.value.right
}
return node
return cloneVNode(node, vProps)
}))
return () => h('div', { class: [ui.value.wrapper, ui.value.rounded, ui.value.shadow] }, clones.value)

View File

@@ -15,7 +15,7 @@
</slot>
</MenuButton>
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" @mouseover="onMouseOver">
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseover="onMouseOver">
<transition appear v-bind="ui.transition">
<MenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background]" static>
<div v-for="(subItems, index) of items" :key="index" :class="ui.padding">
@@ -127,7 +127,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.dropdown>>(() => defu({}, props.ui, appConfig.ui.dropdown))
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
const [trigger, container] = usePopper(popper.value)
@@ -149,6 +149,12 @@ export default defineComponent({
}, 200)
})
const containerStyle = computed(() => {
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8
return props.mode === 'hover' ? { paddingTop: `${offsetDistance}px`, paddingBottom: `${offsetDistance}px` } : {}
})
function onMouseOver () {
if (props.mode !== 'hover' || !menuApi.value) {
return
@@ -194,6 +200,7 @@ export default defineComponent({
ui,
trigger,
container,
containerStyle,
onMouseOver,
onMouseLeave,
omit

View File

@@ -8,6 +8,8 @@
:required="required"
:value="value"
:disabled="disabled"
:checked="checked"
:indeterminate="indeterminate"
type="checkbox"
:class="[ui.base, ui.custom]"
@focus="$emit('focus', $event)"
@@ -40,7 +42,7 @@ import appConfig from '#build/app.config'
export default defineComponent({
props: {
value: {
type: [String, Number, Boolean],
type: [String, Number, Boolean, Object],
default: null
},
modelValue: {
@@ -55,6 +57,14 @@ export default defineComponent({
type: Boolean,
default: false
},
checked: {
type: Boolean,
default: false
},
indeterminate: {
type: Boolean,
default: false
},
help: {
type: String,
default: null

View File

@@ -1,4 +1,4 @@
import { h, computed, defineComponent } from 'vue'
import { h, cloneVNode, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { getSlotsChildren } from '../../utils'
@@ -53,18 +53,20 @@ export default defineComponent({
const children = computed(() => getSlotsChildren(slots))
const clones = computed(() => children.value.map((node) => {
const vProps: any = {}
if (props.error) {
node.props.oldColor = node.props.color
node.props.color = 'red'
vProps.oldColor = node.props.color
vProps.color = 'red'
} else {
node.props.color = node.props.oldColor
vProps.color = vProps.oldColor
}
if (props.name) {
node.props.name = props.name
vProps.name = props.name
}
return node
return cloneVNode(node, vProps)
}))
return () => h('div', { class: [ui.value.wrapper] }, [

View File

@@ -18,12 +18,18 @@
@blur="$emit('blur', $event)"
>
<slot />
<div v-if="isLeading && leadingIconName" :class="leadingIconClass">
<UIcon :name="leadingIconName" :class="iconClass" />
</div>
<div v-if="isTrailing && trailingIconName" :class="trailingIconClass">
<UIcon :name="trailingIconName" :class="iconClass" />
</div>
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span>
</div>
</template>
@@ -147,7 +153,7 @@ export default defineComponent({
}
},
emits: ['update:modelValue', 'focus', 'blur'],
setup (props, { emit }) {
setup (props, { emit, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
@@ -181,8 +187,8 @@ export default defineComponent({
ui.value.size[props.size],
props.padded && ui.value.padding[props.size],
variant?.replaceAll('{color}', props.color),
isLeading.value && ui.value.leading.padding[props.size],
isTrailing.value && ui.value.trailing.padding[props.size],
(isLeading.value || slots.leading) && ui.value.leading.padding[props.size],
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[props.size],
ui.value.custom
)
})
@@ -211,7 +217,15 @@ export default defineComponent({
return props.trailingIcon || props.icon
})
const iconClass = computed(() => {
const leadingWrapperIconClass = computed(() => {
return classNames(
ui.value.icon.leading.wrapper,
ui.value.icon.leading.pointer,
ui.value.icon.leading.padding[props.size]
)
})
const leadingIconClass = computed(() => {
return classNames(
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
@@ -220,17 +234,20 @@ export default defineComponent({
)
})
const leadingIconClass = computed(() => {
const trailingWrapperIconClass = computed(() => {
return classNames(
ui.value.icon.leading.wrapper,
ui.value.icon.leading.padding[props.size]
ui.value.icon.trailing.wrapper,
ui.value.icon.trailing.pointer,
ui.value.icon.trailing.padding[props.size]
)
})
const trailingIconClass = computed(() => {
return classNames(
ui.value.icon.trailing.wrapper,
ui.value.icon.trailing.padding[props.size]
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size],
props.loading && !isLeading.value && 'animate-spin'
)
})
@@ -241,11 +258,12 @@ export default defineComponent({
isLeading,
isTrailing,
inputClass,
iconClass,
leadingIconName,
leadingIconClass,
leadingWrapperIconClass,
trailingIconName,
trailingIconClass,
trailingWrapperIconClass,
onInput
}
}

View File

@@ -5,7 +5,7 @@
:name="name"
:value="modelValue"
:required="required"
:disabled="disabled"
:disabled="disabled || loading"
:class="selectClass"
@input="onInput"
>
@@ -36,12 +36,16 @@
</template>
</select>
<div v-if="icon" :class="leadingIconClass">
<UIcon :name="icon" :class="iconClass" />
</div>
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span v-if="trailingIcon" :class="trailingIconClass">
<UIcon :name="trailingIcon" :class="iconClass" aria-hidden="true" />
<span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</slot>
</span>
</div>
</template>
@@ -89,18 +93,38 @@ export default defineComponent({
type: String,
default: null
},
loadingIcon: {
type: String,
default: () => appConfig.ui.input.default.loadingIcon
},
leadingIcon: {
type: String,
default: null
},
trailingIcon: {
type: String,
default: () => appConfig.ui.select.default.trailingIcon
},
options: {
type: Array,
default: () => []
trailing: {
type: Boolean,
default: false
},
leading: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
padded: {
type: Boolean,
default: true
},
options: {
type: Array,
default: () => []
},
size: {
type: String,
default: () => appConfig.ui.select.default.size,
@@ -139,7 +163,7 @@ export default defineComponent({
}
},
emits: ['update:modelValue', 'focus', 'blur'],
setup (props, { emit }) {
setup (props, { emit, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
@@ -207,35 +231,70 @@ export default defineComponent({
return classNames(
ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size],
props.padded && ui.value.padding[props.size],
variant?.replaceAll('{color}', props.color),
!!props.icon && ui.value.leading.padding[props.size],
ui.value.trailing.padding[props.size],
(isLeading.value || slots.leading) && ui.value.leading.padding[props.size],
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[props.size],
ui.value.custom
)
})
const iconClass = computed(() => {
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 classNames(
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size]
ui.value.icon.leading.wrapper,
ui.value.icon.leading.pointer,
ui.value.icon.leading.padding[props.size]
)
})
const leadingIconClass = computed(() => {
return classNames(
ui.value.icon.leading.wrapper,
ui.value.icon.leading.padding[props.size]
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size],
props.loading && 'animate-spin'
)
})
const trailingWrapperIconClass = computed(() => {
return classNames(
ui.value.icon.trailing.wrapper,
ui.value.icon.trailing.pointer,
ui.value.icon.trailing.padding[props.size]
)
})
const trailingIconClass = computed(() => {
return classNames(
ui.value.icon.trailing.wrapper,
ui.value.icon.trailing.padding[props.size]
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size],
props.loading && !isLeading.value && 'animate-spin'
)
})
@@ -244,10 +303,15 @@ export default defineComponent({
ui,
normalizedOptionsWithPlaceholder,
normalizedValue,
isLeading,
isTrailing,
selectClass,
iconClass,
leadingIconName,
leadingIconClass,
leadingWrapperIconClass,
trailingIconName,
trailingIconClass,
trailingWrapperIconClass,
onInput
}
}

View File

@@ -6,7 +6,7 @@
:name="name"
:model-value="modelValue"
:multiple="multiple"
:disabled="disabled"
:disabled="disabled || loading"
as="div"
:class="ui.wrapper"
@update:model-value="onUpdate"
@@ -27,19 +27,24 @@
role="button"
class="inline-flex w-full"
>
<slot :open="open" :disabled="disabled">
<button :class="selectMenuClass" :disabled="disabled" type="button">
<span v-if="icon" :class="leadingIconClass">
<UIcon :name="icon" :class="iconClass" />
<slot :open="open" :disabled="disabled" :loading="loading">
<button :class="selectMenuClass" :disabled="disabled || loading" type="button">
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<slot name="label">
<span v-if="modelValue" class="block truncate">{{ typeof modelValue === 'string' ? modelValue : modelValue[optionAttribute] }}</span>
<span v-else class="block truncate text-gray-400 dark:text-gray-500">{{ placeholder || '&nbsp;' }}</span>
<span v-if="multiple && Array.isArray(modelValue) && modelValue.length" class="block truncate">{{ modelValue.length }} selected</span>
<span v-else-if="!multiple && modelValue" class="block truncate">{{ typeof modelValue === 'string' ? modelValue : modelValue[optionAttribute] }}</span>
<span v-else class="block truncate" :class="ui.placeholder">{{ placeholder || '&nbsp;' }}</span>
</slot>
<span v-if="trailingIcon" :class="trailingIconClass">
<UIcon :name="trailingIcon" :class="iconClass" aria-hidden="true" />
<span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</slot>
</span>
</button>
</slot>
@@ -166,10 +171,30 @@ export default defineComponent({
type: String,
default: null
},
loadingIcon: {
type: String,
default: () => appConfig.ui.input.default.loadingIcon
},
leadingIcon: {
type: String,
default: null
},
trailingIcon: {
type: String,
default: () => appConfig.ui.select.default.trailingIcon
},
trailing: {
type: Boolean,
default: false
},
leading: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
selectedIcon: {
type: String,
default: () => appConfig.ui.selectMenu.default.selectedIcon
@@ -248,7 +273,7 @@ export default defineComponent({
}
},
emits: ['update:modelValue', 'open', 'close'],
setup (props, { emit }) {
setup (props, { emit, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
@@ -268,38 +293,73 @@ export default defineComponent({
return classNames(
uiSelect.value.base,
uiSelect.value.rounded,
uiSelect.value.placeholder,
'text-left cursor-default',
uiSelect.value.size[props.size],
uiSelect.value.gap[props.size],
props.padded && uiSelect.value.padding[props.size],
variant?.replaceAll('{color}', props.color),
!!props.icon && uiSelect.value.leading.padding[props.size],
uiSelect.value.trailing.padding[props.size],
(isLeading.value || slots.leading) && uiSelect.value.leading.padding[props.size],
(isTrailing.value || slots.trailing) && uiSelect.value.trailing.padding[props.size],
uiSelect.value.custom,
'inline-flex items-center'
)
})
const iconClass = computed(() => {
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 classNames(
uiSelect.value.icon.base,
appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
uiSelect.value.icon.size[props.size]
uiSelect.value.icon.leading.wrapper,
uiSelect.value.icon.leading.pointer,
uiSelect.value.icon.leading.padding[props.size]
)
})
const leadingIconClass = computed(() => {
return classNames(
uiSelect.value.icon.leading.wrapper,
uiSelect.value.icon.leading.padding[props.size]
uiSelect.value.icon.base,
appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
uiSelect.value.icon.size[props.size],
props.loading && 'animate-spin'
)
})
const trailingWrapperIconClass = computed(() => {
return classNames(
uiSelect.value.icon.trailing.wrapper,
uiSelect.value.icon.trailing.pointer,
uiSelect.value.icon.trailing.padding[props.size]
)
})
const trailingIconClass = computed(() => {
return classNames(
uiSelect.value.icon.trailing.wrapper,
uiSelect.value.icon.trailing.padding[props.size]
uiSelect.value.icon.base,
appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
uiSelect.value.icon.size[props.size],
props.loading && !isLeading.value && 'animate-spin'
)
})
@@ -339,10 +399,15 @@ export default defineComponent({
ui,
trigger,
container,
isLeading,
isTrailing,
selectMenuClass,
iconClass,
leadingIconName,
leadingIconClass,
leadingWrapperIconClass,
trailingIconName,
trailingIconClass,
trailingWrapperIconClass,
filteredOptions,
queryOption,
query,

View File

@@ -1,14 +1,15 @@
<template>
<Switch
v-model="active"
:name="name"
:class="[active ? ui.active : ui.inactive, ui.base]"
>
<span :class="[active ? ui.container.active : ui.container.inactive, ui.container.base]">
<span v-if="iconOn" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
<UIcon :name="iconOn" :class="ui.icon.on" />
<span v-if="onIcon" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
<UIcon :name="onIcon" :class="ui.icon.on" />
</span>
<span v-if="iconOff" :class="[active ? ui.icon.inactive : ui.icon.active, ui.icon.base]" aria-hidden="true">
<UIcon :name="iconOff" :class="ui.icon.off" />
<span v-if="offIcon" :class="[active ? ui.icon.inactive : ui.icon.active, ui.icon.base]" aria-hidden="true">
<UIcon :name="offIcon" :class="ui.icon.off" />
</span>
</span>
</Switch>
@@ -34,17 +35,21 @@ export default defineComponent({
UIcon
},
props: {
name: {
type: String,
default: null
},
modelValue: {
type: Boolean,
default: false
},
iconOn: {
onIcon: {
type: String,
default: null
default: () => appConfig.ui.toggle.default.onIcon
},
iconOff: {
offIcon: {
type: String,
default: null
default: () => appConfig.ui.toggle.default.offIcon
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.toggle>>,

View File

@@ -19,9 +19,9 @@
/>
<UButton
v-if="close"
v-bind="close"
:class="ui.input.close"
v-if="closeButton"
v-bind="{ ...ui.default.closeButton, ...closeButton }"
:class="ui.input.closeButton"
aria-label="Close"
@click="onClear"
/>
@@ -51,10 +51,10 @@
</CommandPaletteGroup>
</ComboboxOptions>
<div v-else-if="empty" :class="ui.empty.wrapper">
<UIcon v-if="empty.icon" :name="empty.icon" :class="ui.empty.icon" aria-hidden="true" />
<p :class="query ? ui.empty.queryLabel : ui.empty.label">
{{ query ? empty.queryLabel : empty.label }}
<div v-else-if="emptyState" :class="ui.emptyState.wrapper">
<UIcon v-if="emptyState.icon" :name="emptyState.icon" :class="ui.emptyState.icon" aria-hidden="true" />
<p :class="query ? ui.emptyState.queryLabel : ui.emptyState.label">
{{ query ? emptyState.queryLabel : emptyState.label }}
</p>
</div>
</div>
@@ -133,13 +133,13 @@ export default defineComponent({
type: String,
default: () => appConfig.ui.commandPalette.default.selectedIcon
},
close: {
closeButton: {
type: Object as PropType<Partial<Button>>,
default: () => appConfig.ui.commandPalette.default.close
default: () => appConfig.ui.commandPalette.default.closeButton
},
empty: {
emptyState: {
type: Object as PropType<{ icon: string, label: string, queryLabel: string }>,
default: () => appConfig.ui.commandPalette.default.empty
default: () => appConfig.ui.commandPalette.default.emptyState
},
placeholder: {
type: String,
@@ -280,6 +280,8 @@ export default defineComponent({
)
})
const emptyState = computed(() => ({ ...ui.value.default.emptyState, ...props.emptyState }))
// Methods
function activateFirstOption () {
@@ -327,6 +329,8 @@ export default defineComponent({
query,
iconName,
iconClass,
// eslint-disable-next-line vue/no-dupe-keys
emptyState,
onSelect,
onClear
}

View File

@@ -16,15 +16,15 @@
</p>
<div v-if="description && actions.length" class="mt-3 flex items-center gap-2">
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.action, ...action }" @click.stop="onAction(action)" />
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
</div>
</div>
<div class="flex-shrink-0 flex items-center gap-3">
<div v-if="!description && actions.length" class="flex items-center gap-2">
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.action, ...action }" @click.stop="onAction(action)" />
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
</div>
<UButton v-if="close" v-bind="{ ...ui.default.close, ...close }" @click.stop="onClose" />
<UButton v-if="closeButton" v-bind="{ ...ui.default.closeButton, ...closeButton }" @click.stop="onClose" />
</div>
</div>
</div>
@@ -80,9 +80,9 @@ export default defineComponent({
type: Object as PropType<Partial<Avatar>>,
default: null
},
close: {
closeButton: {
type: Object as PropType<Partial<Button>>,
default: () => appConfig.ui.notification.default.close
default: () => appConfig.ui.notification.default.closeButton
},
timeout: {
type: Number,

View File

@@ -33,6 +33,9 @@ export const defineShortcuts = (config: ShortcutsConfig) => {
let shortcuts: Shortcut[] = []
const onKeyDown = (e: KeyboardEvent) => {
// Input autocomplete triggers a keydown event
if (!e.key) { return }
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key)
for (const shortcut of shortcuts) {

View File

@@ -12,7 +12,7 @@ export interface Notification {
description: string
icon?: string
avatar?: Partial<Avatar>
close?: Partial<Button>
closeButton?: Partial<Button>
timeout: number
actions?: NotificationAction[]
click?: Function