Compare commits

...

56 Commits

Author SHA1 Message Date
Benjamin Canac
b654c93e93 chore(release): v2.20.0 2024-12-09 12:30:42 +01:00
renovate[bot]
b7e04db645 chore(deps): lock file maintenance (dev) (#2862)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 10:18:14 +01:00
renovate[bot]
e6034a2765 chore(deps): update pnpm to v9.15.0 (dev) (#2847)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 10:17:34 +01:00
renovate[bot]
a8c38224c6 chore(deps): update devdependency @nuxt/test-utils to ^3.15.1 (dev) (#2838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 00:55:49 +01:00
renovate[bot]
a9ef6406ea chore(deps): update all non-major dependencies (dev) (#2819)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 12:59:41 +01:00
renovate[bot]
96e846ddee chore(deps): update dependency tailwindcss to ^3.4.16 (dev) (#2830)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-04 13:41:45 +01:00
Benjamin Canac
16dbc1b536 docs(app): remove banner 2024-12-03 10:54:25 +01:00
Benjamin Canac
c6b2ae45e5 docs(Header): hide color mode button on mobile 2024-12-03 10:54:19 +01:00
renovate[bot]
547c657ee7 chore(deps): lock file maintenance (dev) (#2817)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 11:18:30 +01:00
renovate[bot]
b16b434041 chore(deps): update all non-major dependencies (dev) (#2756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-12-02 10:56:20 +01:00
Benjamin Canac
fb12323304 docs(Header): move dropdown out of link 2024-11-30 11:48:03 +01:00
Benjamin Canac
0a404615ff docs(Header): replace badge by dropdown 2024-11-30 11:33:09 +01:00
renovate[bot]
cbf0f22efd chore(deps): update vueuse monorepo to v12 (dev) (major) (#2783)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 12:11:47 +01:00
Sandro Circi
4cde571e38 fix(Link): exactQuery prop type (#2781) 2024-11-27 09:47:39 +01:00
Benjamin Canac
023497d144 chore(README): update 2024-11-26 15:18:27 +01:00
Benjamin Canac
56d4ca3b74 docs(Header): update GitHub link 2024-11-26 15:09:10 +01:00
Harsh Patel
11b8c3d9db feat(Notification): add pauseTimeoutOnHover prop (#2661) 2024-11-25 22:09:40 +01:00
Hans Knöchel
419a24f703 feat(Accordion): add close event (#2750) 2024-11-25 14:58:30 +01:00
renovate[bot]
854bb81295 chore(deps): lock file maintenance (dev) (#2751)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-25 12:54:17 +01:00
Benjamin Canac
bf8e3954a4 docs(Banner): update for black friday 2024-11-25 12:26:31 +01:00
renovate[bot]
637ec4d27b chore(deps): update all non-major dependencies (dev) (#2704)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 11:27:00 +01:00
kyyy
f3632ddee5 fix(Form)!: resolve async validation in yup & issue directly mutate state (#2701) 2024-11-23 19:29:54 +01:00
Jevin
dbd2aed20b docs(table): columns select is obscured (#2714) 2024-11-21 11:19:37 +01:00
Giorgio Boa
51c8b8e3e5 fix(components): replace as const with correct type in config (#2652)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-20 10:54:37 +01:00
renovate[bot]
588a908358 chore(deps): update all non-major dependencies (dev) (#2693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 10:21:06 +01:00
renovate[bot]
d692a81b1e chore(deps): update nuxt framework to ^3.14.1592 (dev) (#2699)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 09:52:12 +01:00
Daniel Roe
ec98d415b4 docs: remove local module from list (#2690) 2024-11-19 18:24:40 +01:00
renovate[bot]
c80d2e6c12 chore(deps): lock file maintenance (dev) (#2671)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-19 11:40:14 +01:00
renovate[bot]
ce61a2b6db chore(deps): update all non-major dependencies (dev) (#2641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-19 10:50:58 +01:00
Benjamin Canac
eee5bb9939 chore(deps): set chokidar resolution 2024-11-18 09:35:35 +01:00
Benjamin Canac
d3804157ec docs(input): correct loading behavior
Resolves nuxt/ui#2669
2024-11-18 09:35:23 +01:00
Sandro Circi
03e24f4583 feat(Link): allow partial query match for activeClass (#2663) 2024-11-17 12:15:22 +01:00
jcahal
d0e626c551 docs(table): correct spelling of contextmenu right-clickable (#2653) 2024-11-15 17:32:37 +01:00
renovate[bot]
670d8bfbac chore(deps): update dependency tailwindcss to ^3.4.15 (dev) (#2648)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 09:47:28 +01:00
renovate[bot]
64b703df8d chore(deps): update dependency @nuxt/icon to ^1.7.5 (dev) (#2638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 16:45:31 +01:00
Julien Blatecky
976b03f241 fix(types): improve DeepPartial type for App Config (#2621) 2024-11-14 10:33:26 +01:00
renovate[bot]
35e3b8c720 chore(deps): update all non-major dependencies (dev) (#2628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 10:16:36 +01:00
Benjamin Canac
07ef771b17 fix(Carousel): wrong ui type with strategy 2024-11-13 21:02:00 +01:00
Maxime Pauvert
5c75b5c490 docs(Banner): wrong aria label (#2632) 2024-11-13 17:53:50 +01:00
kyyy
53df9d9a8c feat(InputMenu/SelectMenu): add support for dot notation in by prop (#2607) 2024-11-13 12:25:31 +01:00
Malik-Jouda
0d1a76e3c6 feat(Badge): handle icon prop (#2594)
Co-authored-by: malik jouda <m.jouda@approved.tech>
2024-11-12 16:16:20 +01:00
renovate[bot]
b2ed4662af chore(deps): update devdependency @release-it/conventional-changelog to ^9.0.3 (dev) (#2604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 16:07:30 +01:00
Benjamin Canac
423c48879d chore(github): update issue templates 2024-11-12 13:11:09 +01:00
kyyy
acecff40ec fix(Form): use parsed value from joi instead of original state (#2587) 2024-11-11 19:29:46 +01:00
renovate[bot]
1fd5fac295 chore(deps): lock file maintenance (dev) (#2595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 18:45:14 +01:00
kyyy
b23f2decfc fix(Table): data outdated when rows change (#2600) 2024-11-11 18:44:49 +01:00
kyyy
7154254ac2 fix(InputMenu/SelectMenu): use by prop to compare objects & support dot notation in value-attribute (#2566) 2024-11-10 19:44:20 +01:00
Benjamin Canac
49f85d55c5 chore(deps): set nuxt resolution to 3.13.2
Causes some `EMFILE: too many open files` errors
2024-11-10 18:19:48 +01:00
kyyy
97037864b3 fix(Table): prevent onClick while blocking element (#2592) 2024-11-10 16:59:34 +01:00
renovate[bot]
0abccabc26 chore(deps): update dependency @nuxt/icon to ^1.7.2 (dev) (#2591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 14:45:02 +01:00
kyyy
ac323c4ccc feat(Table): add custom @select:all event (#2581) 2024-11-09 18:48:52 +01:00
kyyy
d4e408cfd8 fix(Notification): element renders even when no notification is present (#2561) 2024-11-09 11:24:13 +01:00
renovate[bot]
f3bf69c233 chore(deps): update dependency @nuxt/icon to ^1.7.0 (dev) (#2575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 18:03:26 +01:00
kyyy
d6daf466ac feat(Table): allow dynamically render checkbox (#2549)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-08 17:24:41 +01:00
kyyy
6e66990372 fix(Table): missing type on props loadingState (#2551) 2024-11-08 09:46:00 +01:00
Benjamin Canac
56e28d80db docs: update figma links 2024-11-07 18:21:51 +01:00
52 changed files with 2782 additions and 2212 deletions

View File

@@ -29,11 +29,20 @@ body:
- Build Modules: `-` - Build Modules: `-`
validations: validations:
required: true required: true
- type: dropdown
id: package
attributes:
label: Is this bug related to Nuxt or Vue?
options:
- Nuxt
- Vue
validations:
required: true
- type: input - type: input
id: version id: version
attributes: attributes:
label: Version label: Version
placeholder: v3.0.0-alpha.5 placeholder: v3.0.0-alpha.x
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

@@ -12,7 +12,7 @@ body:
label: For what version of Nuxt UI are you suggesting this? label: For what version of Nuxt UI are you suggesting this?
options: options:
- v2.x - v2.x
- v3-alpha - v3.0.0-alpha.x
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

@@ -12,7 +12,7 @@ body:
label: For what version of Nuxt UI are you asking this question? label: For what version of Nuxt UI are you asking this question?
options: options:
- v2.x - v2.x
- v3-alpha - v3.0.0-alpha.x
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

@@ -1,5 +1,38 @@
# Changelog # Changelog
## [2.20.0](https://github.com/nuxt/ui/compare/v2.19.2...v2.20.0) (2024-12-09)
### ⚠ BREAKING CHANGES
* **Form:** resolve async validation in yup & issue directly mutate state (#2701)
### Features
* **Accordion:** add `close` event ([#2750](https://github.com/nuxt/ui/issues/2750)) ([419a24f](https://github.com/nuxt/ui/commit/419a24f7034cefda2c6669f3c26742552e500f63))
* **Badge:** handle `icon` prop ([#2594](https://github.com/nuxt/ui/issues/2594)) ([0d1a76e](https://github.com/nuxt/ui/commit/0d1a76e3c69e08534abb295b96548e67cfbea00c))
* **InputMenu/SelectMenu:** add support for `dot notation` in `by` prop ([#2607](https://github.com/nuxt/ui/issues/2607)) ([53df9d9](https://github.com/nuxt/ui/commit/53df9d9a8cd6850803bdafc7ef6efe4e7404d334))
* **Link:** allow partial query match for `activeClass` ([#2663](https://github.com/nuxt/ui/issues/2663)) ([03e24f4](https://github.com/nuxt/ui/commit/03e24f45836bdddd94b30cbaecc2288a78b56b0b))
* **Notification:** add `pauseTimeoutOnHover` prop ([#2661](https://github.com/nuxt/ui/issues/2661)) ([11b8c3d](https://github.com/nuxt/ui/commit/11b8c3d9db1ec62b1c3557703c7ab5c99cb42df5))
* **Table:** add contextmenu handling to table rows ([#2283](https://github.com/nuxt/ui/issues/2283)) ([c9e6256](https://github.com/nuxt/ui/commit/c9e6256e7f2c06da8bfda13700f56f6994e76eab))
* **Table:** add custom `[@select](https://github.com/select):all` event ([#2581](https://github.com/nuxt/ui/issues/2581)) ([ac323c4](https://github.com/nuxt/ui/commit/ac323c4cccd930f2cd8c1f54b325bd509acd40bf))
* **Table:** allow dynamically render `checkbox` ([#2549](https://github.com/nuxt/ui/issues/2549)) ([d6daf46](https://github.com/nuxt/ui/commit/d6daf466ace42b828151c45b18cd47179e85d66d))
### Bug Fixes
* **AvatarGroup/ButtonGroup/MeterGroup:** allow deeply partial `ui` config ([#2542](https://github.com/nuxt/ui/issues/2542)) ([bf58086](https://github.com/nuxt/ui/commit/bf580863af11d6a1a4c6c6774b44ec37b082e933))
* **Carousel:** wrong `ui` type with `strategy` ([07ef771](https://github.com/nuxt/ui/commit/07ef771b17c72e275508a273371454a5e8a62257))
* **components:** replace `as const` with correct type in config ([#2652](https://github.com/nuxt/ui/issues/2652)) ([51c8b8e](https://github.com/nuxt/ui/commit/51c8b8e3e59d7eceff72625650a199fcf7c6feca))
* **date-picker:** undefined `dayIndex` ([#2545](https://github.com/nuxt/ui/issues/2545)) ([ce955d2](https://github.com/nuxt/ui/commit/ce955d24f1dfd222e87ce88428c0612c3f13cd50))
* **Form:** resolve async validation in yup & issue directly mutate state ([#2701](https://github.com/nuxt/ui/issues/2701)) ([f3632dd](https://github.com/nuxt/ui/commit/f3632ddee511f0fccb24d4fc37403421e84ffdae))
* **Form:** use parsed value from `joi` instead of original state ([#2587](https://github.com/nuxt/ui/issues/2587)) ([acecff4](https://github.com/nuxt/ui/commit/acecff40ec0156e45b4934c5d10c4dfa7c135f8e))
* **InputMenu/SelectMenu:** use `by` prop to compare objects & support dot notation in `value-attribute` ([#2566](https://github.com/nuxt/ui/issues/2566)) ([7154254](https://github.com/nuxt/ui/commit/7154254ac22830f651ec200f7f3af2f5577f2de0))
* **Link:** `exactQuery` prop type ([#2781](https://github.com/nuxt/ui/issues/2781)) ([4cde571](https://github.com/nuxt/ui/commit/4cde571e387775a9b12759f6f8c99117c84cbcff))
* **Notification:** element renders even when no `notification` is present ([#2561](https://github.com/nuxt/ui/issues/2561)) ([d4e408c](https://github.com/nuxt/ui/commit/d4e408cfd8e2ef26021519f2f30f57e9120e1939))
* **Table:** data outdated when rows change ([#2600](https://github.com/nuxt/ui/issues/2600)) ([b23f2de](https://github.com/nuxt/ui/commit/b23f2decfc9607555a315d0d087d0a042f03a938))
* **Table:** missing type on props `loadingState` ([#2551](https://github.com/nuxt/ui/issues/2551)) ([6e66990](https://github.com/nuxt/ui/commit/6e66990372ef6bd7c109a64c753d9b50e96a450b))
* **Table:** prevent `onClick` while blocking element ([#2592](https://github.com/nuxt/ui/issues/2592)) ([9703786](https://github.com/nuxt/ui/commit/97037864b39749db228fa5f51981f19e4a9c29dd))
* **types:** improve `DeepPartial` type for App Config ([#2621](https://github.com/nuxt/ui/issues/2621)) ([976b03f](https://github.com/nuxt/ui/commit/976b03f241ef9626a6338685e43c844a8b3953fd))
## [2.19.2](https://github.com/nuxt/ui/compare/v2.19.1...v2.19.2) (2024-11-05) ## [2.19.2](https://github.com/nuxt/ui/compare/v2.19.1...v2.19.2) (2024-11-05)
### Bug Fixes ### Bug Fixes

View File

@@ -1,4 +1,4 @@
[![nuxt-ui.png](https://repository-images.githubusercontent.com/428329515/43fec891-9030-4601-8233-5d45ba5c6013)](https://ui.nuxt.com) [![nuxt-ui.png](https://volta.s3.fr-par.scw.cloud/nuxt_ui_social_card_531d133fa2.png)](https://ui.nuxt.com)
# Nuxt UI # Nuxt UI
@@ -20,7 +20,7 @@ Its goal is to provide everything related to UI when building a Nuxt app. This i
- Keyboard shortcuts - Keyboard shortcuts
- Bundled icons - Bundled icons
- Fully typed - Fully typed
- [Figma Kit](https://www.figma.com/community/file/1288455405058138934) - [Figma Kit](https://www.figma.com/community/file/1436401057300493073)
Read more on [ui.nuxt.com](https://ui.nuxt.com) Read more on [ui.nuxt.com](https://ui.nuxt.com)

View File

@@ -3,7 +3,7 @@
<div> <div>
<NuxtLoadingIndicator /> <NuxtLoadingIndicator />
<Banner v-if="!$route.path.startsWith('/examples')" /> <!-- <Banner v-if="!$route.path.startsWith('/examples')" /> -->
<Header v-if="!$route.path.startsWith('/examples')" :links="links" /> <Header v-if="!$route.path.startsWith('/examples')" :links="links" />

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const id = 'nuxt-ui-banner-2' const id = 'nuxt-ui-banner-3'
const to = 'https://ui3.nuxt.dev' const to = '/pro/pricing'
const hideBanner = () => { const hideBanner = () => {
localStorage.setItem(id, 'true') localStorage.setItem(id, 'true')
@@ -28,9 +28,8 @@ if (import.meta.server) {
<NuxtLink <NuxtLink
v-if="to" v-if="to"
:to="to" :to="to"
target="_blank"
class="focus:outline-none" class="focus:outline-none"
aria-label="Nuxt UI Pro pricing" aria-label="20% off on all Nuxt UI Pro products for Black Friday week"
tabindex="-1" tabindex="-1"
> >
<span class="absolute inset-0 " aria-hidden="true" /> <span class="absolute inset-0 " aria-hidden="true" />
@@ -40,19 +39,19 @@ if (import.meta.server) {
<div class="lg:flex-1 hidden lg:flex items-center" /> <div class="lg:flex-1 hidden lg:flex items-center" />
<p class="text-sm font-medium text-white dark:text-gray-900 truncate"> <p class="text-sm font-medium text-white dark:text-gray-900 truncate">
<UIcon name="i-heroicons-rocket-launch" class="w-5 h-5 align-top flex-shrink-0 pointer-events-none mr-2" /> <UIcon name="i-ri-discount-percent-fill" class="size-5 align-top flex-shrink-0 pointer-events-none mr-2" />
<span class="font-semibold">Nuxt UI v3-alpha</span> has been released! <span class="font-bold">Black Friday Week</span>: <UBadge label="20% off" color="white" class="ring-0 font-semibold" /> on all Nuxt UI Pro products from <span class="font-semibold">Nov 25</span> to <span class="font-semibold">Dec 2</span>!
</p> </p>
<UButton <!-- <UButton
to="https://ui3.nuxt.dev" :to="to"
target="_blank" target="_blank"
label="Try it out" label="Buy now"
color="black" color="black"
variant="solid" variant="solid"
size="2xs" size="2xs"
trailing-icon="i-heroicons-arrow-right-20-solid" trailing-icon="i-heroicons-arrow-right-20-solid"
/> /> -->
<div class="flex items-center justify-end lg:flex-1"> <div class="flex items-center justify-end lg:flex-1">
<button <button

View File

@@ -30,7 +30,7 @@ const { $ui } = useNuxtApp()
const links = [{ const links = [{
icon: 'i-simple-icons-figma', icon: 'i-simple-icons-figma',
label: 'Figma Kit', label: 'Figma Kit',
to: 'https://www.figma.com/community/file/1288455405058138934', to: 'https://www.figma.com/community/file/1436401057300493073',
target: '_blank' target: '_blank'
}, { }, {
label: 'Playground', label: 'Playground',

View File

@@ -10,12 +10,34 @@
}" }"
> >
<template #left> <template #left>
<NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-gray-900 dark:text-white min-w-0" aria-label="Nuxt UI"> <NuxtLink to="/" class="flex items-end gap-2 text-xl text-gray-900 dark:text-white min-w-0 shrink-0" aria-label="Nuxt UI">
<LogoPro v-if="$route.path.startsWith('/pro')" class="w-auto h-6 shrink-0" /> <LogoPro v-if="$route.path.startsWith('/pro')" class="w-auto h-6 shrink-0" />
<Logo v-else class="w-auto h-6 shrink-0" /> <Logo v-else class="w-auto h-6 shrink-0" />
<UBadge :label="$route.path.startsWith('/pro') ? `v${pkg.version.split('-')[0]}` : `v${config.version}`" variant="subtle" size="xs" class="-mb-[2px] rounded font-semibold truncate hidden sm:inline-flex" />
</NuxtLink> </NuxtLink>
<UDropdown
:items="[[{ label: $route.path.startsWith('/pro') ? `v${pkg.version.split('-')[0]}` : `v${config.version}`, class: 'text-primary-500 dark:text-primary-400' }, { label: 'v3.0.0-alpha.x', to: 'https://ui3.nuxt.dev' }]]"
:popper="{ strategy: 'absolute', offsetDistance: 11, placement: 'bottom-start' }"
:ui="{
background: 'dark:bg-gray-900',
ring: 'dark:ring-gray-800',
width: 'w-auto',
item: {
padding: 'p-1',
size: 'text-xs',
active: 'dark:bg-gray-800/50'
}
}"
>
<UButton
:label="$route.path.startsWith('/pro') ? `v${pkg.version.split('-')[0]}` : `v${config.version}`"
trailing-icon="i-lucide-chevron-down"
variant="outline"
size="2xs"
truncate
class="-mb-[6px] font-semibold rounded-full truncate ring-primary-500/25 dark:ring-primary-400/25 bg-primary-500/10 dark:bg-primary-400/10 hover:bg-primary-500/15 dark:hover:bg-primary-400/15 transition-colors"
/>
</UDropdown>
</template> </template>
<template #right> <template #right>
@@ -25,10 +47,10 @@
<UContentSearchButton :label="null" /> <UContentSearchButton :label="null" />
</UTooltip> </UTooltip>
<UColorModeButton /> <UColorModeButton class="hidden lg:inline-flex" />
<UButton <UButton
to="https://github.com/nuxt/ui" to="https://github.com/nuxt/ui/tree/dev"
target="_blank" target="_blank"
icon="i-simple-icons-github" icon="i-simple-icons-github"
aria-label="GitHub" aria-label="GitHub"

View File

@@ -45,7 +45,7 @@ const ui = {
inactive: 'text-gray-400 dark:text-gray-500' inactive: 'text-gray-400 dark:text-gray-500'
}, },
avatar: { avatar: {
size: '2xs' as const size: '2xs'
} }
} }
} }

View File

@@ -1,6 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
// Columns // Columns
const columns = [{ const columns = [{
key: 'select',
class: 'w-2'
}, {
key: 'id', key: 'id',
label: '#', label: '#',
sortable: true sortable: true
@@ -20,6 +23,7 @@ const columns = [{
const selectedColumns = ref(columns) const selectedColumns = ref(columns)
const columnsTable = computed(() => columns.filter(column => selectedColumns.value.includes(column))) const columnsTable = computed(() => columns.filter(column => selectedColumns.value.includes(column)))
const excludeSelectColumn = computed(() => columns.filter(v => v.key !== 'select'))
// Selected Rows // Selected Rows
const selectedRows = ref([]) const selectedRows = ref([])
@@ -153,7 +157,7 @@ const { data: todos, status } = await useLazyAsyncData<{
</UButton> </UButton>
</UDropdown> </UDropdown>
<USelectMenu v-model="selectedColumns" :options="columns" multiple> <USelectMenu v-model="selectedColumns" :options="excludeSelectColumn" multiple>
<UButton <UButton
icon="i-heroicons-view-columns" icon="i-heroicons-view-columns"
color="gray" color="gray"

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
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]])
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'User name'
}, {
key: 'title',
label: 'Job position'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role'
}, {
key: 'select',
class: 'w-2'
}]
</script>
<template>
<UTable v-model="selected" :rows="people" :columns="columns" />
</template>

View File

@@ -16,7 +16,7 @@ Its goal is to provide everything related to UI when building a Nuxt app. This i
- Keyboard shortcuts - Keyboard shortcuts
- Bundled icons - Bundled icons
- Fully typed - Fully typed
- [Figma Kit](https://www.figma.com/community/file/1288455405058138934) - [Figma Kit](https://www.figma.com/community/file/1436401057300493073)
## Credits ## Credits

View File

@@ -141,6 +141,74 @@ Badge
You can customize the whole [preset](#preset) by using the `ui` prop. You can customize the whole [preset](#preset) by using the `ui` prop.
:: ::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.
::component-card
---
props:
icon: 'i-heroicons-rocket-launch'
size: 'sm'
color: 'primary'
variant: 'solid'
label: Badge
trailing: false
options:
- name: variant
restriction: only
values:
- solid
excludedProps:
- icon
- label
---
::
## 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: Badge
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-rocket-launch" class="w-4 h-4" />
props:
label: Badge
color: 'gray'
excludedProps:
- color
---
#trailing
:u-icon{name="i-heroicons-rocket-launch" class="w-4 h-4"}
::
## Props ## Props
:component-props :component-props

View File

@@ -142,7 +142,7 @@ props:
### Loading ### Loading
Use the `loading` prop to show a loading icon and disable the Input. Use the `loading` prop to show a loading icon in the Input.
Use the `loading-icon` prop to set a different icon or change it globally in `ui.input.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`. Use the `loading-icon` prop to set a different icon or change it globally in `ui.input.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.

View File

@@ -14,6 +14,7 @@ The Link component is a wrapper around [`<NuxtLink>`](https://nuxt.com/docs/api/
- `inactive-class` prop to set a class when the link is inactive, `active-class` is used when active. - `inactive-class` prop to set a class when the link is inactive, `active-class` is used when active.
- `exact` prop to style with `active-class` when the link is active and the route is exactly the same as the current route. - `exact` prop to style with `active-class` when the link is active and the route is exactly the same as the current route.
- `exact-query` and `exact-hash` props to style with `active-class` when the link is active and the query or hash is exactly the same as the current query or hash. - `exact-query` and `exact-hash` props to style with `active-class` when the link is active and the query or hash is exactly the same as the current query or hash.
- use `exact-query="partial"` to style with `active-class` when the link is active and the query partially match the current query.
The incentive behind this is to provide the same API as NuxtLink back in Nuxt 2 / Vue 2. You can read more about it in the Vue Router [migration from Vue 2](https://router.vuejs.org/guide/migration/#removal-of-the-exact-prop-in-router-link) guide. The incentive behind this is to provide the same API as NuxtLink back in Nuxt 2 / Vue 2. You can read more about it in the Vue Router [migration from Vue 2](https://router.vuejs.org/guide/migration/#removal-of-the-exact-prop-in-router-link) guide.

View File

@@ -137,9 +137,9 @@ excludedProps:
### Timeout ### 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. 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. The `pauseTimeoutOnHover` prop (`true` by default) controls whether hovering the notification should pause 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. 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 if `pauseTimeoutOnHover` is enabled; otherwise, it won't stop.
::component-card ::component-card
--- ---
@@ -149,6 +149,7 @@ baseProps:
description: 'This is a notification.' description: 'This is a notification.'
props: props:
timeout: 60000 timeout: 60000
pauseTimeoutOnHover: true
--- ---
:: ::

View File

@@ -62,7 +62,7 @@ extraClass: 'overflow-hidden'
padding: false padding: false
component: 'table-example-columns-selectable' component: 'table-example-columns-selectable'
componentProps: componentProps:
class: 'flex-1 flex-col overflow-hidden' class: 'flex-1 flex-col overflow-hidden min-h-[230px]'
--- ---
:: ::
@@ -285,9 +285,68 @@ componentProps:
--- ---
:: ::
#### Event Selectable
The `UTable` component provides two key events for handling row selection:
##### ***@select:all***
The `@select:all` event is emitted when the header checkbox in a selectable table is toggled. This event returns a boolean value indicating whether all rows are selected (true) or deselected (false).
##### ***@update:modelValue***
The `@update:modelValue` event is emitted whenever the selection state changes, including both individual row selection and bulk selection. This event returns an array containing the currently selected rows.
Here's how to implement both events:
```vue
<script setup lang="ts">
const selected = ref([])
const onHandleSelectAll = (isSelected: boolean) => {
console.log('All rows selected:', isSelected)
}
const onUpdateSelection = (selectedRows: any[]) => {
console.log('Currently selected rows:', selectedRows)
}
</script>
<template>
<UTable
v-model="selected"
:rows="people"
@select:all="onHandleSelectAll"
@update:modelValue="onUpdateSelection"
/>
</template>
```
#### Single Select Mode
Control how the select function allows only one row to be selected at a time.
```vue
<template>
<!-- Allow only one row to be selectable at a time -->
<UTable :single-select="true" />
</template>
```
#### Checkbox Placement
You can customize the checkbox column position by using the `select` key in the `columns` configuration.
::component-example{class="grid"}
---
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-dynamically-render-selectable'
componentProps:
class: 'flex-1'
---
::
### Contextmenu ### Contextmenu
Use the `contextmenu` listener on your Table to make the rows righ-clickable. The function will receive the original event as the first argument and the row as the second argument. Use the `contextmenu` listener on your Table to make the rows right-clickable. The function will receive the original event as the first argument and the row as the second argument.
You can use this to open a [ContextMenu](/components/context-menu) for that row. You can use this to open a [ContextMenu](/components/context-menu) for that row.
@@ -393,7 +452,6 @@ Controls whether multiple rows can be expanded simultaneously in the table.
<!-- Or simply --> <!-- Or simply -->
<UTable /> <UTable />
</template> </template>
``` ```
#### Disable Row Expansion #### Disable Row Expansion
@@ -534,6 +592,82 @@ componentProps:
--- ---
:: ::
### `select-header`
This slot allows you to customize the checkbox appearance in the table header for selecting all rows at once while using feature [Selectable](#selectable).
#### Usage
```vue
<template>
<UTable v-model="selectable">
<template #select-header="{ checked, change, indeterminate }">
<!-- Place your custom component here -->
</template>
</UTable>
</template>
```
#### Props
| Prop | Type | Description |
|------|------|-------------|
| `checked` | `Boolean` | Indicates if all rows are selected |
| `change` | `Function` | Function to handle selection state changes. Must receive a boolean value (true/false) |
| `indeterminate` | `Boolean` | Indicates partial selection (when some rows are selected) |
#### Example
```vue
<template>
<UTable>
<!-- Header checkbox customization -->
<template #select-header="{ indeterminate, checked, change }">
<input
type="checkbox"
:indeterminate="indeterminate"
:checked="checked"
@change="e => change(e.target.checked)"
/>
</template>
</UTable>
</template>
```
### `select-data`
This slot allows you to customize the checkbox appearance for each row in the table while using feature [Selectable](#selectable).
#### Usage
```vue
<template>
<UTable v-model="selectable">
<template #select-data="{ checked, change }">
<!-- Place your custom component here -->
</template>
</UTable>
</template>
```
#### Props
| Prop | Type | Description |
|------|------|-------------|
| `checked` | `Boolean` | Indicates if the current row is selected |
| `change` | `Function` | Function to handle selection state changes. Must receive a boolean value (true/false) |
#### Example
```vue
<template>
<UTable>
<!-- Row checkbox customization -->
<template #select-data="{ checked, change }">
<input
type="checkbox"
:checked="checked"
@change="e => change(e.target.checked)"
/>
</template>
</UTable>
</template>
```
### `expand-action` ### `expand-action`
The `#expand-action` slot allows you to customize the expansion control interface for expandable table rows. This feature provides a flexible way to implement custom expand/collapse functionality while maintaining access to essential row data and state. The `#expand-action` slot allows you to customize the expansion control interface for expandable table rows. This feature provides a flexible way to implement custom expand/collapse functionality while maintaining access to essential row data and state.

View File

@@ -27,8 +27,7 @@ export default defineNuxtConfig({
'@nuxtjs/plausible', '@nuxtjs/plausible',
'@vueuse/nuxt', '@vueuse/nuxt',
'nuxt-component-meta', 'nuxt-component-meta',
'nuxt-cloudflare-analytics', 'nuxt-cloudflare-analytics'
'modules/content-examples-code'
], ],
site: { site: {

View File

@@ -4,27 +4,27 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@iconify-json/heroicons": "^1.2.1", "@iconify-json/heroicons": "^1.2.1",
"@iconify-json/simple-icons": "^1.2.11", "@iconify-json/simple-icons": "^1.2.14",
"@iconify-json/vscode-icons": "^1.2.2", "@iconify-json/vscode-icons": "^1.2.3",
"@nuxt/content": "^2.13.4", "@nuxt/content": "^2.13.4",
"@nuxt/fonts": "^0.10.2", "@nuxt/fonts": "^0.10.3",
"@nuxt/image": "^1.8.1", "@nuxt/image": "^1.8.1",
"@nuxt/ui": "latest", "@nuxt/ui": "latest",
"@nuxt/ui-pro": "^1.5.0", "@nuxt/ui-pro": "^1.5.0",
"@nuxtjs/plausible": "^1.0.3", "@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^21.0.2", "@octokit/rest": "^21.0.2",
"@vueuse/nuxt": "^11.2.0", "@vueuse/nuxt": "^12.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"joi": "^17.13.3", "joi": "^17.13.3",
"nuxt": "^3.14.159", "nuxt": "^3.14.1592",
"nuxt-cloudflare-analytics": "^1.0.8", "nuxt-cloudflare-analytics": "^1.0.8",
"nuxt-component-meta": "^0.9.0", "nuxt-component-meta": "^0.9.0",
"nuxt-og-image": "^3.0.8", "nuxt-og-image": "^3.1.1",
"prettier": "^3.3.3", "prettier": "^3.4.2",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"v-calendar": "^3.1.2", "v-calendar": "^3.1.2",
"valibot": "^0.42.1", "valibot": "^0.42.1",
"yup": "^1.4.0", "yup": "^1.5.0",
"zod": "^3.23.8" "zod": "^3.23.8"
} }
} }

View File

@@ -98,7 +98,7 @@ const communityLinks = computed(() => [{
const resourcesLinks = [{ const resourcesLinks = [{
icon: 'i-simple-icons-figma', icon: 'i-simple-icons-figma',
label: 'Figma Kit', label: 'Figma Kit',
to: 'https://www.figma.com/community/file/1288455405058138934', to: 'https://www.figma.com/community/file/1436401057300493073',
target: '_blank' target: '_blank'
}, { }, {
label: 'Playground', label: 'Playground',

View File

@@ -1,8 +1,8 @@
{ {
"name": "@nuxt/ui", "name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.", "description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "2.19.2", "version": "2.20.0",
"packageManager": "pnpm@9.12.3", "packageManager": "pnpm@9.15.0",
"repository": "nuxt/ui", "repository": "nuxt/ui",
"homepage": "https://ui.nuxt.com", "homepage": "https://ui.nuxt.com",
"type": "module", "type": "module",
@@ -35,8 +35,8 @@
"@headlessui/tailwindcss": "^0.2.1", "@headlessui/tailwindcss": "^0.2.1",
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@iconify-json/heroicons": "^1.2.1", "@iconify-json/heroicons": "^1.2.1",
"@nuxt/icon": "^1.6.1", "@nuxt/icon": "^1.9.1",
"@nuxt/kit": "^3.14.159", "@nuxt/kit": "^3.14.1592",
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/tailwindcss": "^6.12.2", "@nuxtjs/tailwindcss": "^6.12.2",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
@@ -44,42 +44,45 @@
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@vueuse/core": "^11.2.0", "@vueuse/core": "^12.0.0",
"@vueuse/integrations": "^11.2.0", "@vueuse/integrations": "^12.0.0",
"@vueuse/math": "^11.2.0", "@vueuse/math": "^12.0.0",
"defu": "^6.1.4", "defu": "^6.1.4",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"scule": "^1.3.0", "scule": "^1.3.0",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.5",
"tailwindcss": "^3.4.14" "tailwindcss": "^3.4.16"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint-config": "^0.6.1", "@nuxt/eslint-config": "^0.7.2",
"@nuxt/module-builder": "^0.8.4", "@nuxt/module-builder": "^0.8.4",
"@nuxt/test-utils": "^3.14.4", "@nuxt/test-utils": "^3.15.1",
"@release-it/conventional-changelog": "^9.0.2", "@release-it/conventional-changelog": "^9.0.3",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"eslint": "^9.14.0", "eslint": "^9.16.0",
"happy-dom": "^14.12.3", "happy-dom": "^14.12.3",
"joi": "^17.13.3", "joi": "^17.13.3",
"nuxt": "^3.14.159", "nuxt": "^3.14.1592",
"release-it": "^17.10.0", "release-it": "^17.10.0",
"superstruct": "^2.0.2", "superstruct": "^2.0.2",
"typescript": "^5.6.3",
"unbuild": "^2.0.0", "unbuild": "^2.0.0",
"valibot": "^0.42.1", "valibot": "^0.42.1",
"valibot30": "npm:valibot@0.30.0", "valibot30": "npm:valibot@0.30.0",
"valibot31": "npm:valibot@0.31.0", "valibot31": "npm:valibot@0.31.0",
"vitest": "^2.1.4", "vitest": "^2.1.8",
"vitest-environment-nuxt": "^1.0.1", "vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.1.10", "vue-tsc": "^2.1.10",
"yup": "^1.4.0", "yup": "^1.5.0",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"resolutions": { "resolutions": {
"@nuxt/ui": "workspace:*", "@nuxt/ui": "workspace:*",
"@nuxt/content": "2.13.2", "@nuxt/content": "2.13.2",
"@nuxtjs/mdc": "0.9.0" "@nuxtjs/mdc": "0.9.0",
"chokidar": "3.6.0",
"typescript": "5.6.3"
} }
} }

View File

@@ -9,6 +9,6 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/ui": "latest", "@nuxt/ui": "latest",
"nuxt": "^3.14.159" "nuxt": "^3.14.1592"
} }
} }

4037
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,28 +8,27 @@
</slot> </slot>
<thead :class="ui.thead"> <thead :class="ui.thead">
<tr :class="ui.tr.base"> <tr :class="ui.tr.base">
<th v-if="modelValue" scope="col" :class="ui.checkbox.padding">
<UCheckbox
:model-value="isAllRowChecked"
:indeterminate="indeterminate"
v-bind="ui.default.checkbox"
aria-label="Select all"
@change="onChange"
/>
</th>
<th v-if="expand" scope="col" :class="ui.tr.base"> <th v-if="expand" scope="col" :class="ui.tr.base">
<span class="sr-only">Expand</span> <span class="sr-only">Expand</span>
</th> </th>
<th <th
v-for="(column, index) in columns" v-for="(column, index) in columns"
:key="index" :key="index"
scope="col" scope="col"
:class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size, column.class]" :class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size, column.key === 'select' && ui.checkbox.padding, column.class]"
:aria-sort="getAriaSort(column)" :aria-sort="getAriaSort(column)"
> >
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort"> <slot v-if="!singleSelect && modelValue && (column.key === 'select' || shouldRenderColumnInFirstPlace(index, 'select'))" name="select-header" :indeterminate="indeterminate" :checked="isAllRowChecked" :change="onChange">
<UCheckbox
:model-value="isAllRowChecked"
:indeterminate="indeterminate"
v-bind="ui.default.checkbox"
aria-label="Select all"
@change="onChange"
/>
</slot>
<slot v-else :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
<UButton <UButton
v-if="column.sortable" v-if="column.sortable"
v-bind="{ ...(ui.default.sortButton || {}), ...sortButton }" v-bind="{ ...(ui.default.sortButton || {}), ...sortButton }"
@@ -77,16 +76,7 @@
<template v-else> <template v-else>
<template v-for="(row, index) in rows" :key="index"> <template v-for="(row, index) in rows" :key="index">
<tr :class="[ui.tr.base, isSelected(row) && ui.tr.selected, isExpanded(row) && ui.tr.expanded, ($attrs.onSelect || $attrs.onContextmenu) && ui.tr.active, row?.class]" @click="() => onSelect(row)" @contextmenu="(event) => onContextmenu(event, row)"> <tr :class="[ui.tr.base, isSelected(row) && ui.tr.selected, isExpanded(row) && ui.tr.expanded, $attrs.onSelect && ui.tr.active, row?.class]" @click="() => onSelect(row)" @contextmenu="(event) => onContextmenu(event, row)">
<td v-if="modelValue" :class="ui.checkbox.padding">
<UCheckbox
:model-value="isSelected(row)"
v-bind="ui.default.checkbox"
aria-label="Select row"
@change="onChangeCheckbox($event, row)"
@click.capture.stop="() => onSelect(row)"
/>
</td>
<td <td
v-if="expand" v-if="expand"
:class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]"
@@ -102,8 +92,26 @@
@click.capture.stop="toggleOpened(row)" @click.capture.stop="toggleOpened(row)"
/> />
</td> </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, column?.rowClass, row[column.key]?.class]"> <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, column?.rowClass, row[column.key]?.class, column.key === 'select' && ui.checkbox.padding]">
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" :get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)"> <slot v-if="modelValue && (column.key === 'select' || shouldRenderColumnInFirstPlace(subIndex, 'select')) " name="select-data" :checked="isSelected(row)" :change="(ev: boolean) => onChangeCheckbox(ev, row)">
<UCheckbox
:model-value="isSelected(row)"
v-bind="ui.default.checkbox"
aria-label="Select row"
@change="onChangeCheckbox($event, row)"
@click.capture.stop="() => onSelect(row)"
/>
</slot>
<slot
v-else
:key="retriggerSlot"
:name="`${column.key}-data`"
:column="column"
:row="row"
:index="index"
:get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)"
>
{{ getRowData(row, column.key) }} {{ getRowData(row, column.key) }}
</slot> </slot>
</td> </td>
@@ -125,11 +133,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, toRaw, toRef } from 'vue' import { computed, defineComponent, ref, toRaw, toRef, watch } from 'vue'
import type { PropType, AriaAttributes } from 'vue' import type { PropType, AriaAttributes } from 'vue'
import { upperFirst } from 'scule' import { upperFirst } from 'scule'
import { defu } from 'defu' import { defu } from 'defu'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { isEqual } from 'ohash'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import UProgress from '../elements/Progress.vue' import UProgress from '../elements/Progress.vue'
@@ -144,7 +153,7 @@ import { table } from '#ui/ui.config'
const config = mergeConfig<typeof table>(appConfig.ui.strategy, appConfig.ui.table, table) const config = mergeConfig<typeof table>(appConfig.ui.strategy, appConfig.ui.table, table)
function defaultComparator<T>(a: T, z: T): boolean { function defaultComparator<T>(a: T, z: T): boolean {
return JSON.stringify(a) === JSON.stringify(z) return isEqual(a, z)
} }
function defaultSort(a: any, b: any, direction: 'asc' | 'desc') { function defaultSort(a: any, b: any, direction: 'asc' | 'desc') {
@@ -159,6 +168,14 @@ function defaultSort(a: any, b: any, direction: 'asc' | 'desc') {
} }
} }
function getStringifiedSet(arr: TableRow[]) {
return new Set(arr.map(item => JSON.stringify(item)))
}
function accessor<T extends Record<string, any>>(key: string) {
return (obj: T) => get(obj, key)
}
export default defineComponent({ export default defineComponent({
components: { components: {
UIcon, UIcon,
@@ -221,7 +238,7 @@ export default defineComponent({
default: false default: false
}, },
loadingState: { loadingState: {
type: Object as PropType<{ icon: string, label: string }>, type: Object as PropType<{ icon: string, label: string } | null>,
default: () => config.default.loadingState default: () => config.default.loadingState
}, },
emptyState: { emptyState: {
@@ -247,9 +264,13 @@ export default defineComponent({
multipleExpand: { multipleExpand: {
type: Boolean, type: Boolean,
default: true default: true
},
singleSelect: {
type: Boolean,
default: false
} }
}, },
emits: ['update:modelValue', 'update:sort', 'update:expand'], emits: ['update:modelValue', 'update:sort', 'update:expand', 'select:all'],
setup(props, { emit, attrs: $attrs }) { setup(props, { emit, attrs: $attrs }) {
const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class')) const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class'))
@@ -264,6 +285,8 @@ export default defineComponent({
}) })
}) })
const retriggerSlot = ref(null)
const savedSort = { column: sort.value.column, direction: null } const savedSort = { column: sort.value.column, direction: null }
const rows = computed(() => { const rows = computed(() => {
@@ -292,8 +315,6 @@ export default defineComponent({
} }
}) })
const getStringifiedSet = (arr: TableRow[]) => new Set(arr.map(item => JSON.stringify(item)))
const totalRows = computed(() => props.rows.length) const totalRows = computed(() => props.rows.length)
const countCheckedRow = computed(() => { const countCheckedRow = computed(() => {
@@ -328,10 +349,6 @@ export default defineComponent({
return props.by(a, z) return props.by(a, z)
} }
function accessor<T extends Record<string, any>>(key: string) {
return (obj: T) => get(obj, key)
}
function isSelected(row: TableRow) { function isSelected(row: TableRow) {
if (!props.modelValue) { if (!props.modelValue) {
return false return false
@@ -355,6 +372,11 @@ export default defineComponent({
} }
function onSelect(row: TableRow) { function onSelect(row: TableRow) {
const selection = window.getSelection()
if (selection && selection.toString().length > 0) {
return
}
if (!$attrs.onSelect) { if (!$attrs.onSelect) {
return return
} }
@@ -393,11 +415,12 @@ export default defineComponent({
} else { } else {
selected.value = [] selected.value = []
} }
emit('select:all', checked)
} }
function onChangeCheckbox(checked: boolean, row: TableRow) { function onChangeCheckbox(checked: boolean, row: TableRow) {
if (checked) { if (checked) {
selected.value.push(row) selected.value = props.singleSelect ? [row] : [...selected.value, row]
} else { } else {
const index = selected.value.findIndex(item => compare(item, row)) const index = selected.value.findIndex(item => compare(item, row))
selected.value.splice(index, 1) selected.value.splice(index, 1)
@@ -412,6 +435,13 @@ export default defineComponent({
return expand.value?.openedRows ? expand.value.openedRows.some(openedRow => compare(openedRow, row)) : false return expand.value?.openedRows ? expand.value.openedRows.some(openedRow => compare(openedRow, row)) : false
} }
function shouldRenderColumnInFirstPlace(index: number, key: string) {
if (!props.columns) {
return index === 0
}
return index === 0 && !props.columns.find(col => col.key === key)
}
function toggleOpened(row: TableRow) { function toggleOpened(row: TableRow) {
expand.value = { expand.value = {
openedRows: isExpanded(row) ? expand.value.openedRows.filter(v => !compare(v, row)) : props.multipleExpand ? [...expand.value.openedRows, row] : [row], openedRows: isExpanded(row) ? expand.value.openedRows.filter(v => !compare(v, row)) : props.multipleExpand ? [...expand.value.openedRows, row] : [row],
@@ -439,6 +469,12 @@ export default defineComponent({
return undefined return undefined
} }
watch(rows, () => {
retriggerSlot.value = new Date()
}, {
deep: true
})
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
@@ -465,7 +501,9 @@ export default defineComponent({
getRowData, getRowData,
toggleOpened, toggleOpened,
getAriaSort, getAriaSort,
isExpanded isExpanded,
shouldRenderColumnInFirstPlace,
retriggerSlot
} }
} }
}) })

View File

@@ -126,7 +126,7 @@ export default defineComponent({
default: () => ({}) default: () => ({})
} }
}, },
emits: ['open'], emits: ['open', 'close'],
setup(props, { emit }) { setup(props, { emit }) {
const { ui, attrs } = useUI('accordion', toRef(props, 'ui'), config, toRef(props, 'class')) const { ui, attrs } = useUI('accordion', toRef(props, 'ui'), config, toRef(props, 'class'))
@@ -142,6 +142,8 @@ export default defineComponent({
if (!isOpenBefore && isOpenAfter) { if (!isOpenBefore && isOpenAfter) {
emit('open', index) emit('open', index)
} else if (isOpenBefore && !isOpenAfter) {
emit('close', index)
} }
} }
}, { immediate: true }) }, { immediate: true })

View File

@@ -1,6 +1,18 @@
<template> <template>
<span :class="badgeClass" v-bind="attrs"> <span :class="badgeClass" v-bind="attrs">
<slot>{{ label }}</slot> <slot name="leading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
</slot>
<slot>
<span v-if="label">
{{ label }}
</span>
</slot>
<slot name="trailing">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</slot>
</span> </span>
</template> </template>
@@ -8,6 +20,7 @@
import { computed, toRef, defineComponent } from 'vue' import { computed, toRef, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils' import { mergeConfig } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup' import { useInjectButtonGroup } from '../../composables/useButtonGroup'
@@ -19,6 +32,9 @@ import { badge } from '#ui/ui.config'
const config = mergeConfig<typeof badge>(appConfig.ui.strategy, appConfig.ui.badge, badge) const config = mergeConfig<typeof badge>(appConfig.ui.strategy, appConfig.ui.badge, badge)
export default defineComponent({ export default defineComponent({
components: {
UIcon
},
inheritAttrs: false, inheritAttrs: false,
props: { props: {
size: { size: {
@@ -49,6 +65,26 @@ export default defineComponent({
type: [String, Number], type: [String, Number],
default: null default: null
}, },
icon: {
type: String,
default: null
},
leadingIcon: {
type: String,
default: null
},
trailingIcon: {
type: String,
default: null
},
trailing: {
type: Boolean,
default: false
},
leading: {
type: Boolean,
default: false
},
class: { class: {
type: [String, Object, Array] as PropType<any>, type: [String, Object, Array] as PropType<any>,
default: () => '' default: () => ''
@@ -63,6 +99,14 @@ export default defineComponent({
const { size, rounded } = useInjectButtonGroup({ ui, props }) const { size, rounded } = useInjectButtonGroup({ ui, props })
const isLeading = computed(() => {
return (props.icon && props.leading) || (props.icon && !props.trailing) || !props.trailing || props.leadingIcon
})
const isTrailing = computed(() => {
return (props.icon && props.trailing) || props.trailing || props.trailingIcon
})
const badgeClass = computed(() => { const badgeClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant] const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
@@ -71,13 +115,42 @@ export default defineComponent({
ui.value.font, ui.value.font,
rounded.value, rounded.value,
ui.value.size[size.value], ui.value.size[size.value],
ui.value.gap[size.value],
variant?.replaceAll('{color}', props.color) variant?.replaceAll('{color}', props.color)
), props.class) ), props.class)
}) })
const leadingIconName = computed(() => {
return props.leadingIcon || props.icon
})
const trailingIconName = computed(() => {
return props.trailingIcon || props.icon
})
const leadingIconClass = computed(() => {
return twJoin(
ui.value.icon.base,
ui.value.icon.size[size.value]
)
})
const trailingIconClass = computed(() => {
return twJoin(
ui.value.icon.base,
ui.value.icon.size[size.value]
)
})
return { return {
attrs, attrs,
badgeClass isLeading,
isTrailing,
badgeClass,
leadingIconName,
trailingIconName,
leadingIconClass,
trailingIconClass
} }
} }
}) })

View File

@@ -106,7 +106,7 @@ export default defineComponent({
default: () => '' default: () => ''
}, },
ui: { ui: {
type: Object as PropType<DeepPartial<typeof config & { strategy?: Strategy }>>, type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: undefined default: undefined
} }
}, },

View File

@@ -32,8 +32,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { isEqual } from 'ohash' import { isEqual, diff } from 'ohash'
import { defineComponent } from 'vue' import { type PropType, defineComponent } from 'vue'
import { nuxtLinkProps } from '../../utils' import { nuxtLinkProps } from '../../utils'
export default defineComponent({ export default defineComponent({
@@ -61,7 +61,7 @@ export default defineComponent({
default: false default: false
}, },
exactQuery: { exactQuery: {
type: Boolean, type: [Boolean, String] as PropType<boolean | 'partial'>,
default: false default: false
}, },
exactHash: { exactHash: {
@@ -74,9 +74,21 @@ export default defineComponent({
} }
}, },
setup(props) { setup(props) {
function isPartiallyEqual(item1, item2) {
const diffedKeys = diff(item1, item2).reduce((filtered, q) => {
if (q.type === 'added') {
filtered.push(q.key)
}
return filtered
}, [])
return isEqual(item1, item2, { excludeKeys: key => diffedKeys.includes(key) })
}
function resolveLinkClass(route, $route, { isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) { function resolveLinkClass(route, $route, { isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
if (props.exactQuery && !isEqual(route.query, $route.query)) { if (props.exactQuery === 'partial') {
return props.inactiveClass if (!isPartiallyEqual(route.query, $route.query)) return props.inactiveClass
} else if (props.exactQuery === true) {
if (!isEqual(route.query, $route.query)) return props.inactiveClass
} }
if (props.exactHash && route.hash !== $route.hash) { if (props.exactHash && route.hash !== $route.hash) {
return props.inactiveClass return props.inactiveClass

View File

@@ -60,6 +60,8 @@ export default defineComponent({
const formId = useId() const formId = useId()
const bus = useEventBus<FormEvent>(`form-${formId}`) const bus = useEventBus<FormEvent>(`form-${formId}`)
const parsedValue = ref(null)
onMounted(() => { onMounted(() => {
bus.on(async (event) => { bus.on(async (event) => {
if (event.type !== 'submit' && props.validateOn?.includes(event.type)) { if (event.type !== 'submit' && props.validateOn?.includes(event.type)) {
@@ -87,7 +89,7 @@ export default defineComponent({
if (errors) { if (errors) {
errs = errs.concat(errors) errs = errs.concat(errors)
} else { } else {
Object.assign(props.state, result) parsedValue.value = result
} }
} }
@@ -130,7 +132,7 @@ export default defineComponent({
if (props.validateOn?.includes('submit')) { if (props.validateOn?.includes('submit')) {
await validate() await validate()
} }
event.data = props.state event.data = props.schema ? parsedValue.value : props.state
emit('submit', event) emit('submit', event)
} catch (error) { } catch (error) {
if (!(error instanceof FormException)) { if (!(error instanceof FormException)) {
@@ -252,10 +254,10 @@ async function validateJoiSchema(
schema: JoiSchema schema: JoiSchema
): Promise<ValidateReturnSchema<typeof state>> { ): Promise<ValidateReturnSchema<typeof state>> {
try { try {
await schema.validateAsync(state, { abortEarly: false }) const result = await schema.validateAsync(state, { abortEarly: false })
return { return {
errors: null, errors: null,
result: state result
} }
} catch (error) { } catch (error) {
if (isJoiError(error)) { if (isJoiError(error)) {
@@ -321,7 +323,7 @@ async function validateYupSchema(
schema: YupObjectSchema<any> schema: YupObjectSchema<any>
): Promise<ValidateReturnSchema<typeof state>> { ): Promise<ValidateReturnSchema<typeof state>> {
try { try {
const result = schema.validateSync(state, { abortEarly: false }) const result = await schema.validate(state, { abortEarly: false })
return { return {
errors: null, errors: null,
result result

View File

@@ -48,7 +48,7 @@
v-slot="{ active, selected, disabled: optionDisabled }" v-slot="{ active, selected, disabled: optionDisabled }"
:key="index" :key="index"
as="template" as="template"
:value="valueAttribute ? option[valueAttribute] : option" :value="valueAttribute ? accessor(option, valueAttribute) : option"
:disabled="option.disabled" :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]"> <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]">
@@ -104,6 +104,7 @@ import {
import { computedAsync, useDebounceFn } from '@vueuse/core' import { computedAsync, useDebounceFn } from '@vueuse/core'
import { defu } from 'defu' import { defu } from 'defu'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import { isEqual } from 'ohash'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
@@ -292,6 +293,24 @@ export default defineComponent({
const size = computed(() => sizeButtonGroup.value ?? sizeFormGroup.value) const size = computed(() => sizeButtonGroup.value ?? sizeFormGroup.value)
const by = computed(() => {
if (!props.by) return undefined
if (typeof props.by === 'function') {
return props.by
}
const key = props.by
const hasDot = key.indexOf('.')
if (hasDot > 0) {
return (a: any, z: any) => {
return accessor(a, key) === accessor(z, key)
}
}
return key
})
const internalQuery = ref('') const internalQuery = ref('')
const query = computed({ const query = computed({
get() { get() {
@@ -304,12 +323,30 @@ export default defineComponent({
}) })
const label = computed(() => { const label = computed(() => {
if (!props.modelValue) { if (!props.modelValue) return null
return
function getValue(value: any) {
if (props.valueAttribute) {
return accessor(value, props.valueAttribute)
}
return value
}
function compareValues(value1: any, value2: any) {
if (by.value && typeof by.value !== 'function' && typeof value1 === 'object' && typeof value2 === 'object') {
return isEqual(value1[props.by], value2[props.by])
}
return isEqual(value1, value2)
} }
if (props.valueAttribute) { if (props.valueAttribute) {
const option = options.value.find(option => option[props.valueAttribute] === props.modelValue) const option = options.value.find((option) => {
const optionValue = getValue(option)
return compareValues(optionValue, props.modelValue)
})
return option ? accessor(option, props.optionAttribute) : null return option ? accessor(option, props.optionAttribute) : null
} else { } else {
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : accessor(props.modelValue as Record<string, any>, props.optionAttribute) return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : accessor(props.modelValue as Record<string, any>, props.optionAttribute)
@@ -486,7 +523,9 @@ export default defineComponent({
query, query,
accessor, accessor,
onUpdate, onUpdate,
onQueryChange onQueryChange,
// eslint-disable-next-line vue/no-dupe-keys
by
} }
} }
}) })

View File

@@ -71,7 +71,7 @@
v-slot="{ active, selected: optionSelected, disabled: optionDisabled }" v-slot="{ active, selected: optionSelected, disabled: optionDisabled }"
:key="index" :key="index"
as="template" as="template"
:value="valueAttribute ? option[valueAttribute] : option" :value="valueAttribute ? accessor(option, valueAttribute) : option"
:disabled="option.disabled" :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, optionSelected && uiMenu.option.selected, optionDisabled && uiMenu.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, optionSelected && uiMenu.option.selected, optionDisabled && uiMenu.option.disabled]">
@@ -140,6 +140,7 @@ import {
import { computedAsync, useDebounceFn } from '@vueuse/core' import { computedAsync, useDebounceFn } from '@vueuse/core'
import { defu } from 'defu' import { defu } from 'defu'
import { twMerge, twJoin } from 'tailwind-merge' import { twMerge, twJoin } from 'tailwind-merge'
import { isEqual } from 'ohash'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import { useUI } from '../../composables/useUI' import { useUI } from '../../composables/useUI'
@@ -347,6 +348,24 @@ export default defineComponent({
const [trigger, container] = usePopper(popper.value) const [trigger, container] = usePopper(popper.value)
const by = computed(() => {
if (!props.by) return undefined
if (typeof props.by === 'function') {
return props.by
}
const key = props.by
const hasDot = key.indexOf('.')
if (hasDot > 0) {
return (a: any, z: any) => {
return accessor(a, key) === accessor(z, key)
}
}
return key
})
const { size: sizeButtonGroup, rounded } = useInjectButtonGroup({ ui, props }) const { size: sizeButtonGroup, rounded } = useInjectButtonGroup({ ui, props })
const { emitFormBlur, emitFormChange, inputId, color, size: sizeFormGroup, name } = useFormGroup(props, config) const { emitFormBlur, emitFormChange, inputId, color, size: sizeFormGroup, name } = useFormGroup(props, config)
@@ -364,39 +383,49 @@ export default defineComponent({
}) })
const selected = computed(() => { const selected = computed(() => {
function compareValues(value1: any, value2: any) {
if (by.value && typeof by.value !== 'function' && typeof value1 === 'object' && typeof value2 === 'object') {
return isEqual(value1[by.value], value2[by.value])
}
return isEqual(value1, value2)
}
function getValue(value: any) {
if (props.valueAttribute) {
return accessor(value, props.valueAttribute)
}
return value
}
if (props.multiple) { if (props.multiple) {
if (!Array.isArray(props.modelValue) || !props.modelValue.length) { const modelValue = props.modelValue
if (!Array.isArray(modelValue) || !modelValue.length) {
return [] return []
} }
if (props.valueAttribute) { return options.value.filter((option) => {
return options.value.filter(option => (props.modelValue as any[]).includes(option[props.valueAttribute])) const optionValue = getValue(option)
} return modelValue.some(value => compareValues(value, optionValue))
return options.value.filter(option => (props.modelValue as any[]).includes(option)) })
} }
if (props.valueAttribute) { return options.value.find((option) => {
return options.value.find(option => option[props.valueAttribute] === props.modelValue) const optionValue = getValue(option)
} return compareValues(optionValue, toRaw(props.modelValue))
return options.value.find(option => option === props.modelValue) }) ?? props.modelValue
}) })
const label = computed(() => { const label = computed(() => {
if (props.multiple) { if (!props.modelValue) return null
if (Array.isArray(props.modelValue) && props.modelValue.length) {
return `${selected.value.length} selected` if (Array.isArray(props.modelValue) && props.modelValue.length) {
} else { return `${props.modelValue.length} selected`
return null } else if (['string', 'number'].includes(typeof props.modelValue)) {
} return props.valueAttribute ? accessor(selected.value, props.optionAttribute) : props.modelValue
} else if (props.modelValue !== undefined && props.modelValue !== null) {
if (props.valueAttribute) {
return accessor(selected.value, props.optionAttribute) ?? null
} else {
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : accessor(props.modelValue as Record<string, any>, props.optionAttribute)
}
} }
return null return accessor(props.modelValue as Record<string, any>, props.optionAttribute)
}) })
const selectClass = computed(() => { const selectClass = computed(() => {
@@ -597,7 +626,9 @@ export default defineComponent({
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
query, query,
onUpdate, onUpdate,
onQueryChange onQueryChange,
// eslint-disable-next-line vue/no-dupe-keys
by
} }
} }
}) })

View File

@@ -117,6 +117,10 @@ export default defineComponent({
ui: { ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>, type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}) default: () => ({})
},
pauseTimeoutOnHover: {
type: Boolean,
default: true
} }
}, },
emits: ['close'], emits: ['close'],
@@ -157,13 +161,13 @@ export default defineComponent({
}) })
function onMouseover() { function onMouseover() {
if (timer) { if (props.pauseTimeoutOnHover && timer) {
timer.pause() timer.pause()
} }
} }
function onMouseleave() { function onMouseleave() {
if (timer) { if (props.pauseTimeoutOnHover && timer) {
timer.resume() timer.resume()
} }
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<div :class="wrapperClass" role="region" v-bind="attrs"> <div v-if="notifications.length" :class="wrapperClass" role="region" v-bind="attrs">
<div v-if="notifications.length" :class="ui.container"> <div :class="ui.container">
<div v-for="notification of notifications" :key="notification.id"> <div v-for="notification of notifications" :key="notification.id">
<UNotification <UNotification
v-bind="notification" v-bind="notification"

3
src/runtime/types/checkbox.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
import type colors from '#ui-colors'
export type CheckboxColor = typeof colors[number]

View File

@@ -1,3 +1,3 @@
import type { divider } from '#ui/ui.config' import type { divider } from '../ui.config'
export type DividerSize = keyof typeof divider.border.size.horizontal | keyof typeof divider.border.size.vertical export type DividerSize = keyof typeof divider.border.size.horizontal | keyof typeof divider.border.size.vertical

View File

@@ -4,6 +4,7 @@ export * from './avatar'
export * from './badge' export * from './badge'
export * from './breadcrumb' export * from './breadcrumb'
export * from './button' export * from './button'
export * from './checkbox'
export * from './chip' export * from './chip'
export * from './clipboard' export * from './clipboard'
export * from './command-palette' export * from './command-palette'

View File

@@ -6,7 +6,7 @@ export interface Link extends NuxtLinkProps {
disabled?: boolean disabled?: boolean
active?: boolean active?: boolean
exact?: boolean exact?: boolean
exactQuery?: boolean exactQuery?: boolean | 'partial'
exactHash?: boolean exactHash?: boolean
inactiveClass?: string inactiveClass?: string
} }

View File

@@ -7,7 +7,7 @@ export interface TightMap<O = any> {
export type DeepPartial<T, O = any> = { export type DeepPartial<T, O = any> = {
[P in keyof T]?: T[P] extends object [P in keyof T]?: T[P] extends object
? DeepPartial<T[P], O> ? DeepPartial<T[P], O>
: T[P]; : T[P] extends string ? string : T[P];
} & { } & {
[key: string]: O | TightMap<O> [key: string]: O | TightMap<O>
} }

View File

@@ -1,3 +1,5 @@
import type { ButtonColor, ButtonSize, ButtonVariant, CheckboxColor, ProgressAnimation, ProgressColor } from '../../types'
export default { export default {
wrapper: 'relative overflow-x-auto', wrapper: 'relative overflow-x-auto',
base: 'min-w-full table-fixed', base: 'min-w-full table-fixed',
@@ -51,23 +53,23 @@ export default {
icon: 'i-heroicons-arrows-up-down-20-solid', icon: 'i-heroicons-arrows-up-down-20-solid',
trailing: true, trailing: true,
square: true, square: true,
color: 'gray' as const, color: 'gray' as ButtonColor,
variant: 'ghost' as const, variant: 'ghost' as ButtonVariant,
class: '-m-1.5' class: '-m-1.5'
}, },
expandButton: { expandButton: {
icon: 'i-heroicons-chevron-down', icon: 'i-heroicons-chevron-down',
color: 'gray' as const, color: 'gray' as ButtonColor,
variant: 'ghost' as const, variant: 'ghost' as ButtonVariant,
size: 'xs' as const, size: 'xs' as ButtonSize,
class: '-my-1.5 align-middle' class: '-my-1.5 align-middle'
}, },
checkbox: { checkbox: {
color: 'primary' as const color: 'primary' as CheckboxColor
}, },
progress: { progress: {
color: 'primary' as const, color: 'primary' as ProgressColor,
animation: 'carousel' as const animation: 'carousel' as ProgressAnimation
}, },
loadingState: { loadingState: {
icon: 'i-heroicons-arrow-path-20-solid', icon: 'i-heroicons-arrow-path-20-solid',

View File

@@ -16,7 +16,7 @@ export default {
openIcon: 'i-heroicons-chevron-down-20-solid', openIcon: 'i-heroicons-chevron-down-20-solid',
closeIcon: '', closeIcon: '',
class: 'mb-1.5 w-full', class: 'mb-1.5 w-full',
variant: 'soft' as const, variant: 'soft',
truncate: true truncate: true
} }
} }

View File

@@ -1,3 +1,5 @@
import type { AvatarSize, ButtonColor, ButtonSize, ButtonVariant } from '../../types'
export default { export default {
wrapper: 'w-full relative overflow-hidden', wrapper: 'w-full relative overflow-hidden',
inner: 'w-0 flex-1', inner: 'w-0 flex-1',
@@ -13,7 +15,7 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0 self-center', base: 'flex-shrink-0 self-center',
size: 'md' as const size: 'md' as AvatarSize
}, },
color: { color: {
white: { white: {
@@ -32,9 +34,9 @@ export default {
icon: null, icon: null,
closeButton: null, closeButton: null,
actionButton: { actionButton: {
size: 'xs' as const, size: 'xs' as ButtonSize,
color: 'primary' as const, color: 'primary' as ButtonColor,
variant: 'link' as const variant: 'link' as ButtonVariant
} }
} }
} }

View File

@@ -8,6 +8,12 @@ export default {
md: 'text-sm px-2 py-1', md: 'text-sm px-2 py-1',
lg: 'text-sm px-2.5 py-1.5' lg: 'text-sm px-2.5 py-1.5'
}, },
gap: {
xs: 'gap-0.5',
sm: 'gap-1',
md: 'gap-1',
lg: 'gap-1.5'
},
color: { color: {
white: { white: {
solid: 'ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-900 dark:text-white bg-white dark:bg-gray-900' solid: 'ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-900 dark:text-white bg-white dark:bg-gray-900'
@@ -25,6 +31,15 @@ export default {
soft: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400', soft: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400',
subtle: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-25 dark:ring-opacity-25' subtle: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-25 dark:ring-opacity-25'
}, },
icon: {
base: 'flex-shrink-0',
size: {
xs: 'h-4 w-4',
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-5 w-5'
}
},
default: { default: {
size: 'sm', size: 'sm',
variant: 'solid', variant: 'solid',

View File

@@ -1,3 +1,5 @@
import type { ButtonColor } from '../../types'
export default { export default {
wrapper: 'relative', wrapper: 'relative',
container: 'relative w-full flex overflow-x-auto snap-x snap-mandatory scroll-smooth', container: 'relative w-full flex overflow-x-auto snap-x snap-mandatory scroll-smooth',
@@ -13,12 +15,12 @@ export default {
}, },
default: { default: {
prevButton: { prevButton: {
color: 'black' as const, color: 'black' as ButtonColor,
class: 'rtl:[&_span:first-child]:rotate-180 absolute start-4 top-1/2 transform -translate-y-1/2 rounded-full', class: 'rtl:[&_span:first-child]:rotate-180 absolute start-4 top-1/2 transform -translate-y-1/2 rounded-full',
icon: 'i-heroicons-chevron-left-20-solid' icon: 'i-heroicons-chevron-left-20-solid'
}, },
nextButton: { nextButton: {
color: 'black' as const, color: 'black' as ButtonColor,
class: 'rtl:[&_span:last-child]:rotate-180 absolute end-4 top-1/2 transform -translate-y-1/2 rounded-full', class: 'rtl:[&_span:last-child]:rotate-180 absolute end-4 top-1/2 transform -translate-y-1/2 rounded-full',
icon: 'i-heroicons-chevron-right-20-solid' icon: 'i-heroicons-chevron-right-20-solid'
} }

View File

@@ -1,3 +1,4 @@
import type { AvatarSize } from '../../types'
import { arrow } from '../popper' import { arrow } from '../popper'
export default { export default {
@@ -28,7 +29,7 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as const size: '2xs' as AvatarSize
}, },
label: 'truncate', label: 'truncate',
shortcuts: 'hidden md:inline-flex flex-shrink-0 gap-0.5 ms-auto' shortcuts: 'hidden md:inline-flex flex-shrink-0 gap-0.5 ms-auto'

View File

@@ -1,3 +1,4 @@
import type { AvatarSize } from '../../types'
import { arrow } from '../popper' import { arrow } from '../popper'
export default { export default {
@@ -36,7 +37,7 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as const size: '2xs' as AvatarSize
}, },
chip: { chip: {
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full' base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'

View File

@@ -1,3 +1,5 @@
import type { AvatarSize } from '../../types'
export default { export default {
wrapper: { wrapper: {
base: 'flex items-center align-center text-center', base: 'flex items-center align-center text-center',
@@ -42,11 +44,11 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as const size: '2xs' as AvatarSize
}, },
label: 'text-sm', label: 'text-sm',
default: { default: {
size: '2xs' as const, size: '2xs',
type: 'solid' as const type: 'solid'
} }
} }

View File

@@ -1,3 +1,5 @@
import type { AvatarSize } from '../../types'
export default { export default {
wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800', wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800',
container: 'relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2', container: 'relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2',
@@ -46,7 +48,7 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as const size: '2xs' as AvatarSize
}, },
chip: { chip: {
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full' base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'

View File

@@ -1,3 +1,5 @@
import type { AvatarSize, BadgeColor, BadgeSize, BadgeVariant } from '../../types'
export default { export default {
wrapper: 'relative w-full flex items-center justify-between', wrapper: 'relative w-full flex items-center justify-between',
container: 'flex items-center min-w-0', container: 'flex items-center min-w-0',
@@ -15,12 +17,12 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as const size: '2xs' as AvatarSize
}, },
badge: { badge: {
base: 'flex-shrink-0 ms-auto relative rounded', base: 'flex-shrink-0 ms-auto relative rounded',
color: 'gray' as const, color: 'gray' as BadgeColor,
variant: 'solid' as const, variant: 'solid' as BadgeVariant,
size: 'xs' as const size: 'xs' as BadgeSize
} }
} }

View File

@@ -1,3 +1,5 @@
import type { ButtonColor } from '../../types'
export default { export default {
wrapper: 'flex items-center -space-x-px', wrapper: 'flex items-center -space-x-px',
base: '', base: '',
@@ -5,28 +7,28 @@ export default {
default: { default: {
size: 'sm', size: 'sm',
activeButton: { activeButton: {
color: 'primary' as const color: 'primary' as ButtonColor
}, },
inactiveButton: { inactiveButton: {
color: 'white' as const color: 'white' as ButtonColor
}, },
firstButton: { firstButton: {
color: 'white' as const, color: 'white' as ButtonColor,
class: 'rtl:[&_span:first-child]:rotate-180', class: 'rtl:[&_span:first-child]:rotate-180',
icon: 'i-heroicons-chevron-double-left-20-solid' icon: 'i-heroicons-chevron-double-left-20-solid'
}, },
lastButton: { lastButton: {
color: 'white' as const, color: 'white' as ButtonColor,
class: 'rtl:[&_span:last-child]:rotate-180', class: 'rtl:[&_span:last-child]:rotate-180',
icon: 'i-heroicons-chevron-double-right-20-solid' icon: 'i-heroicons-chevron-double-right-20-solid'
}, },
prevButton: { prevButton: {
color: 'white' as const, color: 'white' as ButtonColor,
class: 'rtl:[&_span:first-child]:rotate-180', class: 'rtl:[&_span:first-child]:rotate-180',
icon: 'i-heroicons-chevron-left-20-solid' icon: 'i-heroicons-chevron-left-20-solid'
}, },
nextButton: { nextButton: {
color: 'white' as const, color: 'white' as ButtonColor,
class: 'rtl:[&_span:last-child]:rotate-180', class: 'rtl:[&_span:last-child]:rotate-180',
icon: 'i-heroicons-chevron-right-20-solid' icon: 'i-heroicons-chevron-right-20-solid'
} }

View File

@@ -1,3 +1,5 @@
import type { AvatarSize, BadgeColor, BadgeSize, BadgeVariant } from '../../types'
export default { export default {
wrapper: 'relative', wrapper: 'relative',
base: 'group relative flex items-center gap-1.5 focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-1 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 before:absolute before:inset-px before:rounded-md disabled:cursor-not-allowed disabled:opacity-75', base: 'group relative flex items-center gap-1.5 focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-1 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 before:absolute before:inset-px before:rounded-md disabled:cursor-not-allowed disabled:opacity-75',
@@ -17,13 +19,13 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '2xs' as const size: '2xs' as AvatarSize
}, },
badge: { badge: {
base: 'flex-shrink-0 ms-auto relative rounded', base: 'flex-shrink-0 ms-auto relative rounded',
color: 'gray' as const, color: 'gray' as BadgeColor,
variant: 'solid' as const, variant: 'solid' as BadgeVariant,
size: 'xs' as const size: 'xs' as BadgeSize
}, },
divider: { divider: {
wrapper: { wrapper: {

View File

@@ -1,3 +1,5 @@
import type { AvatarSize, ButtonColor, ButtonSize, ButtonVariant } from '../../types'
export default { export default {
wrapper: 'w-full pointer-events-auto', wrapper: 'w-full pointer-events-auto',
container: 'relative overflow-hidden', container: 'relative overflow-hidden',
@@ -17,7 +19,7 @@ export default {
}, },
avatar: { avatar: {
base: 'flex-shrink-0 self-center', base: 'flex-shrink-0 self-center',
size: 'md' as const size: 'md' as AvatarSize
}, },
progress: { progress: {
base: 'absolute bottom-0 end-0 start-0 h-1', base: 'absolute bottom-0 end-0 start-0 h-1',
@@ -38,13 +40,13 @@ export default {
timeout: 5000, timeout: 5000,
closeButton: { closeButton: {
icon: 'i-heroicons-x-mark-20-solid', icon: 'i-heroicons-x-mark-20-solid',
color: 'gray' as const, color: 'gray' as ButtonColor,
variant: 'link' as const, variant: 'link' as ButtonVariant,
padded: false padded: false
}, },
actionButton: { actionButton: {
size: 'xs' as const, size: 'xs' as ButtonSize,
color: 'white' as const color: 'white' as ButtonColor
} }
} }
} }