mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 12:39:35 +01:00
Compare commits
150 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d28bb0efa8 | ||
|
|
d67c7482ac | ||
|
|
b8db18513d | ||
|
|
a3b33ac917 | ||
|
|
0f25f8563e | ||
|
|
81126b299a | ||
|
|
4ce8348a43 | ||
|
|
0776455a71 | ||
|
|
1a937919a2 | ||
|
|
b9fe74bca5 | ||
|
|
e116f931b2 | ||
|
|
393b992aeb | ||
|
|
c187d367ff | ||
|
|
d43fb835d8 | ||
|
|
033fcfacd8 | ||
|
|
e0977b2933 | ||
|
|
4405d3239f | ||
|
|
a3a7201396 | ||
|
|
29029ca8ae | ||
|
|
2862741e5f | ||
|
|
e4fd20888b | ||
|
|
5c759c326d | ||
|
|
4c9c8d343a | ||
|
|
28b736a703 | ||
|
|
02d72df527 | ||
|
|
a44bfc8511 | ||
|
|
b0df864379 | ||
|
|
969b02d936 | ||
|
|
d3e19dc65a | ||
|
|
cefa597664 | ||
|
|
bad8a69a36 | ||
|
|
6e4230fd63 | ||
|
|
0be676a9ef | ||
|
|
e48b61b5df | ||
|
|
56e0c9a9a0 | ||
|
|
c6841d06a4 | ||
|
|
8508e84958 | ||
|
|
6154ae94a9 | ||
|
|
6384edf92a | ||
|
|
4f3af6cfdb | ||
|
|
893b2466ff | ||
|
|
9807e58f8f | ||
|
|
4124406032 | ||
|
|
3258167a14 | ||
|
|
520624bd64 | ||
|
|
e4b8fffc32 | ||
|
|
5d781112f1 | ||
|
|
1c8122a00b | ||
|
|
0976833753 | ||
|
|
bc00f9c4b2 | ||
|
|
c6aa4215d7 | ||
|
|
3334e2af3d | ||
|
|
3844714644 | ||
|
|
84e6392981 | ||
|
|
c2ef6237d8 | ||
|
|
f735db04d6 | ||
|
|
e8f573b6bb | ||
|
|
288abf239f | ||
|
|
44d93a1cfd | ||
|
|
217840bb41 | ||
|
|
ea2a24b5fe | ||
|
|
4a25a12390 | ||
|
|
d64cb8a6fd | ||
|
|
00d0fd5919 | ||
|
|
30e7a3ca20 | ||
|
|
7d572c81bb | ||
|
|
97a3975197 | ||
|
|
43b999c88e | ||
|
|
7151b7b97d | ||
|
|
ffd20b3991 | ||
|
|
29e64ca963 | ||
|
|
556ee0d9c4 | ||
|
|
debafef0fa | ||
|
|
2d9038bcb0 | ||
|
|
f7f8f06b91 | ||
|
|
56e1fed373 | ||
|
|
648eec31b9 | ||
|
|
d0ce8ee1c4 | ||
|
|
9d8f358139 | ||
|
|
bc6474a9ad | ||
|
|
31924e32f2 | ||
|
|
c963ba688f | ||
|
|
4dee128524 | ||
|
|
4c84839a01 | ||
|
|
fd30022550 | ||
|
|
1a1c640220 | ||
|
|
5c99ae131d | ||
|
|
b22bd70d54 | ||
|
|
0c8ab9d98e | ||
|
|
0fdc8f70b6 | ||
|
|
23770d8cf0 | ||
|
|
84e75ad237 | ||
|
|
00dd8c27bd | ||
|
|
5f81a79edf | ||
|
|
1a02b3abe7 | ||
|
|
83631ccbca | ||
|
|
0f9b5d47a6 | ||
|
|
f623ec1130 | ||
|
|
a79f7c0a34 | ||
|
|
1c9835d7f1 | ||
|
|
6d8d82a265 | ||
|
|
66a80c7486 | ||
|
|
0546c7922c | ||
|
|
eafe707c7d | ||
|
|
5d1919a538 | ||
|
|
781365a5ed | ||
|
|
0129e2db40 | ||
|
|
45b1a4bd32 | ||
|
|
f32f578125 | ||
|
|
4b044866a5 | ||
|
|
9b768ec12b | ||
|
|
7584d72f42 | ||
|
|
6b5ddc18bd | ||
|
|
4dd92f7f36 | ||
|
|
cbc27422a4 | ||
|
|
db508b218f | ||
|
|
ad33b26729 | ||
|
|
f07968afef | ||
|
|
a8dc9b216a | ||
|
|
32474e21f7 | ||
|
|
c023fb400c | ||
|
|
4548809ee5 | ||
|
|
6b52963339 | ||
|
|
2c2ff0f473 | ||
|
|
0b762d61e7 | ||
|
|
9cbb68871c | ||
|
|
7c5b47ea72 | ||
|
|
7196d81b4c | ||
|
|
1cb8df869f | ||
|
|
67cc349c6c | ||
|
|
1f0f6181db | ||
|
|
18b6133b11 | ||
|
|
51bfb9a4e1 | ||
|
|
76e1cc84db | ||
|
|
d539f2540b | ||
|
|
e53cdeaf0b | ||
|
|
4d72a758fa | ||
|
|
a2e9b7da07 | ||
|
|
e408eabd8b | ||
|
|
5718dfd69a | ||
|
|
4a9b66aeb3 | ||
|
|
464ff0b703 | ||
|
|
6984989a2c | ||
|
|
2dcc11ff89 | ||
|
|
29efa99fb7 | ||
|
|
6c432028ae | ||
|
|
0270ce9251 | ||
|
|
182e3b6e8f | ||
|
|
26afa45fbf | ||
|
|
edd92d01a9 |
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,7 +4,7 @@
|
||||
|
||||
### 🔗 Linked issue
|
||||
|
||||
<!-- Please ensure there is an open issue and mention its number as #123 -->
|
||||
<!-- If it resolves an open issue, please link the issue here. For example "Resolves #123" -->
|
||||
|
||||
### ❓ Type of change
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
<!-- Describe your changes in detail -->
|
||||
<!-- Why is this change required? What problem does it solve? -->
|
||||
<!-- If it resolves an open issue, please link to the issue here. For example "Resolves #1337" -->
|
||||
|
||||
### 📝 Checklist
|
||||
|
||||
|
||||
2
.github/workflows/ci-dev.yml
vendored
2
.github/workflows/ci-dev.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
|
||||
1
.npmrc
1
.npmrc
@@ -1,2 +1,3 @@
|
||||
shamefully-hoist=true
|
||||
auto-install-peers=true
|
||||
ignore-workspace-root-check=true
|
||||
|
||||
1
.nuxtrc
1
.nuxtrc
@@ -1,3 +1,2 @@
|
||||
imports.autoImport=false
|
||||
typescript.includeWorkspace=true
|
||||
typescript.strict=false
|
||||
|
||||
86
CHANGELOG.md
86
CHANGELOG.md
@@ -1,5 +1,91 @@
|
||||
# Changelog
|
||||
|
||||
## [2.12.1](https://github.com/nuxt/ui/compare/v2.12.0...v2.12.1) (2024-01-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Button:** inherit nuxt link props without breaking `nuxt-component-meta` ([d3e19dc](https://github.com/nuxt/ui/commit/d3e19dc65a530201c3adc7738e95e5a09b0a9274)), closes [#578](https://github.com/nuxt/ui/issues/578)
|
||||
* **Button:** pass-through nuxt link props to `ULink` ([a44bfc8](https://github.com/nuxt/ui/commit/a44bfc85114bed15ed25bb8c79d7ed52adc8d43c))
|
||||
* **InputMenu:** take `option-attribute` into account to display label ([1a93791](https://github.com/nuxt/ui/commit/1a937919a26546cfd7edb3f6a11ef790d401999d))
|
||||
* **Link:** prevent `type` bind on `<a>` ([b0df864](https://github.com/nuxt/ui/commit/b0df86437902696b594e5e7042601506a8bf4436))
|
||||
* **SelectMenu:** take `option-attribute` into account to display label ([b9fe74b](https://github.com/nuxt/ui/commit/b9fe74bca5f48555e76c16237c2acc868f69e243)), closes [#1151](https://github.com/nuxt/ui/issues/1151)
|
||||
* **Tooltip:** typo in kbd component ([4405d32](https://github.com/nuxt/ui/commit/4405d3239f7e19d399659347f079555318b3231b))
|
||||
|
||||
## [2.12.0](https://github.com/nuxt/ui/compare/v2.11.1...v2.12.0) (2024-01-09)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **Card:** remove `overflow-hidden` on wrapper
|
||||
|
||||
### Features
|
||||
|
||||
* **Breadcrumb:** handle `labelClass` and merge `iconClass` ([f623ec1](https://github.com/nuxt/ui/commit/f623ec1130edf448988784b36c15a850470685c4))
|
||||
* **Dropdown:** handle `labelClass` and merge `iconClass` ([1c9835d](https://github.com/nuxt/ui/commit/1c9835d7f149231cf2e3e053e5ea08eceeaaa61d)), closes [#716](https://github.com/nuxt/ui/issues/716)
|
||||
* **Dropdown:** handle manual mode ([3844714](https://github.com/nuxt/ui/commit/38447146445618a1310a6315c608f4cd21069e17)), closes [#1143](https://github.com/nuxt/ui/issues/1143)
|
||||
* **Form:** expose submit function ([#1186](https://github.com/nuxt/ui/issues/1186)) ([4a25a12](https://github.com/nuxt/ui/commit/4a25a12390f8ecae83c1081c89eba99a8fda14f8))
|
||||
* **InputMenu:** new component ([#1095](https://github.com/nuxt/ui/issues/1095)) ([6d8d82a](https://github.com/nuxt/ui/commit/6d8d82a265692aaee556e40b09e4b3048ae044da))
|
||||
* **Pagination:** add `disabled` prop ([0976833](https://github.com/nuxt/ui/commit/0976833753cd2140649bc324f53a263d4e09ecff)), closes [#1189](https://github.com/nuxt/ui/issues/1189)
|
||||
* **Popover:** open and close events ([#1038](https://github.com/nuxt/ui/issues/1038)) ([f32f578](https://github.com/nuxt/ui/commit/f32f578125c12b35e59db2f7981c8b1b5a146397))
|
||||
* **SelectMenu:** add `empty` slot when no options ([5d1919a](https://github.com/nuxt/ui/commit/5d1919a5381b316637d50405d287428f67f2b9cc)), closes [#1089](https://github.com/nuxt/ui/issues/1089)
|
||||
* **SelectMenu:** allow control of search query ([f735db0](https://github.com/nuxt/ui/commit/f735db04d62fca678ca30ecd565b32e70bcda3e0)), closes [#1174](https://github.com/nuxt/ui/issues/1174)
|
||||
* **SelectMenu:** allow creating option despite search ([#1080](https://github.com/nuxt/ui/issues/1080)) ([0fdc8f7](https://github.com/nuxt/ui/commit/0fdc8f70b6a656114d30b07d682e4edcd61a23fb))
|
||||
* **Table:** add `sort-mode` prop ([56e0c9a](https://github.com/nuxt/ui/commit/56e0c9a9a05e1e8491e2d460b8d51084bd2c1305)), closes [#1149](https://github.com/nuxt/ui/issues/1149)
|
||||
* **Table:** add custom sort function to columns ([#1075](https://github.com/nuxt/ui/issues/1075)) ([4f3af6c](https://github.com/nuxt/ui/commit/4f3af6cfdb5213d1be3d2680fcf3a95f7b3bc0b3))
|
||||
* **VerticalNavigation:** ability to add dividers ([#963](https://github.com/nuxt/ui/issues/963)) ([ffd20b3](https://github.com/nuxt/ui/commit/ffd20b3991a35ae7fa0e249fa009e330fd963705))
|
||||
* **VerticalNavigation:** handle `labelClass` and merge `iconClass` ([a79f7c0](https://github.com/nuxt/ui/commit/a79f7c0a34c0414fe4feb95691e1f044b07ef087))
|
||||
* **VerticalNavigation:** improve accessibility ([#948](https://github.com/nuxt/ui/issues/948)) ([29e64ca](https://github.com/nuxt/ui/commit/29e64ca963eeed1e82640957860f43391d8683ed))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Alert:** always pass a function to actions click events ([5d78111](https://github.com/nuxt/ui/commit/5d781112f1eb464658c83047bf80c2ea7c9a2b05)), closes [#1197](https://github.com/nuxt/ui/issues/1197)
|
||||
* **Card:** remove `overflow-hidden` on wrapper ([4124406](https://github.com/nuxt/ui/commit/412440603206151d63b04ffe6bed1bbc5b0e6615)), closes [#806](https://github.com/nuxt/ui/issues/806) [#1034](https://github.com/nuxt/ui/issues/1034)
|
||||
* **config:** prevent class merge of `avatar` size ([b22bd70](https://github.com/nuxt/ui/commit/b22bd70d54e68c3217ba42690210084749fee656))
|
||||
* **Dropdown:** improve placement with `hover` mode ([c6aa421](https://github.com/nuxt/ui/commit/c6aa4215d7f9003adeefa7cdff76c7a88715f20c)), closes [#1179](https://github.com/nuxt/ui/issues/1179)
|
||||
* **Dropdown:** merge item `class` ([7151b7b](https://github.com/nuxt/ui/commit/7151b7b97d42f389506521044ebaffa8a299e7fb)), closes [#1157](https://github.com/nuxt/ui/issues/1157)
|
||||
* **Form:** invalid errors when using `clear` by path ([#1165](https://github.com/nuxt/ui/issues/1165)) ([97a3975](https://github.com/nuxt/ui/commit/97a39751977bf1e942e2bafd5839141383b7af2f))
|
||||
* **Form:** memory leak ([#1185](https://github.com/nuxt/ui/issues/1185)) ([ea2a24b](https://github.com/nuxt/ui/commit/ea2a24b5fe6ddc87e6eb951a662ce8b84b9d987f))
|
||||
* **forms:** dont disable inputs and selects on `loading` ([3258167](https://github.com/nuxt/ui/commit/3258167a1431b664cd1dcc925a4b3fe06a996831)), closes [#1117](https://github.com/nuxt/ui/issues/1117)
|
||||
* **Link:** handle `active` override when value is false ([83631cc](https://github.com/nuxt/ui/commit/83631ccbca1364f012b0c2899f97e2166dd1d360))
|
||||
* **Popover:** allow manual mode without blocking normal behaviour ([3334e2a](https://github.com/nuxt/ui/commit/3334e2af3de2844de08ee530e62f2e4e2fd7ed24))
|
||||
* **Popover:** improve placement with `hover` mode ([bc00f9c](https://github.com/nuxt/ui/commit/bc00f9c4b25dd4b99cb6e53014624f41ee929654)), closes [#781](https://github.com/nuxt/ui/issues/781)
|
||||
* **RadioGroup:** pass `option.disabled` to children ([0c8ab9d](https://github.com/nuxt/ui/commit/0c8ab9d98e494c49cceac111edc0606ee4d63638)), closes [#1109](https://github.com/nuxt/ui/issues/1109)
|
||||
* **SelectMenu:** input border focus after `tailwindcss` 3.4 ([e8f573b](https://github.com/nuxt/ui/commit/e8f573b6bb32a22873d9f93b40883ca12b481d7e))
|
||||
* **Table:** display nothing instead of error when key is missing ([00d0fd5](https://github.com/nuxt/ui/commit/00d0fd59192cc171abb3d2ddaee46b2b9fa9422f)), closes [#1173](https://github.com/nuxt/ui/issues/1173)
|
||||
* **Table:** respect sort prop updates from parent component ([#1208](https://github.com/nuxt/ui/issues/1208)) ([c6841d0](https://github.com/nuxt/ui/commit/c6841d06a48ffef95d238f94a4822a1e48b85422))
|
||||
* **Toggle:** add missing `change` event ([4c84839](https://github.com/nuxt/ui/commit/4c84839a0183756b9f8df8674aace8cd40e44dcd)), closes [#1113](https://github.com/nuxt/ui/issues/1113)
|
||||
* update vue and fix type issues ([#1112](https://github.com/nuxt/ui/issues/1112)) ([5c99ae1](https://github.com/nuxt/ui/commit/5c99ae131d1a50a8db21f1d5794a06080c515831))
|
||||
* **useShortcuts:** include `contenteditable="plaintext-only"` elements in `usingInput` ([#1159](https://github.com/nuxt/ui/issues/1159)) ([648eec3](https://github.com/nuxt/ui/commit/648eec31b99fcffb65c042e0a5587da941c8e90f))
|
||||
* **useShortcuts:** invalid code after [#1159](https://github.com/nuxt/ui/issues/1159) ([56e1fed](https://github.com/nuxt/ui/commit/56e1fed373786fc158ca9da9f02a9ec4e273afce))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "docs: pull `nuxt/ui-pro` docs from `main` branch" ([d0ce8ee](https://github.com/nuxt/ui/commit/d0ce8ee1c4a3d7b2285885d76e02e03168011110))
|
||||
|
||||
## [2.11.1](https://github.com/nuxt/ui/compare/v2.11.0...v2.11.1) (2023-12-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Breadcrumb:** handle divider in rtl ([#1049](https://github.com/nuxt/ui/issues/1049)) ([e53cdea](https://github.com/nuxt/ui/commit/e53cdeaf0b3746da76cb6a658a5f71064d97fc9a))
|
||||
* **CommandPalette:** improve performances and avoid multiple recompute ([db508b2](https://github.com/nuxt/ui/commit/db508b218f5277b2522566f790bd268eae2ee1e5))
|
||||
* **CommandPalette:** missing right padding on input with close button ([ad33b26](https://github.com/nuxt/ui/commit/ad33b26729b1bf3d21f8d480e04c197f4fbb6119))
|
||||
* **components:** move remaining classes to config ([#1039](https://github.com/nuxt/ui/issues/1039)) ([e408eab](https://github.com/nuxt/ui/commit/e408eabd8b841cdf8c71ce27c35c9675f2db8625))
|
||||
* **module:** prevent class merging on `default` children ([f07968a](https://github.com/nuxt/ui/commit/f07968afef263d38183ce6c9cd9185ef7eee0494)), closes [#1076](https://github.com/nuxt/ui/issues/1076)
|
||||
* **Notification:** handle dynamic backgrounds ([#1063](https://github.com/nuxt/ui/issues/1063)) ([1f0f618](https://github.com/nuxt/ui/commit/1f0f6181db7fa1ab45b8f7fec8df1cedccaec688))
|
||||
* **RadioGroup:** props reactivity issues ([#1065](https://github.com/nuxt/ui/issues/1065)) ([7196d81](https://github.com/nuxt/ui/commit/7196d81b4cecf1711a01bed5fed1236ab3b2398b))
|
||||
* **types:** favor `Record<string, any>>` instead of `object` ([4d72a75](https://github.com/nuxt/ui/commit/4d72a758fad5cffa09f3aaf6b3df9baf7edc2a9f))
|
||||
* **types:** improve with strict mode ([#1041](https://github.com/nuxt/ui/issues/1041)) ([4a9b66a](https://github.com/nuxt/ui/commit/4a9b66aeb32a332e2d5be7e236e5d4567044b3e2))
|
||||
* **types:** workaround for `popper` weak type ([5718dfd](https://github.com/nuxt/ui/commit/5718dfd69a7040987354485b30f7da7aee342abb)), closes [#644](https://github.com/nuxt/ui/issues/644)
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "chore(deps): pin `vitest`" ([6984989](https://github.com/nuxt/ui/commit/6984989a2c20fbde177d1e64ea1a7cae07f03c4d))
|
||||
|
||||
## [2.11.0](https://github.com/nuxt/ui/compare/v2.10.0...v2.11.0) (2023-11-23)
|
||||
|
||||
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NavItem } from '@nuxt/content/dist/runtime/types'
|
||||
import type { Link } from '#ui-pro/types'
|
||||
import type { HeaderLink } from '#ui-pro/types'
|
||||
|
||||
defineProps<{
|
||||
links: Link[]
|
||||
links: HeaderLink[]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -56,7 +56,7 @@ defineProps({
|
||||
<h1 class="m-0 text-[75px] font-semibold mb-2 text-white flex items-center">
|
||||
<span>{{ title }}</span>
|
||||
</h1>
|
||||
<p class="text-[32px] text-[#94a3b8] leading-tight">
|
||||
<p v-if="description" class="text-[32px] text-[#94a3b8] leading-tight">
|
||||
{{ description.slice(0, 200) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
45
docs/components/ads/AdsCarbon.vue
Normal file
45
docs/components/ads/AdsCarbon.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div ref="carbonads" class="carbon" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const carbonads = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (carbonads.value) {
|
||||
const script = document.createElement('script')
|
||||
script.setAttribute('type', 'text/javascript')
|
||||
script.setAttribute('src', 'https://cdn.carbonads.com/carbon.js?serve=CWYIVK3E&placement=uinuxtcom')
|
||||
script.setAttribute('id', '_carbonads_js')
|
||||
carbonads.value.appendChild(script)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="postcss">
|
||||
.carbon > #carbonads {
|
||||
@apply relative border border-gray-200 dark:border-gray-800 rounded-lg bg-white dark:bg-gray-800/50 hover:border-gray-300 dark:hover:border-gray-700 w-full transition-colors min-h-[220px];
|
||||
|
||||
&:hover {
|
||||
.carbon-text {
|
||||
@apply text-gray-700 dark:text-gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.carbon-img {
|
||||
@apply flex justify-center p-2 w-full;
|
||||
|
||||
& > img {
|
||||
@apply !max-w-full w-full rounded;
|
||||
}
|
||||
}
|
||||
|
||||
.carbon-text {
|
||||
@apply flex px-2 text-sm text-gray-500 dark:text-gray-400 transition-colors text-center w-full;
|
||||
}
|
||||
|
||||
.carbon-poweredby {
|
||||
@apply block text-xs text-center text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 pt-1 pb-2 px-2 transition-colors;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
27
docs/components/ads/AdsPro.vue
Normal file
27
docs/components/ads/AdsPro.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="relative group/ad border border-gray-200 dark:border-gray-800 rounded-lg bg-white dark:bg-gray-800/50 hover:border-gray-300 dark:hover:border-gray-700 p-2 w-full transition-colors">
|
||||
<NuxtLink to="/pro" class="focus:outline-none" tabindex="-1">
|
||||
<span class="absolute inset-0" aria-hidden="true" />
|
||||
</NuxtLink>
|
||||
|
||||
<UColorModeImage
|
||||
light="/pro/illustrations/docs-light.svg"
|
||||
dark="/pro/illustrations/docs-dark.svg"
|
||||
alt="Nuxt UI Pro"
|
||||
loading="lazy"
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col items-center mt-2 text-center">
|
||||
<div class="inline-flex gap-1.5">
|
||||
<Logo class="h-4 w-auto" />
|
||||
|
||||
<UBadge variant="subtle" size="xs" label="Pro" class="font-semibold" />
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 group-hover/ad:text-gray-700 dark:group-hover/ad:text-gray-200 mt-1 transition-colors">
|
||||
The Building Blocks for Modern Web Apps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -110,6 +110,10 @@ const props = defineProps({
|
||||
componentClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
ignoreVModel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -213,11 +217,15 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
const code = computed(() => {
|
||||
let code = `\`\`\`html
|
||||
<template>
|
||||
<${name}`
|
||||
for (const [key, value] of Object.entries(fullProps.value)) {
|
||||
if (value === 'undefined' || value === null) {
|
||||
continue
|
||||
}
|
||||
if (key === 'modelValue' && props.ignoreVModel) {
|
||||
continue
|
||||
}
|
||||
|
||||
code += ` ${(typeof value === 'boolean' && (value !== true || key === 'modelValue')) || typeof value === 'object' || typeof value === 'number' ? ':' : ''}${key === 'modelValue' ? 'model-value' : kebabCase(key)}${typeof value === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}`
|
||||
}
|
||||
@@ -239,7 +247,7 @@ const code = computed(() => {
|
||||
} else {
|
||||
code += ' />'
|
||||
}
|
||||
code += `
|
||||
code += `\n</template>
|
||||
\`\`\`
|
||||
`
|
||||
return code
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Field v-bind="prop">
|
||||
<code v-if="prop.default">{{ prop.default }}</code>
|
||||
<code v-if="prop.default" class="leading-6">{{ prop.default }}</code>
|
||||
<p v-if="prop.description">
|
||||
{{ prop.description }}
|
||||
</p>
|
||||
@@ -22,8 +22,8 @@
|
||||
<ComponentPropsField v-for="subProp in Object.values(prop.schema.schema)" :key="(subProp as any).name" :prop="subProp" />
|
||||
</FieldGroup>
|
||||
</Collapsible>
|
||||
<div v-else-if="prop.schema?.kind === 'enum' && prop.schema.type !== 'boolean' && startsWithCapital(prop.schema.type) && !prop.schema.type.startsWith(prop.schema.schema[0])" class="space-x-1 leading-7 -my-1">
|
||||
<code v-for="value in prop.schema.schema.filter(value => typeof value === 'string')" :key="value" class="whitespace-pre-wrap break-words">{{ value }}</code>
|
||||
<div v-else-if="prop.schema?.kind === 'enum' && prop.schema.type !== 'boolean' && startsWithCapital(prop.schema.type) && !prop.schema.type.startsWith(prop.schema.schema[0])" class="flex items-center flex-wrap gap-1 -my-1">
|
||||
<code v-for="value in prop.schema.schema.filter(value => typeof value === 'string')" :key="value" class="whitespace-pre-wrap break-words leading-6">{{ value }}</code>
|
||||
</div>
|
||||
</Field>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,7 @@ const form = reactive({ email: 'mail@example.com', password: 'password' })
|
||||
<UButton label="Login" color="gray" block />
|
||||
</div>
|
||||
|
||||
<UDivider label="OR" color="gray" orientation="vertical" />
|
||||
<UDivider label="OR" orientation="vertical" />
|
||||
|
||||
<div class="space-y-4 flex flex-col justify-center">
|
||||
<UButton color="black" label="Login with GitHub" icon="i-simple-icons-github" block />
|
||||
@@ -37,7 +37,7 @@ const form = reactive({ email: 'mail@example.com', password: 'password' })
|
||||
|
||||
<UButton label="Login" color="gray" block />
|
||||
|
||||
<UDivider label="OR" color="gray" />
|
||||
<UDivider label="OR" />
|
||||
|
||||
<UButton color="black" label="Login with GitHub" icon="i-simple-icons-github" block />
|
||||
<UButton color="black" label="Login with Google" icon="i-simple-icons-google" block />
|
||||
|
||||
22
docs/components/content/examples/DropdownExampleOpen.vue
Normal file
22
docs/components/content/examples/DropdownExampleOpen.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
const items = [
|
||||
[{
|
||||
label: 'Profile',
|
||||
avatar: {
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
}
|
||||
}]
|
||||
]
|
||||
|
||||
const open = ref(true)
|
||||
|
||||
defineShortcuts({
|
||||
o: () => open.value = !open.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDropdown v-model:open="open" :items="items" :popper="{ placement: 'bottom-start' }">
|
||||
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
</UDropdown>
|
||||
</template>
|
||||
@@ -10,6 +10,7 @@ const options = [
|
||||
|
||||
const state = reactive({
|
||||
input: undefined,
|
||||
inputMenu: undefined,
|
||||
textarea: undefined,
|
||||
select: undefined,
|
||||
selectMenu: undefined,
|
||||
@@ -23,6 +24,9 @@ const state = reactive({
|
||||
|
||||
const schema = z.object({
|
||||
input: z.string().min(10),
|
||||
inputMenu: z.any().refine(option => option?.value === 'option-2', {
|
||||
message: 'Select Option 2'
|
||||
}),
|
||||
textarea: z.string().min(10),
|
||||
select: z.string().refine(value => value === 'option-2', {
|
||||
message: 'Select Option 2'
|
||||
@@ -61,6 +65,10 @@ async function onSubmit (event: FormSubmitEvent<Schema>) {
|
||||
<UInput v-model="state.input" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup name="inputMenu" label="Input Menu">
|
||||
<UInputMenu v-model="state.inputMenu" :options="options" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup name="textarea" label="Textarea">
|
||||
<UTextarea v-model="state.textarea" />
|
||||
</UFormGroup>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" />
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
const people = []
|
||||
|
||||
const selected = ref()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people">
|
||||
<template #empty>
|
||||
No people
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
36
docs/components/content/examples/InputMenuExampleObjects.vue
Normal file
36
docs/components/content/examples/InputMenuExampleObjects.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
const people = [{
|
||||
id: 'benjamincanac',
|
||||
label: 'benjamincanac',
|
||||
href: 'https://github.com/benjamincanac',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
|
||||
}, {
|
||||
id: 'Atinux',
|
||||
label: 'Atinux',
|
||||
href: 'https://github.com/Atinux',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
|
||||
}, {
|
||||
id: 'smarroufin',
|
||||
label: 'smarroufin',
|
||||
href: 'https://github.com/smarroufin',
|
||||
target: '_blank',
|
||||
avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' }
|
||||
}, {
|
||||
id: 'nobody',
|
||||
label: 'Nobody',
|
||||
icon: 'i-heroicons-user-circle'
|
||||
}]
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people">
|
||||
<template #leading>
|
||||
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4 mx-0.5" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" class="mx-0.5" />
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
const people = [{
|
||||
id: 1,
|
||||
name: 'Wade Cooper'
|
||||
}, {
|
||||
id: 2,
|
||||
name: 'Arlene Mccoy'
|
||||
}, {
|
||||
id: 3,
|
||||
name: 'Devon Webb'
|
||||
}, {
|
||||
id: 4,
|
||||
name: 'Tom Cook'
|
||||
}]
|
||||
|
||||
const selected = ref(people[0].id)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
v-model="selected"
|
||||
:options="people"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" searchable>
|
||||
<template #option-empty="{ query }">
|
||||
<q>{{ query }}</q> not found
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
const people = [
|
||||
{ name: 'Wade Cooper', online: true },
|
||||
{ name: 'Arlene Mccoy', online: false },
|
||||
{ name: 'Devon Webb', online: false },
|
||||
{ name: 'Tom Cook', online: true },
|
||||
{ name: 'Tanya Fox', online: false },
|
||||
{ name: 'Hellen Schmidt', online: true },
|
||||
{ name: 'Caroline Schultz', online: true },
|
||||
{ name: 'Mason Heaney', online: false },
|
||||
{ name: 'Claudie Smitham', online: true },
|
||||
{ name: 'Emil Schaefer', online: false }
|
||||
]
|
||||
|
||||
const selected = ref(people[3])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" option-attribute="name">
|
||||
<template #option="{ option: person }">
|
||||
<span :class="[person.online ? 'bg-green-400' : 'bg-gray-200', 'inline-block h-2 w-2 flex-shrink-0 rounded-full']" aria-hidden="true" />
|
||||
<span class="truncate">{{ person.name }}</span>
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" :popper="{ arrow: true }" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" :popper="{ offsetDistance: 0 }" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref(people[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people" :popper="{ placement: 'right-start' }" />
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
const loading = ref(false)
|
||||
const selected = ref()
|
||||
|
||||
async function search (q) {
|
||||
loading.value = true
|
||||
|
||||
const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
|
||||
|
||||
loading.value = false
|
||||
|
||||
return users
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
v-model="selected"
|
||||
:search="search"
|
||||
:loading="loading"
|
||||
placeholder="Search for a user..."
|
||||
option-attribute="name"
|
||||
trailing
|
||||
by="id"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
const options = [
|
||||
{ id: 1, name: 'Wade Cooper', colors: ['red', 'yellow'] },
|
||||
{ id: 2, name: 'Arlene Mccoy', colors: ['blue', 'yellow'] },
|
||||
{ id: 3, name: 'Devon Webb', colors: ['green', 'blue'] },
|
||||
{ id: 4, name: 'Tom Cook', colors: ['blue', 'red'] },
|
||||
{ id: 5, name: 'Tanya Fox', colors: ['green', 'red'] },
|
||||
{ id: 5, name: 'Hellen Schmidt', colors: ['green', 'yellow'] }
|
||||
]
|
||||
|
||||
const selected = ref(options[1])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
v-model="selected"
|
||||
:options="options"
|
||||
placeholder="Select a person"
|
||||
by="id"
|
||||
option-attribute="name"
|
||||
:search-attributes="['name', 'colors']"
|
||||
>
|
||||
<template #option="{ option: person }">
|
||||
<span v-for="color in person.colors" :key="color.id" class="h-2 w-2 rounded-full" :class="`bg-${color}-500 dark:bg-${color}-400`" />
|
||||
<span class="truncate">{{ person.name }}</span>
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref()
|
||||
const query = ref('Wade')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
v-model="selected"
|
||||
v-model:query="query"
|
||||
:options="people"
|
||||
placeholder="Select a person"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,18 +1,19 @@
|
||||
<script setup>
|
||||
const open = ref(false)
|
||||
const open = ref(true)
|
||||
|
||||
defineShortcuts({
|
||||
o: () => open.value = !open.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-4 items-center">
|
||||
<UToggle v-model="open" />
|
||||
<UPopover :open="open">
|
||||
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
<UPopover v-model:open="open">
|
||||
<UButton color="white" :label="open.toString()" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
|
||||
<template #panel>
|
||||
<div class="p-4">
|
||||
<Placeholder class="h-20 w-48" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
<template #panel>
|
||||
<div class="p-4">
|
||||
<Placeholder class="h-20 w-48" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<template>
|
||||
<div class="flex gap-4 items-center">
|
||||
<UPopover overlay>
|
||||
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
<UPopover overlay>
|
||||
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||
|
||||
<template #panel>
|
||||
<div class="p-4">
|
||||
<Placeholder class="h-20 w-48" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
<template #panel>
|
||||
<div class="p-4">
|
||||
<Placeholder class="h-20 w-48" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
|
||||
@@ -23,6 +23,7 @@ const labels = computed({
|
||||
|
||||
// In a real app, you would make an API call to create the label
|
||||
const response = {
|
||||
id: options.value.length + 1,
|
||||
name: label.name,
|
||||
color: generateColorFromString(label.name)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
const options = ref([
|
||||
{ id: 1, name: 'bug' },
|
||||
{ id: 2, name: 'documentation' },
|
||||
{ id: 3, name: 'duplicate' },
|
||||
{ id: 4, name: 'enhancement' },
|
||||
{ id: 5, name: 'good first issue' },
|
||||
{ id: 6, name: 'help wanted' },
|
||||
{ id: 7, name: 'invalid' },
|
||||
{ id: 8, name: 'question' },
|
||||
{ id: 9, name: 'wontfix' }
|
||||
])
|
||||
|
||||
const selected = ref([])
|
||||
|
||||
const labels = computed({
|
||||
get: () => selected.value,
|
||||
set: async (labels) => {
|
||||
const promises = labels.map(async (label) => {
|
||||
if (label.id) {
|
||||
return label
|
||||
}
|
||||
|
||||
// In a real app, you would make an API call to create the label
|
||||
const response = {
|
||||
id: options.value.length + 1,
|
||||
name: label.name
|
||||
}
|
||||
|
||||
options.value.push(response)
|
||||
|
||||
return response
|
||||
})
|
||||
|
||||
selected.value = await Promise.all(promises)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
v-model="labels"
|
||||
by="id"
|
||||
name="labels"
|
||||
:options="options"
|
||||
option-attribute="name"
|
||||
multiple
|
||||
searchable
|
||||
creatable
|
||||
show-create-option-when="always"
|
||||
placeholder="Select labels"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
const people = []
|
||||
|
||||
const selected = ref()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu v-model="selected" :options="people">
|
||||
<template #empty>
|
||||
No people
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
@@ -28,11 +28,9 @@ const selected = ref(people[0])
|
||||
|
||||
<template>
|
||||
<USelectMenu v-model="selected" :options="people">
|
||||
<template #label>
|
||||
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" />
|
||||
|
||||
{{ selected.label }}
|
||||
<template #leading>
|
||||
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4 mx-0.5" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" class="mx-0.5" />
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
|
||||
@@ -14,8 +14,6 @@ const people = [{
|
||||
}]
|
||||
|
||||
const selected = ref(people[0].id)
|
||||
|
||||
const current = computed(() => people.find(person => person.id === selected.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -25,9 +23,5 @@ const current = computed(() => people.find(person => person.id === selected.valu
|
||||
placeholder="Select people"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
>
|
||||
<template #label>
|
||||
{{ current.name }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
<script setup>
|
||||
const search = async (q) => {
|
||||
const loading = ref(false)
|
||||
const selected = ref([])
|
||||
|
||||
async function search (q) {
|
||||
loading.value = true
|
||||
|
||||
const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
|
||||
|
||||
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email })).filter(Boolean)
|
||||
}
|
||||
loading.value = false
|
||||
|
||||
const selected = ref([])
|
||||
return users
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
v-model="selected"
|
||||
:loading="loading"
|
||||
:searchable="search"
|
||||
placeholder="Search for a user..."
|
||||
option-attribute="name"
|
||||
multiple
|
||||
trailing
|
||||
by="id"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
const options = [
|
||||
{ id: 1, name: 'Wade Cooper', favoriteColors: ['red', 'yellow'] },
|
||||
{ id: 2, name: 'Arlene Mccoy', favoriteColors: ['blue', 'yellow'] },
|
||||
{ id: 3, name: 'Devon Webb', favoriteColors: ['green', 'blue'] },
|
||||
{ id: 4, name: 'Tom Cook', favoriteColors: ['blue', 'red'] },
|
||||
{ id: 5, name: 'Tanya Fox', favoriteColors: ['green', 'red'] },
|
||||
{ id: 5, name: 'Hellen Schmidt', favoriteColors: ['green', 'yellow'] }
|
||||
{ id: 1, name: 'Wade Cooper', colors: ['red', 'yellow'] },
|
||||
{ id: 2, name: 'Arlene Mccoy', colors: ['blue', 'yellow'] },
|
||||
{ id: 3, name: 'Devon Webb', colors: ['green', 'blue'] },
|
||||
{ id: 4, name: 'Tom Cook', colors: ['blue', 'red'] },
|
||||
{ id: 5, name: 'Tanya Fox', colors: ['green', 'red'] },
|
||||
{ id: 5, name: 'Hellen Schmidt', colors: ['green', 'yellow'] }
|
||||
]
|
||||
|
||||
const selected = ref(options[1])
|
||||
@@ -15,16 +15,15 @@ const selected = ref(options[1])
|
||||
<USelectMenu
|
||||
v-model="selected"
|
||||
:options="options"
|
||||
class="w-full lg:w-96"
|
||||
placeholder="Select an user"
|
||||
placeholder="Select a person"
|
||||
searchable
|
||||
searchable-placeholder="Search by name or favorite colors"
|
||||
searchable-placeholder="Search by name or color"
|
||||
option-attribute="name"
|
||||
by="id"
|
||||
:search-attributes="['name', 'favoriteColors']"
|
||||
:search-attributes="['name', 'colors']"
|
||||
>
|
||||
<template #option="{ option: person }">
|
||||
<span v-for="color in person.favoriteColors" :key="color.id" class="h-2 w-2 rounded-full" :class="`bg-${color}-500 dark:bg-${color}-400`" />
|
||||
<span v-for="color in person.colors" :key="color.id" class="h-2 w-2 rounded-full" :class="`bg-${color}-500 dark:bg-${color}-400`" />
|
||||
<span class="truncate">{{ person.name }}</span>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
|
||||
const selected = ref()
|
||||
const query = ref('Wade')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
v-model="selected"
|
||||
v-model:query="query"
|
||||
:options="people"
|
||||
placeholder="Select a person"
|
||||
searchable
|
||||
/>
|
||||
</template>
|
||||
@@ -77,6 +77,7 @@ const resetFilters = () => {
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const sort = ref({ column: 'id', direction: 'asc' as const })
|
||||
const page = ref(1)
|
||||
const pageCount = ref(10)
|
||||
const pageTotal = ref(200) // This value should be dynamic coming from the API
|
||||
@@ -92,11 +93,13 @@ const { data: todos, pending } = await useLazyAsyncData<{
|
||||
query: {
|
||||
q: search.value,
|
||||
'_page': page.value,
|
||||
'_limit': pageCount.value
|
||||
'_limit': pageCount.value,
|
||||
'_sort': sort.value.column,
|
||||
'_order': sort.value.direction
|
||||
}
|
||||
}), {
|
||||
default: () => [],
|
||||
watch: [page, search, searchStatus, pageCount]
|
||||
watch: [page, search, searchStatus, pageCount, sort]
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -175,11 +178,13 @@ const { data: todos, pending } = await useLazyAsyncData<{
|
||||
<!-- Table -->
|
||||
<UTable
|
||||
v-model="selectedRows"
|
||||
v-model:sort="sort"
|
||||
:rows="todos"
|
||||
:columns="columnsTable"
|
||||
:loading="pending"
|
||||
sort-asc-icon="i-heroicons-arrow-up"
|
||||
sort-desc-icon="i-heroicons-arrow-down"
|
||||
sort-mode="manual"
|
||||
class="w-full"
|
||||
:ui="{ td: { base: 'max-w-[0] truncate' } }"
|
||||
@select="select"
|
||||
|
||||
@@ -60,5 +60,5 @@ const people = [{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :columns="columns" :rows="people" :sort="{ column: 'title' }" />
|
||||
<UTable :columns="columns" :rows="people" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup>
|
||||
const links = [
|
||||
[
|
||||
{
|
||||
label: 'Profile',
|
||||
avatar: {
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||
},
|
||||
badge: 100
|
||||
}, {
|
||||
label: 'Installation',
|
||||
icon: 'i-heroicons-home',
|
||||
to: '/getting-started/installation'
|
||||
}, {
|
||||
label: 'Vertical Navigation',
|
||||
icon: 'i-heroicons-chart-bar',
|
||||
to: '/navigation/vertical-navigation'
|
||||
}, {
|
||||
label: 'Command Palette',
|
||||
icon: 'i-heroicons-command-line',
|
||||
to: '/navigation/command-palette'
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Examples',
|
||||
icon: 'i-heroicons-light-bulb',
|
||||
to: '/getting-started/examples#verticalnavigation'
|
||||
},
|
||||
{
|
||||
label: 'Help',
|
||||
icon: 'i-heroicons-question-mark-circle',
|
||||
to: '/getting-started/examples'
|
||||
}
|
||||
]
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UVerticalNavigation :links="links" />
|
||||
</template>
|
||||
@@ -14,9 +14,6 @@ const links = [{
|
||||
}, {
|
||||
label: 'Examples',
|
||||
to: '/getting-started/examples'
|
||||
}, {
|
||||
label: 'Roadmap',
|
||||
to: '/getting-started/roadmap'
|
||||
}]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ onMounted(() => {
|
||||
/>
|
||||
</UAvatarGroup>
|
||||
|
||||
<UButton label="Button" icon="i-heroicons-pencil-square" />
|
||||
<UButton label="Button" loading />
|
||||
|
||||
<UBadge label="Badge" />
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<NuxtLink v-if="date.release" :to="`https://github.com/nuxt/ui/releases/tag/${date.release.name}`" target="_blank" class="text-gray-900 dark:text-white font-bold text-3xl mt-2 group hover:text-primary-500 dark:hover:text-primary-400 transition-[color]">
|
||||
{{ date.release.name }}
|
||||
</NuxtLink>
|
||||
<ul v-else-if="date.pulls?.length" class="mt-2 space-y-1 text-gray-600 dark:text-gray-300">
|
||||
<ul v-if="date.pulls?.length" class="mt-2 space-y-1 text-gray-600 dark:text-gray-300">
|
||||
<li v-for="pull in date.pulls" :key="pull.id" class="text-sm/6 break-all">
|
||||
<NuxtLink :to="`https://github.com/${pull.user.login}`" target="_blank" class="text-gray-900 dark:text-white transition-colors inline-flex items-center gap-1 rounded-full bg-gray-100/50 dark:bg-gray-800/50 dark:hover:bg-gray-800 p-0.5 pr-1 ring-1 ring-gray-300 dark:ring-gray-700 text-xs font-medium flex-shrink-0 align-middle">
|
||||
<UAvatar :src="`https://github.com/${pull.user.login}.png`" size="3xs" />
|
||||
|
||||
@@ -20,6 +20,10 @@ yarn add @nuxt/ui
|
||||
npm install @nuxt/ui
|
||||
```
|
||||
|
||||
```bash [bun]
|
||||
bun add @nuxt/ui
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
2. Add it to your `modules` section in your `nuxt.config`:
|
||||
@@ -32,8 +36,114 @@ export default defineNuxtConfig({
|
||||
|
||||
That's it! You can now use all the components and composables in your Nuxt app ✨
|
||||
|
||||
## Modules
|
||||
|
||||
Nuxt UI will automatically install the [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/), [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) and [nuxt-icon](https://github.com/nuxt-modules/icon) modules for you.
|
||||
|
||||
::callout{icon="i-heroicons-exclamation-triangle"}
|
||||
As this module installs [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/) and [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) for you, you should remove them from your `modules` and `dependencies` if you've previously installed them manually.
|
||||
You should remove them from your `modules` and `dependencies` if you've previously installed them.
|
||||
::
|
||||
|
||||
### `@nuxtjs/tailwindcss`
|
||||
|
||||
This module is pre-configured and will automatically load the following plugins:
|
||||
|
||||
- [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms)
|
||||
- [@tailwindcss/typography](https://github.com/tailwindlabs/tailwindcss-typography)
|
||||
- [@tailwindcss/aspect-ratio](https://github.com/tailwindlabs/tailwindcss-aspect-ratio)
|
||||
- [@tailwindcss/container-queries](https://github.com/tailwindlabs/tailwindcss-container-queries)
|
||||
- [@headlessui/tailwindcss](https://github.com/tailwindlabs/headlessui/tree/main/packages/%40headlessui-vue)
|
||||
|
||||
Note that the `@tailwindcss/aspect-ratio` plugin disables the default aspect ratio utilities:
|
||||
|
||||
- `aspect-auto`
|
||||
- `aspect-square`
|
||||
- `aspect-video`
|
||||
|
||||
You can re-enable them by adding the following to your `tailwind.config.ts`:
|
||||
|
||||
```ts [tailwind.config.ts]
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
export default <Partial<Config>>{
|
||||
theme: {
|
||||
extend: {
|
||||
aspectRatio: {
|
||||
auto: 'auto',
|
||||
square: '1 / 1',
|
||||
video: '16 / 9'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `@nuxtjs/color-mode`
|
||||
|
||||
This module is installed to provide dark mode support out of the box thanks to the Tailwind CSS dark mode `class` strategy.
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
You can read more about this in the [Theming](/getting-started/theming#dark-mode) section.
|
||||
::
|
||||
|
||||
### `nuxt-icon`
|
||||
|
||||
This module is installed when using the `dynamic` prop on the `Icon` component or globally through the `ui.icons.dynamic` option in your `app.config.ts`.
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
You can read more about this in the [Theming](/getting-started/theming#dynamic-icons) section and on the [Icon](/elements/icon) component page.
|
||||
::
|
||||
|
||||
## TypeScript
|
||||
|
||||
This module is written in TypeScript and provides typings for all the components and composables. You can look at the [source code](https://github.com/nuxt/ui/tree/dev/src/runtime/types) to see all the available types.
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb" to="https://nuxt.com/docs/guide/concepts/typescript" target="_blank"}
|
||||
You can read more about TypeScript on the official Nuxt documentation.
|
||||
::
|
||||
|
||||
You can use those types in your own components by importing them from `#ui/types`, for example when defining wrapper components:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<UBreadcrumb :links="links">
|
||||
<template #icon="{ link }">
|
||||
<UIcon :name="link.icon" />
|
||||
</template>
|
||||
</UBreadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { BreadcrumbLink } from '#ui/types'
|
||||
|
||||
export interface Props {
|
||||
links: BreadcrumbLink[]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
```
|
||||
|
||||
You don't have to use TypeScript yourself, but doing so will give you access to prop validation and autocomplete.
|
||||
|
||||
We've managed to provide dynamic typings on props such as `color`, `size`, `variant`, etc. based on your custom config. For example, you'll be suggested the `custom` color and the `subtle` variant when using the `Button` component with an `app.config.ts` as such:
|
||||
|
||||
```ts [app.config.ts]
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
button: {
|
||||
color: {
|
||||
custom: {
|
||||
subtle: '...'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
You can read more about components configuration in the [Theming](/getting-started/theming#appconfigts) section.
|
||||
::
|
||||
|
||||
## IntelliSense
|
||||
@@ -142,18 +252,10 @@ To use the latest updates pushed on the [`dev`](https://github.com/nuxt/ui/tree/
|
||||
|
||||
Update your `package.json` to the following:
|
||||
|
||||
```json [package.json]
|
||||
{
|
||||
"devDependencies": {
|
||||
"@nuxt/ui": "npm:@nuxt/ui-edge@latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```diff [package.json]
|
||||
{
|
||||
"devDependencies": {
|
||||
- "@nuxt/ui": "^2.9.0"
|
||||
- "@nuxt/ui": "^2.11.0"
|
||||
+ "@nuxt/ui": "npm:@nuxt/ui-edge@latest"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,12 +281,14 @@ export default defineNuxtConfig({
|
||||
Search the icon you want to use on https://icones.js.org built by [@antfu](https://github.com/antfu).
|
||||
::
|
||||
|
||||
Unlike the official [nuxt-icon](https://github.com/nuxt-modules/icon/) module, this module will not fetch any icon from the web and will only bundle the icons you use in your app thanks to [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons).
|
||||
|
||||
However, you will need to install either `@iconify/json` (full icon collections, 50MB) or the individual icon packages you want to use in your app.
|
||||
Thanks to [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons) plugin, only the icons you use in your app will be bundled in your CSS. However, you need to install the icon collections you specified in the `ui.icons` key:
|
||||
|
||||
::code-group
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm i @iconify-json/{collection_name}
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add @iconify-json/{collection_name}
|
||||
```
|
||||
@@ -295,25 +297,21 @@ yarn add @iconify-json/{collection_name}
|
||||
npm install @iconify-json/{collection_name}
|
||||
```
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm i @iconify-json/{collection_name}
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
When using `@iconify/json`, you can specifiy `icons: 'all'` in your `nuxt.config.ts` to use any icon in your app.
|
||||
If you choose to use the full `@iconify/json` icon collection (50MB), you can specifiy `icons: 'all'` or `icons: {}` in your `nuxt.config.ts` to use any icon in your app.
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
ui: {
|
||||
icons: 'all'
|
||||
icons: {}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Custom config :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
### Custom config
|
||||
|
||||
If you have specific needs, like using a custom icon collection, you can use the `icons` option in your `nuxt.config.ts` as an object to override the config of the [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
|
||||
If you have specific needs, like using a custom icon collection, you can use the `icons` option in your `nuxt.config.ts` as an object to override the config of the [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
import { getIconCollections } from '@egoist/tailwindcss-icons'
|
||||
@@ -345,7 +343,13 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
### Dynamic icons
|
||||
|
||||
The `Icon` component also has a `dynamic` prop to use the [nuxt-icon](https://github.com/nuxt-modules/icon/) module instead of the [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
|
||||
|
||||
Read more about this in the [Icon](/elements/icon#dynamic) component page.
|
||||
|
||||
### Defaults
|
||||
|
||||
You can easily replace all the default icons of the components in your `app.config.ts`.
|
||||
|
||||
|
||||
@@ -42,6 +42,12 @@ props:
|
||||
variant: 'soft'
|
||||
size: 'sm'
|
||||
options:
|
||||
- name: color
|
||||
restriction: included
|
||||
values:
|
||||
- gray
|
||||
- white
|
||||
- black
|
||||
- name: variant
|
||||
restriction: included
|
||||
values:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
---
|
||||
description: Display a chip indicator on any component.
|
||||
navigation:
|
||||
badge: New
|
||||
links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
|
||||
@@ -14,12 +14,14 @@ links:
|
||||
Pass an array of arrays to the `items` prop of the Dropdown component. Each array represents a group of items. Each item can have the following properties:
|
||||
|
||||
- `label` - The label of the item.
|
||||
- `labelClass` - The class of the item label. :u-badge{label="New" class="!rounded-full" variant="subtle"}
|
||||
- `icon` - The icon of the item.
|
||||
- `iconClass` - The class of the icon of the item.
|
||||
- `iconClass` - The class of the item icon.
|
||||
- `avatar` - The avatar of the item. You can pass all the props of the [Avatar](/elements/avatar) component.
|
||||
- `shortcuts` - The shortcuts of the item.
|
||||
- `slot` - The slot of the item.
|
||||
- `disabled` - Whether the item is disabled.
|
||||
- `class` - The class of the item.
|
||||
- `click` - The click handler of the item.
|
||||
|
||||
You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
|
||||
@@ -32,6 +34,12 @@ Use the `mode` prop to switch between `click` and `hover` modes.
|
||||
|
||||
:component-example{component="dropdown-example-mode"}
|
||||
|
||||
### Manual :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use a `v-model:open` to manually control the state. In this example, press :shortcut{value="O"} to toggle the dropdown.
|
||||
|
||||
:component-example{component="dropdown-example-open"}
|
||||
|
||||
## Popper
|
||||
|
||||
Use the `popper` prop to customize the popper instance.
|
||||
|
||||
@@ -21,7 +21,7 @@ props:
|
||||
You won't be able to use any icon in the `name` prop here as icons are bundled using [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons), read more about this in [Theming](/getting-started/theming#icons).
|
||||
::
|
||||
|
||||
### Dynamic :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
### Dynamic
|
||||
|
||||
You can use the `dynamic` prop to enable dynamic icon loading. This will use [`nuxt-icon`](https://github.com/nuxt-modules/icon) instead and allow you to use any icon from [Iconify](https://iconify.design/) as well as the `{collection_name}:{icon_name}` pattern.
|
||||
|
||||
|
||||
@@ -32,3 +32,7 @@ Link
|
||||
It also renders an `<a>` tag when a `to` prop is provided, otherwise it defaults to rendering a `<button>` tag. The default behavior can be customized using the `as` prop.
|
||||
|
||||
It is used underneath by the [Button](/elements/button), [Dropdown](/elements/dropdown) and [VerticalNavigation](/navigation/vertical-navigation) components.
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
@@ -148,6 +148,21 @@ excludedProps:
|
||||
---
|
||||
::
|
||||
|
||||
### Padded
|
||||
|
||||
Use the `padded` prop to remove the padding of the Input.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
padded: false
|
||||
baseProps:
|
||||
placeholder: 'Search...'
|
||||
variant: 'none'
|
||||
class: 'w-full'
|
||||
---
|
||||
::
|
||||
|
||||
## Slots
|
||||
|
||||
### `leading`
|
||||
@@ -157,13 +172,13 @@ Use the `#leading` slot to set the content of the leading icon.
|
||||
::component-card
|
||||
---
|
||||
slots:
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5" />
|
||||
baseProps:
|
||||
placeholder: 'Search...'
|
||||
---
|
||||
|
||||
#leading
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5"}
|
||||
::
|
||||
|
||||
### `trailing`
|
||||
|
||||
@@ -203,6 +203,9 @@ componentProps:
|
||||
## API
|
||||
|
||||
::field-group
|
||||
::field{name="submit ()" type="Promise<void>"}
|
||||
Triggers form submission.
|
||||
::
|
||||
::field{name="validate (path?: string, opts: { silent?: boolean })" type="Promise<T>"}
|
||||
Triggers form validation. Will raise any errors unless `opts.silent` is set to true.
|
||||
::
|
||||
@@ -217,5 +220,5 @@ componentProps:
|
||||
::
|
||||
::field{name="errors" type="Ref<FormError[]>"}
|
||||
A reference to the array containing validation errors. Use this to access or manipulate the error information.
|
||||
::
|
||||
::
|
||||
::
|
||||
|
||||
210
docs/content/3.forms/2.input-menu.md
Normal file
210
docs/content/3.forms/2.input-menu.md
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
title: InputMenu
|
||||
description: Display an autocomplete input with real-time suggestions.
|
||||
links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/InputMenu.vue
|
||||
- label: 'Combobox'
|
||||
icon: i-simple-icons-headlessui
|
||||
to: 'https://headlessui.com/vue/combobox'
|
||||
navigation:
|
||||
badge: New
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
The `InputMenu` component renders by default an [Input](/forms/input) component and is based on the `ui.input` preset. You can use most of the `Input` props to configure the display such as [color](/forms/input#style), [variant](/forms/input#style), [size](/forms/input#size), [placeholder](/forms/input#placeholder), [icon](/forms/input#icon), [disabled](/forms/input#disabled), etc.
|
||||
|
||||
You can use the `ui` prop like the `Input` component to override the default config. The `uiMenu` prop can be used to override the default menu config.
|
||||
|
||||
Pass an array of strings or objects to the `options` prop to display in the menu.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-basic'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-exclamation-triangle"}
|
||||
This component does not support multiple values. Use the [SelectMenu](/forms/select-menu#multiple) component instead.
|
||||
::
|
||||
|
||||
### Objects
|
||||
|
||||
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `option-attribute` prop that defaults to `label`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-objects'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
Use the `search-attributes` prop with an array of property names to search on each option object. Nested attributes can be accessed using `dot.notation`. When the property value is an array or object, these are cast to string so these can be searched within.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-search-attributes'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
If you only want to select a single object property rather than the whole object as value, you can set the `value-attribute` property. This prop defaults to `null`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-objects-value-attribute'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### Icon
|
||||
|
||||
The `InputMenu` has a button on the right to toggle the menu. Use the `trailing-icon` prop to set a different icon or change it globally in `ui.inputMenu.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'w-full lg:w-48'
|
||||
placeholder: 'Select a person'
|
||||
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
props:
|
||||
trailingIcon: 'i-heroicons-chevron-up-down-20-solid'
|
||||
excludedProps:
|
||||
- trailingIcon
|
||||
---
|
||||
::
|
||||
|
||||
Use the `selected-icon` prop to set a different icon or change it globally in `ui.inputMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'w-full lg:w-48'
|
||||
placeholder: 'Select a person'
|
||||
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
props:
|
||||
selectedIcon: 'i-heroicons-hand-thumb-up-solid'
|
||||
excludedProps:
|
||||
- selectedIcon
|
||||
---
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
Learn how to customize icons from the [Input](/forms/input#icon) component.
|
||||
::
|
||||
|
||||
## Searchable
|
||||
|
||||
### Attributes
|
||||
|
||||
Use the `search-attributes` prop with an array of property names to search on each option object. Nested attributes can be accessed using `dot.notation`. When the property value is an array or object, these are cast to string so these can be searched within.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-search-attributes'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### Control the query
|
||||
|
||||
Use a `v-model:query` to control the search query.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-search-query'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### Async search
|
||||
|
||||
Pass a function to the `search` prop to customize the search behavior and filter options according to your needs. The function will receive the query as its first argument and should return an array.
|
||||
|
||||
Use the `debounce` prop to adjust the delay of the function.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-search-async'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
## Popper
|
||||
|
||||
Use the `popper` prop to customize the popper instance.
|
||||
|
||||
### Arrow
|
||||
|
||||
:component-example{component="input-menu-example-popper-arrow"}
|
||||
|
||||
### Placement
|
||||
|
||||
:component-example{component="input-menu-example-popper-placement"}
|
||||
|
||||
### Offset
|
||||
|
||||
:component-example{component="input-menu-example-popper-offset"}
|
||||
|
||||
## Slots
|
||||
|
||||
### `option`
|
||||
|
||||
Use the `#option` slot to customize the option content. You will have access to the `option`, `active` and `selected` properties in the slot scope.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-option-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### `option-empty`
|
||||
|
||||
Use the `#option-empty` slot to customize the content displayed when the `searchable` prop is `true` and there is no options. You will have access to the `query` property in the slot scope.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-option-empty-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### `empty`
|
||||
|
||||
Use the `#empty` slot to customize the content displayed when there is no options. Defaults to `No options.`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-empty-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Config
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
Use the `ui` prop to override the input config and the `uiMenu` prop to override the menu config.
|
||||
::
|
||||
|
||||
::tabs{:selectedIndex="1"}
|
||||
:component-preset{label="Input (ui)" slug="Input"}
|
||||
:component-preset{label="InputMenu (uiMenu)"}
|
||||
::
|
||||
@@ -132,6 +132,21 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Padded
|
||||
|
||||
Use the `padded` prop to remove the padding of the Textarea.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
padded: false
|
||||
baseProps:
|
||||
placeholder: 'Search...'
|
||||
variant: 'none'
|
||||
class: 'w-full'
|
||||
---
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
@@ -175,6 +175,25 @@ excludedProps:
|
||||
---
|
||||
::
|
||||
|
||||
### Padded
|
||||
|
||||
Use the `padded` prop to remove the padding of the Select.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
padded: false
|
||||
baseProps:
|
||||
placeholder: 'Search...'
|
||||
options:
|
||||
- 'United States'
|
||||
- 'Canada'
|
||||
- 'Mexico'
|
||||
variant: 'none'
|
||||
class: 'w-full'
|
||||
---
|
||||
::
|
||||
|
||||
## Slots
|
||||
|
||||
### `leading`
|
||||
@@ -184,7 +203,7 @@ Use the `#leading` slot to set the content of the leading icon.
|
||||
::component-card
|
||||
---
|
||||
slots:
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5" />
|
||||
baseProps:
|
||||
options:
|
||||
- 'United States'
|
||||
@@ -194,7 +213,7 @@ baseProps:
|
||||
---
|
||||
|
||||
#leading
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5"}
|
||||
::
|
||||
|
||||
### `trailing`
|
||||
|
||||
@@ -14,13 +14,15 @@ links:
|
||||
|
||||
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.
|
||||
|
||||
You can use the `ui` prop like the `Select` component to override the default config. The `uiMenu` prop can be used to override the default menu config.
|
||||
|
||||
Like the `Select` component, you can use the `options` prop to pass an array of strings or objects.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-basic'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -32,7 +34,7 @@ You can use the `multiple` prop to select multiple values.
|
||||
---
|
||||
component: 'select-menu-example-multiple'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -44,7 +46,7 @@ You can pass an array of objects to `options` and either compare on the whole ob
|
||||
---
|
||||
component: 'select-menu-example-objects'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -54,7 +56,7 @@ If you only want to select a single object property rather than the whole object
|
||||
---
|
||||
component: 'select-menu-example-objects-value-attribute'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -65,7 +67,7 @@ Use the `selected-icon` prop to set a different icon or change it globally in `u
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
placeholder: 'Select a person'
|
||||
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
props:
|
||||
@@ -79,7 +81,7 @@ excludedProps:
|
||||
Learn how to customize icons from the [Select](/forms/select#icon) component.
|
||||
::
|
||||
|
||||
### Search
|
||||
## Searchable
|
||||
|
||||
Use the `searchable` prop to enable search.
|
||||
|
||||
@@ -90,7 +92,7 @@ This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) compon
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
placeholder: 'Select a person'
|
||||
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||
props:
|
||||
@@ -99,28 +101,28 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
#### Search Attributes
|
||||
### Attributes
|
||||
|
||||
Use the `search-attributes` with an array of property names to search on each option object.
|
||||
|
||||
Nested attributes can be accessed using `dot.notation`. When the property value is an array or object, these are cast to string so these can be searched within.
|
||||
Use the `search-attributes` prop with an array of property names to search on each option object. Nested attributes can be accessed using `dot.notation`. When the property value is an array or object, these are cast to string so these can be searched within.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-search-attributes'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-96'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
#### Clear on close :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
### Clear on close
|
||||
|
||||
By default, the search query will be kept after the menu is closed. To clear it on close, set the `clear-search-on-close` prop.
|
||||
|
||||
You can also configure this globally through the `ui.selectMenu.default.clearSearchOnClose` config. Defaults to `false`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
placeholder: 'Select a person'
|
||||
searchable: true
|
||||
searchablePlaceholder: 'Search a person...'
|
||||
@@ -130,6 +132,18 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Control the query :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use a `v-model:query` to control the search query.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-search-query'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### Async search
|
||||
|
||||
Pass a function to the `searchable` prop to customize the search behavior and filter options according to your needs. The function will receive the query as its first argument and should return an array.
|
||||
@@ -138,13 +152,13 @@ Use the `debounce` prop to adjust the delay of the function.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-async-search'
|
||||
component: 'select-menu-example-search-async'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
### Create option
|
||||
## Creatable
|
||||
|
||||
Use the `creatable` prop to enable the creation of new options when the search doesn't return any results (only works with `searchable`).
|
||||
|
||||
@@ -154,7 +168,21 @@ Try to search for something that doesn't exist in the example below.
|
||||
---
|
||||
component: 'select-menu-example-creatable'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
However, if you want to create options despite search query (apart from exact match), you can set the `show-create-option-when` prop to `'always'`.
|
||||
|
||||
You can also configure this globally through the `ui.selectMenu.default.showCreateOptionWhen` config. Defaults to `empty`.
|
||||
|
||||
Try to search for something that exists in the example below, but not an exact match.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-creatable-always'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -184,7 +212,7 @@ You can override the `#label` slot and handle the display yourself.
|
||||
---
|
||||
component: 'select-menu-example-multiple-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -196,7 +224,7 @@ You can also override the `#default` slot entirely.
|
||||
---
|
||||
component: 'select-menu-example-button'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -208,7 +236,7 @@ Use the `#option` slot to customize the option content. You will have access to
|
||||
---
|
||||
component: 'select-menu-example-option-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -220,7 +248,7 @@ Use the `#option-empty` slot to customize the content displayed when the `search
|
||||
---
|
||||
component: 'select-menu-example-option-empty-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-40'
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -232,10 +260,29 @@ Use the `#option-create` slot to customize the content displayed when the `creat
|
||||
An example is available in the [Create option](#create-option) section.
|
||||
::
|
||||
|
||||
### `empty` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `#empty` slot to customize the content displayed when there is no options. Defaults to `No options.`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-empty-slot'
|
||||
componentProps:
|
||||
class: 'w-full lg:w-48'
|
||||
---
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
## Config
|
||||
|
||||
:component-preset
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
Use the `ui` prop to override the select config and the `uiMenu` prop to override the menu config.
|
||||
::
|
||||
|
||||
::tabs{:selectedIndex="1"}
|
||||
:component-preset{label="Select (ui)" slug="Select"}
|
||||
:component-preset{label="SelectMenu (uiMenu)"}
|
||||
::
|
||||
|
||||
@@ -60,7 +60,7 @@ Use the `disabled` prop to disable the RadioGroup.
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
options: [{ value: 'email', label: 'Email' }, { value: 'sms', label: 'Phone (SMS)' }, { value: 'push', label: 'Push notification' }]
|
||||
options: [{ value: 'email', label: 'Email' }, { value: 'sms', label: 'Phone (SMS)' }, { value: 'push', label: 'Push notification', disabled: true }]
|
||||
modelValue: 'sms'
|
||||
props:
|
||||
disabled: true
|
||||
@@ -68,7 +68,7 @@ props:
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
This prop also work on the Radio component.
|
||||
This prop also work on the Radio component and you can set the `disabled` field in the `options` to disable a specific Radio.
|
||||
::
|
||||
|
||||
### Label
|
||||
|
||||
@@ -26,7 +26,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Size :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
### Size
|
||||
|
||||
Use the `size` prop to change the size of the Toggle.
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ code: >-
|
||||
This will only work with form elements that support the `size` prop.
|
||||
::
|
||||
|
||||
### Eager Validation :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
### Eager Validation
|
||||
|
||||
By default, validation is only triggered after the initial `blur` event. This is to prevent the form from being validated as the user is typing. You can override this behavior by setting the `eager-validation` prop to `true`
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ links:
|
||||
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/data/Table.vue
|
||||
---
|
||||
|
||||
::callout{icon="i-heroicons-puzzle-piece" to="/getting-started/examples#table"}
|
||||
Check out an example of a Table with advanced features like sorting, pagination, search, etc.
|
||||
::
|
||||
|
||||
## Usage
|
||||
|
||||
Use the `rows` prop to set the data to display in the table. By default, the table will display all the fields of the rows.
|
||||
@@ -28,6 +32,7 @@ Use the `columns` prop to configure which columns to display. It's an array of o
|
||||
- `sortable` - Whether the column is sortable. Defaults to `false`.
|
||||
- `direction` - The sort direction to use on first click. Defaults to `asc`.
|
||||
- `class` - The class to apply to the column cells.
|
||||
- `sort` - Pass your own `sort` function. Defaults to a simple _greater than_ / _less than_ comparison.
|
||||
|
||||
::component-example{class="grid"}
|
||||
---
|
||||
@@ -53,6 +58,8 @@ componentProps:
|
||||
|
||||
You can make the columns sortable by setting the `sortable` property to `true` in the column configuration.
|
||||
|
||||
You may specify the default direction of each column through the `direction` property. It can be either `asc` or `desc`, but it will default to `asc`.
|
||||
|
||||
::component-example{class="grid"}
|
||||
---
|
||||
padding: false
|
||||
@@ -62,17 +69,84 @@ componentProps:
|
||||
---
|
||||
::
|
||||
|
||||
You may specify the default direction of each column through the `direction` property. It can be either `asc` or `desc`, but it will default to `asc`.
|
||||
#### Default sorting
|
||||
|
||||
You can specify a default sort for the table through the `sort` prop. It's an object with the following properties:
|
||||
|
||||
- `column` - The column to sort by.
|
||||
- `direction` - The sort direction. Can be either `asc` or `desc` and defaults to `asc`.
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
This will set the default sort and will work even if no column is set as `sortable`.
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const sort = ref({
|
||||
column: 'name',
|
||||
direction: 'desc'
|
||||
})
|
||||
|
||||
const columns = [{
|
||||
label: 'Name',
|
||||
key: 'name',
|
||||
sortable: true
|
||||
}]
|
||||
|
||||
const people = [{
|
||||
id: 1,
|
||||
name: 'Lindsay Walton',
|
||||
title: 'Front-end Developer',
|
||||
email: 'lindsay.walton@example.com',
|
||||
role: 'Member'
|
||||
}, {
|
||||
id: 2,
|
||||
name: 'Courtney Henry',
|
||||
title: 'Designer',
|
||||
email: 'courtney.henry@example.com',
|
||||
role: 'Admin'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :sort="sort" :columns="columns" :rows="people" />
|
||||
</template>
|
||||
```
|
||||
|
||||
#### Reactive sorting
|
||||
|
||||
You can use a `v-model:sort` to make the sorting reactive. You may also use `@update:sort` to call your own function with the sorting data.
|
||||
|
||||
When fetching data from an API, we can take advantage of the [`useFetch`](https://nuxt.com/docs/api/composables/use-fetch) or [`useAsyncData`](https://nuxt.com/docs/api/composables/use-async-data) composables to fetch the data based on the sorting column and direction every time the `sort` reactive element changes.
|
||||
|
||||
When doing so, you might want to set the `sort-mode` prop to `manual` to disable the automatic sorting and return the rows as is. :u-badge{label="New" class="!rounded-full" variant="subtle"}
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// Ensure it uses `ref` instead of `reactive`.
|
||||
const sort = ref({
|
||||
column: 'name',
|
||||
direction: 'desc'
|
||||
})
|
||||
|
||||
const columns = [{
|
||||
label: 'Name',
|
||||
key: 'name',
|
||||
sortable: true
|
||||
}]
|
||||
|
||||
const { data, pending } = await useLazyFetch(() => `/api/users?orderBy=${sort.value.column}&order=${sort.value.direction}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable v-model:sort="sort" :loading="pending" :columns="columns" :rows="data" sort-mode="manual" />
|
||||
</template>
|
||||
```
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb" to="https://nuxt.com/docs/api/composables/use-fetch#params" target="_blank"}
|
||||
We pass a function to `useLazyFetch` here make the url reactive but you can use the `query` / `params` options alongside `watch`.
|
||||
::
|
||||
|
||||
#### Custom sorting
|
||||
|
||||
Use the `sort-button` prop to customize the sort button in the header. You can pass all the props of the [Button](/elements/button) component to customize it through this prop or globally through `ui.table.default.sortButton`. Its icon defaults to `i-heroicons-arrows-up-down-20-solid`.
|
||||
|
||||
::component-card{class="grid"}
|
||||
@@ -151,43 +225,6 @@ Use the `sort-desc-icon` prop to set a different icon or change it globally in `
|
||||
You can also customize the entire header cell, read more in the [Slots](#slots) section.
|
||||
::
|
||||
|
||||
#### Reactive sorting
|
||||
|
||||
Sometimes you will want to fetch new data depending on the sorted column and direction. You can use the `v-model:sort` to automatically update the `ref` reactive element every time the sorting changes on the Table. You may also use `@update:sort` to call your own function with the sorting data.
|
||||
|
||||
For example, we can take advantage of `useLazyRefresh` computed URL to automatically fetch the data depending on the sorting column and direction every time the `sort` reactive element changes.
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// Ensure it uses `ref` instead of `reactive`.
|
||||
const sort = ref({
|
||||
column: 'name',
|
||||
direction: 'desc'
|
||||
})
|
||||
|
||||
const columns = [...]
|
||||
|
||||
const { data, pending } = useLazyFetch(() => {
|
||||
return `/api/users?orderBy=${sort.value.column}&order=${sort.value.direction}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable v-model:sort="sort" :loading="pending" :columns="columns" :rows="data" />
|
||||
</template>
|
||||
```
|
||||
|
||||
The initial value of `sort` will be respected as the initial sort column and direction, as well as each column default sorting direction.
|
||||
|
||||
::component-example{class="grid"}
|
||||
---
|
||||
padding: false
|
||||
component: 'table-example-reactive-sorting'
|
||||
componentProps:
|
||||
class: 'flex-1'
|
||||
---
|
||||
::
|
||||
|
||||
### Selectable
|
||||
|
||||
Use a `v-model` to make the table selectable. The `v-model` will be an array of the selected rows.
|
||||
|
||||
@@ -12,8 +12,9 @@ links:
|
||||
Pass an array to the `links` prop of the VerticalNavigation component. Each link can have the following properties:
|
||||
|
||||
- `label` - The label of the link.
|
||||
- `labelClass` - The class of the link label. :u-badge{label="New" class="!rounded-full" variant="subtle"}
|
||||
- `icon` - The icon of the link.
|
||||
- `iconClass` - The class of the icon link.
|
||||
- `iconClass` - The class of the link icon.
|
||||
- `avatar` - The avatar of the link. You can pass all the props of the [Avatar](/elements/avatar) component.
|
||||
- `badge` - A badge to display next to the label.
|
||||
- `click` - The click handler of the link.
|
||||
@@ -26,6 +27,12 @@ You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/com
|
||||
Learn how to build a Tailwind like vertical navigation in the [Examples](/getting-started/examples#verticalnavigation) page.
|
||||
::
|
||||
|
||||
## Sections
|
||||
|
||||
Group your navigation links into distinct sections, separated by a divider. You can do this by passing an array of arrays to the `links` prop of the VerticalNavigation component.
|
||||
|
||||
:component-example{component="vertical-navigation-example-sections"}
|
||||
|
||||
## Slots
|
||||
|
||||
You can use slots to customize links display.
|
||||
|
||||
@@ -46,6 +46,22 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Disabled :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `disabled` prop to disable all the buttons.
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
modelValue: 1
|
||||
total: 100
|
||||
showLast: true
|
||||
showFirst: true
|
||||
props:
|
||||
disabled: true
|
||||
---
|
||||
::
|
||||
|
||||
### Active / Inactive
|
||||
|
||||
Use the `active-button` and `inactive-button` props to customize the active and inactive buttons of the Pagination.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
title: Breadcrumb
|
||||
description: A list of links that indicate the current page's location within a navigational hierarchy.
|
||||
navigation:
|
||||
badge: New
|
||||
---
|
||||
|
||||
## Usage
|
||||
@@ -10,8 +8,9 @@ navigation:
|
||||
Pass an array to the `links` prop of the Breadcrumb component. Each link can have the following properties:
|
||||
|
||||
- `label` - The label of the link.
|
||||
- `labelClass` - The class of the link label. :u-badge{label="New" class="!rounded-full" variant="subtle"}
|
||||
- `icon` - The icon of the link.
|
||||
- `iconClass` - The class of the icon link.
|
||||
- `iconClass` - The class of the link icon.
|
||||
|
||||
You can also pass any property from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
|
||||
|
||||
|
||||
@@ -21,18 +21,14 @@ Use the `mode` prop to switch between `click` and `hover` modes.
|
||||
|
||||
### Manual
|
||||
|
||||
Use the `open` prop to manually control showing the panel.
|
||||
Use a `v-model:open` to manually control the state. In this example, press :shortcut{value="O"} to toggle the popover.
|
||||
|
||||
:component-example{component="popover-example-open"}
|
||||
|
||||
### Overlay :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
### Overlay
|
||||
|
||||
:component-example{component="popover-example-overlay"}
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
Clicking on the `overlay` emits `update:open`. If you are manually controlling the `open` prop, you will need to use a [`v-model` argument](https://vuejs.org/guide/components/v-model.html#v-model-arguments) (`v-model:open`).
|
||||
::
|
||||
|
||||
## Popper
|
||||
|
||||
Use the `popper` prop to customize the popper instance.
|
||||
|
||||
@@ -133,7 +133,7 @@ excludedProps:
|
||||
|
||||
### Timeout
|
||||
|
||||
Use the `timeout` prop to configure how long the Notification will remain. Set it to `0` to disable the timeout.
|
||||
Use the `timeout` prop to configure how long the Notification will remain. The default value is `5000`, set it to `0` to disable the timeout.
|
||||
|
||||
You will see a progress bar at the bottom of the Notification which will indicate the remaining time. When hovering the Notification, the progress bar will be paused.
|
||||
|
||||
|
||||
@@ -67,8 +67,8 @@ sections:
|
||||
color: white
|
||||
size: lg
|
||||
trailingIcon: i-heroicons-arrow-right-20-solid
|
||||
- title: 'A collection of <span class="text-primary">30+</span> components'
|
||||
description: 'Get access to 30+ beautifully designed and fully customizable components built for Nuxt. These components<br class="hidden lg:block"> are updated regularly to ensure that you always have the latest features and functionalities.'
|
||||
- title: 'A collection of <span class="text-primary">40+</span> components'
|
||||
description: 'Get access to 40+ beautifully designed and fully customizable components built for Nuxt. These components<br class="hidden lg:block"> are updated regularly to ensure that you always have the latest features and functionalities.'
|
||||
class: 'dark:bg-gradient-to-b from-gray-950/50 to-gray-900'
|
||||
slot: categories
|
||||
links:
|
||||
@@ -82,12 +82,12 @@ sections:
|
||||
to: /elements/dropdown
|
||||
image:
|
||||
path: /illustrations/elements
|
||||
badge: 9
|
||||
badge: 15
|
||||
- label: Forms
|
||||
to: /forms/form
|
||||
image:
|
||||
path: /illustrations/forms
|
||||
badge: 10
|
||||
badge: 12
|
||||
- label: Data
|
||||
to: /data/table
|
||||
image:
|
||||
@@ -97,17 +97,17 @@ sections:
|
||||
to: /navigation/command-palette
|
||||
image:
|
||||
path: /illustrations/navigation
|
||||
badge: 4
|
||||
badge: 5
|
||||
- label: Overlays
|
||||
to: /overlays/modal
|
||||
image:
|
||||
path: /illustrations/overlays
|
||||
badge: 6
|
||||
badge: 7
|
||||
- label: Layout
|
||||
to: /layout/card
|
||||
image:
|
||||
path: /illustrations/layout
|
||||
badge: 3
|
||||
badge: 4
|
||||
cta:
|
||||
title: Trusted and supported by our<br class="hidden lg:block"> amazing community
|
||||
pro:
|
||||
|
||||
@@ -49,7 +49,7 @@ const navigation = computed(() => {
|
||||
]
|
||||
}
|
||||
|
||||
return nav.value.filter(item => item._path !== '/dev')
|
||||
return nav.value?.filter(item => item._path !== '/dev') || []
|
||||
})
|
||||
|
||||
const links = computed(() => {
|
||||
|
||||
@@ -17,8 +17,8 @@ export default defineNuxtConfig({
|
||||
].filter(Boolean),
|
||||
modules: [
|
||||
'@nuxt/content',
|
||||
'@nuxt/image',
|
||||
'nuxt-og-image',
|
||||
// '@nuxt/devtools',
|
||||
// '@nuxthq/studio',
|
||||
module,
|
||||
'@nuxtjs/fontaine',
|
||||
@@ -86,7 +86,8 @@ export default defineNuxtConfig({
|
||||
'/api/search.json',
|
||||
'/api/releases.json',
|
||||
'/api/pulls.json'
|
||||
]
|
||||
],
|
||||
ignore: !process.env.NUXT_UI_PRO_PATH && !process.env.NUXT_GITHUB_TOKEN ? ['/pro'] : []
|
||||
}
|
||||
},
|
||||
componentMeta: {
|
||||
@@ -102,6 +103,7 @@ export default defineNuxtConfig({
|
||||
process.env.NUXT_UI_PRO_PATH ? resolve(process.env.NUXT_UI_PRO_PATH, '.docs', 'components') : '.c12'
|
||||
],
|
||||
metaFields: {
|
||||
type: false,
|
||||
props: true,
|
||||
slots: true,
|
||||
events: false,
|
||||
@@ -123,5 +125,13 @@ export default defineNuxtConfig({
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
typescript: {
|
||||
strict: false
|
||||
},
|
||||
vite: {
|
||||
optimizeDeps: {
|
||||
include: ['date-fns']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
"@nuxt/ui": "workspace:latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/heroicons": "^1.1.13",
|
||||
"@iconify-json/simple-icons": "^1.1.79",
|
||||
"@nuxt/content": "^2.9.0",
|
||||
"@nuxt/devtools": "^1.0.3",
|
||||
"@iconify-json/heroicons": "^1.1.19",
|
||||
"@iconify-json/simple-icons": "^1.1.88",
|
||||
"@nuxt/content": "^2.10.0",
|
||||
"@nuxt/devtools": "^1.0.8",
|
||||
"@nuxt/eslint-config": "^0.2.0",
|
||||
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@0.4.2-28344234.90e73a4",
|
||||
"@nuxthq/studio": "^1.0.5",
|
||||
"@nuxt/image": "^1.3.0",
|
||||
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@0.7.0-28425529.a466815",
|
||||
"@nuxthq/studio": "^1.0.8",
|
||||
"@nuxtjs/fontaine": "^0.4.1",
|
||||
"@nuxtjs/google-fonts": "^3.0.2",
|
||||
"@nuxtjs/mdc": "^0.2.8",
|
||||
"@nuxtjs/plausible": "^0.2.3",
|
||||
"@nuxtjs/google-fonts": "^3.1.3",
|
||||
"@nuxtjs/plausible": "^0.2.4",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@vueuse/nuxt": "^10.6.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"eslint": "^8.54.0",
|
||||
"joi": "^17.11.0",
|
||||
"nuxt": "^3.8.2",
|
||||
"@vueuse/nuxt": "^10.7.2",
|
||||
"date-fns": "^3.2.0",
|
||||
"eslint": "^8.56.0",
|
||||
"joi": "^17.11.1",
|
||||
"nuxt": "^3.9.3",
|
||||
"nuxt-cloudflare-analytics": "^1.0.8",
|
||||
"nuxt-component-meta": "npm:nuxt-component-meta-edge@0.5.5-28315603.0a285c7",
|
||||
"nuxt-component-meta": "^0.6.2",
|
||||
"nuxt-og-image": "^2.2.4",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.2",
|
||||
"prettier": "^3.2.4",
|
||||
"typescript": "^5.3.3",
|
||||
"ufo": "^1.3.2",
|
||||
"v-calendar": "^3.1.2",
|
||||
"valibot": "^0.21.0",
|
||||
"yup": "^1.3.2",
|
||||
"valibot": "^0.25.0",
|
||||
"yup": "^1.3.3",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
<UDivider v-if="page.body?.toc?.links?.length" type="dashed" />
|
||||
|
||||
<UPageLinks title="Community" :links="links" />
|
||||
|
||||
<UDivider type="dashed" />
|
||||
|
||||
<div class="space-y-3">
|
||||
<AdsPro />
|
||||
<AdsCarbon />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UDocsToc>
|
||||
|
||||
@@ -41,6 +41,8 @@ const { data: pulls } = await useLazyFetch('/api/pulls.json', { default: () => [
|
||||
|
||||
const dates = computed(() => {
|
||||
const first = releases.value[releases.value.length - 1]
|
||||
if (!first) return []
|
||||
|
||||
const days = eachDayOfInterval({ start: new Date(first.published_at), end: new Date() })
|
||||
|
||||
return days.reverse().map(day => {
|
||||
|
||||
63
package.json
63
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nuxt/ui",
|
||||
"version": "2.11.0",
|
||||
"version": "2.12.1",
|
||||
"repository": "nuxt/ui",
|
||||
"homepage": "https://ui.nuxt.com",
|
||||
"license": "MIT",
|
||||
@@ -32,53 +32,54 @@
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@egoist/tailwindcss-icons": "^1.4.0",
|
||||
"@egoist/tailwindcss-icons": "^1.7.2",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@headlessui/vue": "^1.7.16",
|
||||
"@iconify-json/heroicons": "^1.1.13",
|
||||
"@nuxt/kit": "^3.8.2",
|
||||
"@headlessui/vue": "1.7.16",
|
||||
"@iconify-json/heroicons": "^1.1.19",
|
||||
"@nuxt/kit": "^3.9.3",
|
||||
"@nuxtjs/color-mode": "^3.3.2",
|
||||
"@nuxtjs/tailwindcss": "^6.10.0",
|
||||
"@nuxtjs/tailwindcss": "^6.10.4",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@vueuse/core": "^10.6.1",
|
||||
"@vueuse/integrations": "^10.6.1",
|
||||
"@vueuse/math": "^10.6.1",
|
||||
"defu": "^6.1.3",
|
||||
"@vueuse/core": "^10.7.2",
|
||||
"@vueuse/integrations": "^10.7.2",
|
||||
"@vueuse/math": "^10.7.2",
|
||||
"defu": "^6.1.4",
|
||||
"fuse.js": "^6.6.2",
|
||||
"nuxt-icon": "^0.6.6",
|
||||
"nuxt-icon": "^0.6.8",
|
||||
"ohash": "^1.1.3",
|
||||
"pathe": "^1.1.1",
|
||||
"scule": "^1.1.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.3.5"
|
||||
"pathe": "^1.1.2",
|
||||
"scule": "^1.2.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss": "^3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint-config": "^0.2.0",
|
||||
"@nuxt/module-builder": "^0.5.4",
|
||||
"@nuxt/module-builder": "^0.5.5",
|
||||
"@nuxt/test-utils": "^3.10.0",
|
||||
"@release-it/conventional-changelog": "^8.0.1",
|
||||
"@testing-library/vue": "^8.0.1",
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@vue/test-utils": "^2.4.3",
|
||||
"eslint": "^8.56.0",
|
||||
"happy-dom": "^12.10.3",
|
||||
"joi": "^17.11.0",
|
||||
"nuxt": "^3.8.2",
|
||||
"nuxt-vitest": "^0.11.5",
|
||||
"release-it": "^17.0.0",
|
||||
"typescript": "^5.3.2",
|
||||
"joi": "^17.11.1",
|
||||
"nuxt": "^3.9.3",
|
||||
"release-it": "^17.0.1",
|
||||
"typescript": "^5.3.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"valibot": "^0.21.0",
|
||||
"vitest": "^0.33.0",
|
||||
"vue-tsc": "^1.8.22",
|
||||
"yup": "^1.3.2",
|
||||
"valibot": "^0.25.0",
|
||||
"vitest": "^1.2.1",
|
||||
"vitest-environment-nuxt": "^1.0.0",
|
||||
"vue-tsc": "^1.8.27",
|
||||
"yup": "^1.3.3",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@nuxt/kit": "3.8.2",
|
||||
"@nuxt/schema": "3.8.2",
|
||||
"vue": "3.3.8"
|
||||
"@nuxt/kit": "3.9.3",
|
||||
"@nuxt/schema": "3.9.3",
|
||||
"tailwindcss": "3.4.1",
|
||||
"vue": "3.3.13"
|
||||
}
|
||||
}
|
||||
|
||||
6
playground/app.config.ts
Normal file
6
playground/app.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
primary: 'green',
|
||||
gray: 'cool'
|
||||
}
|
||||
})
|
||||
@@ -1,7 +1,5 @@
|
||||
import module from '../src/module'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
module
|
||||
'../src/module'
|
||||
]
|
||||
})
|
||||
|
||||
6
playground/tailwind.config.ts
Normal file
6
playground/tailwind.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
export default <Partial<Config>>{
|
||||
theme: {
|
||||
}
|
||||
}
|
||||
7237
pnpm-lock.yaml
generated
7237
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -243,7 +243,7 @@ export const generateSafelist = (colors: string[], globalColors) => {
|
||||
}
|
||||
|
||||
export const customSafelistExtractor = (prefix, content: string, colors: string[], safelistColors: string[]) => {
|
||||
const classes = []
|
||||
const classes: string[] = []
|
||||
const regex = /<([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z][A-Za-z0-9]*)*)\s+(?![^>]*:color\b)[^>]*\bcolor=["']([^"']+)["'][^>]*>/gs
|
||||
|
||||
const matches = content.matchAll(regex)
|
||||
|
||||
@@ -10,10 +10,15 @@ import type { DeepPartial, Strategy } from './runtime/types/utils'
|
||||
|
||||
const defaultExtractor = createDefaultExtractor({ tailwindConfig: { separator: ':' } })
|
||||
|
||||
// @ts-ignore
|
||||
delete defaultColors.lightBlue
|
||||
// @ts-ignore
|
||||
delete defaultColors.warmGray
|
||||
// @ts-ignore
|
||||
delete defaultColors.trueGray
|
||||
// @ts-ignore
|
||||
delete defaultColors.coolGray
|
||||
// @ts-ignore
|
||||
delete defaultColors.blueGray
|
||||
|
||||
type UI = {
|
||||
@@ -79,12 +84,15 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
|
||||
// @ts-ignore
|
||||
nuxt.hook('tailwindcss:config', function (tailwindConfig) {
|
||||
tailwindConfig.theme = tailwindConfig.theme || {}
|
||||
tailwindConfig.theme.extend = tailwindConfig.theme.extend || {}
|
||||
tailwindConfig.theme.extend.colors = tailwindConfig.theme.extend.colors || {}
|
||||
|
||||
const globalColors: any = {
|
||||
...(tailwindConfig.theme.colors || defaultColors),
|
||||
...tailwindConfig.theme.extend?.colors
|
||||
}
|
||||
|
||||
tailwindConfig.theme.extend.colors = tailwindConfig.theme.extend.colors || {}
|
||||
// @ts-ignore
|
||||
globalColors.primary = tailwindConfig.theme.extend.colors.primary = {
|
||||
50: 'rgb(var(--color-primary-50) / <alpha-value>)',
|
||||
@@ -132,10 +140,10 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
}
|
||||
|
||||
tailwindConfig.safelist = tailwindConfig.safelist || []
|
||||
tailwindConfig.safelist.push(...generateSafelist(options.safelistColors, colors))
|
||||
tailwindConfig.safelist.push(...generateSafelist(options.safelistColors || [], colors))
|
||||
|
||||
tailwindConfig.plugins = tailwindConfig.plugins || []
|
||||
tailwindConfig.plugins.push(iconsPlugin(Array.isArray(options.icons) || options.icons === 'all' ? { collections: getIconCollections(options.icons) } : typeof options.icons === 'object' ? options.icons as IconsPluginOptions : {}))
|
||||
tailwindConfig.plugins.push(iconsPlugin(Array.isArray(options.icons) ? { collections: getIconCollections(options.icons) } : typeof options.icons === 'object' ? options.icons as IconsPluginOptions : {}))
|
||||
})
|
||||
|
||||
createTemplates(nuxt)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
|
||||
<UButton
|
||||
v-if="column.sortable"
|
||||
v-bind="{ ...ui.default.sortButton, ...sortButton }"
|
||||
v-bind="{ ...(ui.default.sortButton || {}), ...sortButton }"
|
||||
:icon="(!sort.column || sort.column !== column.key) ? (sortButton.icon || ui.default.sortButton.icon) : sort.direction === 'asc' ? sortAscIcon : sortDescIcon"
|
||||
:label="column[columnAttribute]"
|
||||
@click="onSort(column)"
|
||||
@@ -67,15 +67,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, defineComponent, toRaw, toRef } from 'vue'
|
||||
import { computed, defineComponent, toRaw, toRef } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { upperFirst } from 'scule'
|
||||
import { defu } from 'defu'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import UButton from '../elements/Button.vue'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import UCheckbox from '../forms/Checkbox.vue'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
import { mergeConfig, omit, get } from '../../utils'
|
||||
import { mergeConfig, get } from '../../utils'
|
||||
import type { Strategy, Button } from '../../types'
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
@@ -87,6 +88,18 @@ function defaultComparator<T> (a: T, z: T): boolean {
|
||||
return a === z
|
||||
}
|
||||
|
||||
function defaultSort (a: any, b: any, direction: 'asc' | 'desc') {
|
||||
if (a === b) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (direction === 'asc') {
|
||||
return a < b ? -1 : 1
|
||||
} else {
|
||||
return a > b ? -1 : 1
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UButton,
|
||||
@@ -104,11 +117,11 @@ export default defineComponent({
|
||||
default: () => defaultComparator
|
||||
},
|
||||
rows: {
|
||||
type: Array as PropType<{ [key: string]: any, click?: Function }[]>,
|
||||
type: Array as PropType<{ [key: string]: any }[]>,
|
||||
default: () => []
|
||||
},
|
||||
columns: {
|
||||
type: Array as PropType<{ key: string, sortable?: boolean, direction?: 'asc' | 'desc', class?: string, [key: string]: any }[]>,
|
||||
type: Array as PropType<{ key: string, sortable?: boolean, sort?: (a: any, b: any, direction: 'asc' | 'desc') => number, direction?: 'asc' | 'desc', class?: string, [key: string]: any }[]>,
|
||||
default: null
|
||||
},
|
||||
columnAttribute: {
|
||||
@@ -119,6 +132,10 @@ export default defineComponent({
|
||||
type: Object as PropType<{ column: string, direction: 'asc' | 'desc' }>,
|
||||
default: () => ({})
|
||||
},
|
||||
sortMode: {
|
||||
type: String as PropType<'manual' | 'auto'>,
|
||||
default: 'auto'
|
||||
},
|
||||
sortButton: {
|
||||
type: Object as PropType<Button>,
|
||||
default: () => config.default.sortButton as Button
|
||||
@@ -145,25 +162,25 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'update:sort'],
|
||||
setup (props, { emit, attrs: $attrs }) {
|
||||
const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const columns = computed(() => props.columns ?? Object.keys(omit(props.rows[0] ?? {}, ['click'])).map((key) => ({ key, label: upperFirst(key), sortable: false })))
|
||||
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: upperFirst(key), sortable: false, class: undefined, sort: defaultSort })))
|
||||
|
||||
const sort = ref(defu({}, props.sort, { column: null, direction: 'asc' }))
|
||||
const sort = useVModel(props, 'sort', emit, { passive: true, defaultValue: defu({}, props.sort, { column: null, direction: 'asc' }) })
|
||||
|
||||
const defaultSort = { column: sort.value.column, direction: null }
|
||||
const savedSort = { column: sort.value.column, direction: null }
|
||||
|
||||
const rows = computed(() => {
|
||||
if (!sort.value?.column) {
|
||||
if (!sort.value?.column || props.sortMode === 'manual') {
|
||||
return props.rows
|
||||
}
|
||||
|
||||
@@ -173,15 +190,9 @@ export default defineComponent({
|
||||
const aValue = get(a, column)
|
||||
const bValue = get(b, column)
|
||||
|
||||
if (aValue === bValue) {
|
||||
return 0
|
||||
}
|
||||
const sort = columns.value.find((col) => col.key === column)?.sort ?? defaultSort
|
||||
|
||||
if (direction === 'asc') {
|
||||
return aValue < bValue ? -1 : 1
|
||||
} else {
|
||||
return aValue > bValue ? -1 : 1
|
||||
}
|
||||
return sort(aValue, bValue, direction)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -227,15 +238,13 @@ export default defineComponent({
|
||||
const direction = !column.direction || column.direction === 'asc' ? 'desc' : 'asc'
|
||||
|
||||
if (sort.value.direction === direction) {
|
||||
sort.value = defu({}, defaultSort, { column: null, direction: 'asc' })
|
||||
sort.value = defu({}, savedSort, { column: null, direction: 'asc' })
|
||||
} else {
|
||||
sort.value.direction = sort.value.direction === 'asc' ? 'desc' : 'asc'
|
||||
sort.value = { column: sort.value.column, direction: sort.value.direction === 'asc' ? 'desc' : 'asc' }
|
||||
}
|
||||
} else {
|
||||
sort.value = { column: column.key, direction: column.direction || 'asc' }
|
||||
}
|
||||
|
||||
emit('update:sort', sort.value)
|
||||
}
|
||||
|
||||
function onSelect (row) {
|
||||
@@ -267,7 +276,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
function getRowData (row: Object, rowKey: string | string[], defaultValue: any = 'Failed to get cell value') {
|
||||
function getRowData (row: Object, rowKey: string | string[], defaultValue: any = '') {
|
||||
return get(row, rowKey, defaultValue)
|
||||
}
|
||||
|
||||
|
||||
@@ -93,17 +93,17 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const { ui, attrs } = useUI('accordion', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const uiButton = computed<Partial<typeof configButton>>(() => configButton)
|
||||
const uiButton = computed<typeof configButton>(() => configButton)
|
||||
|
||||
const buttonRefs = ref<{ open: boolean, close: (e: EventTarget) => {} }[]>([])
|
||||
|
||||
@@ -114,12 +114,13 @@ export default defineComponent({
|
||||
|
||||
buttonRefs.value.forEach((button) => {
|
||||
if (button.open) {
|
||||
button.close(e.target)
|
||||
button.close(e.target as EventTarget)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onEnter (el: HTMLElement, done) {
|
||||
function onEnter (_el: Element, done: () => void) {
|
||||
const el = _el as HTMLElement
|
||||
el.style.height = '0'
|
||||
el.offsetHeight // Trigger a reflow, flushing the CSS changes
|
||||
el.style.height = el.scrollHeight + 'px'
|
||||
@@ -127,16 +128,19 @@ export default defineComponent({
|
||||
el.addEventListener('transitionend', done, { once: true })
|
||||
}
|
||||
|
||||
function onBeforeLeave (el: HTMLElement) {
|
||||
function onBeforeLeave (_el: Element) {
|
||||
const el = _el as HTMLElement
|
||||
el.style.height = el.scrollHeight + 'px'
|
||||
el.offsetHeight // Trigger a reflow, flushing the CSS changes
|
||||
}
|
||||
|
||||
function onAfterEnter (el: HTMLElement) {
|
||||
function onAfterEnter (_el: Element) {
|
||||
const el = _el as HTMLElement
|
||||
el.style.height = 'auto'
|
||||
}
|
||||
|
||||
function onLeave (el: HTMLElement, done) {
|
||||
function onLeave (_el: Element, done: () => void) {
|
||||
const el = _el as HTMLElement
|
||||
el.style.height = '0'
|
||||
|
||||
el.addEventListener('transitionend', done, { once: true })
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<UIcon v-if="icon" :name="icon" :class="ui.icon.base" />
|
||||
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
|
||||
|
||||
<div class="w-0 flex-1">
|
||||
<div :class="ui.inner">
|
||||
<p :class="ui.title">
|
||||
<slot name="title" :title="title">
|
||||
{{ title }}
|
||||
@@ -17,15 +17,15 @@
|
||||
</p>
|
||||
|
||||
<div v-if="(description || $slots.description) && actions.length" :class="ui.actions">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="action.click" />
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="closeButton || (!description && !$slots.description && actions.length)" :class="twMerge(ui.actions, 'mt-0')">
|
||||
<template v-if="!description && !$slots.description && actions.length">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="action.click" />
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
|
||||
</template>
|
||||
|
||||
<UButton v-if="closeButton" aria-label="Close" v-bind="{ ...ui.default.closeButton, ...closeButton }" @click.stop="$emit('close')" />
|
||||
<UButton v-if="closeButton" aria-label="Close" v-bind="{ ...(ui.default.closeButton || {}), ...closeButton }" @click.stop="$emit('close')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@ import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import UButton from '../elements/Button.vue'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
import type { Avatar, Button, AlertColor, AlertVariant, Strategy } from '../../types'
|
||||
import type { Avatar, Button, AlertColor, AlertVariant, AlertAction, Strategy } from '../../types'
|
||||
import { mergeConfig } from '../../utils'
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
@@ -73,10 +73,10 @@ export default defineComponent({
|
||||
},
|
||||
closeButton: {
|
||||
type: Object as PropType<Button>,
|
||||
default: () => config.default.closeButton as Button
|
||||
default: () => config.default.closeButton as unknown as Button
|
||||
},
|
||||
actions: {
|
||||
type: Array as PropType<(Button & { click?: Function })[]>,
|
||||
type: Array as PropType<AlertAction[]>,
|
||||
default: () => []
|
||||
},
|
||||
color: {
|
||||
@@ -98,11 +98,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
@@ -121,11 +121,18 @@ export default defineComponent({
|
||||
), props.class)
|
||||
})
|
||||
|
||||
function onAction (action: AlertAction) {
|
||||
if (action.click) {
|
||||
action.click()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
attrs,
|
||||
alertClass,
|
||||
onAction,
|
||||
twMerge
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,11 +86,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
|
||||
@@ -29,11 +29,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof avatarGroupConfig & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof avatarGroupConfig> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
|
||||
@@ -51,11 +51,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<ULink :type="type" :disabled="disabled || loading" :class="buttonClass" v-bind="attrs">
|
||||
<ULink :type="type" :disabled="disabled || loading" :class="buttonClass" v-bind="{ ...linkProps, ...attrs }">
|
||||
<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' : '']">
|
||||
<span v-if="label" :class="[truncate ? ui.truncate : '']">
|
||||
{{ label }}
|
||||
</span>
|
||||
</slot>
|
||||
@@ -23,7 +23,7 @@ import { twMerge, twJoin } from 'tailwind-merge'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import ULink from '../elements/Link.vue'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
import { mergeConfig } from '../../utils'
|
||||
import { mergeConfig, nuxtLinkProps, getNuxtLinkProps } from '../../utils'
|
||||
import { useInjectButtonGroup } from '../../composables/useButtonGroup'
|
||||
import type { ButtonColor, ButtonSize, ButtonVariant, Strategy } from '../../types'
|
||||
// @ts-expect-error
|
||||
@@ -39,6 +39,7 @@ export default defineComponent({
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...nuxtLinkProps,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
@@ -121,11 +122,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
@@ -154,7 +155,7 @@ export default defineComponent({
|
||||
ui.value.gap[size.value],
|
||||
props.padded && ui.value[isSquare.value ? 'square' : 'padding'][size.value],
|
||||
variant?.replaceAll('{color}', props.color),
|
||||
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center'
|
||||
props.block ? ui.value.block : ui.value.inline
|
||||
), props.class)
|
||||
})
|
||||
|
||||
@@ -178,7 +179,7 @@ export default defineComponent({
|
||||
return twJoin(
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && 'animate-spin'
|
||||
props.loading && ui.value.icon.loading
|
||||
)
|
||||
})
|
||||
|
||||
@@ -186,11 +187,15 @@ export default defineComponent({
|
||||
return twJoin(
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
props.loading && !isLeading.value && ui.value.icon.loading
|
||||
)
|
||||
})
|
||||
|
||||
const linkProps = computed(() => getNuxtLinkProps(props))
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
attrs,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
@@ -199,7 +204,8 @@ export default defineComponent({
|
||||
leadingIconName,
|
||||
trailingIconName,
|
||||
leadingIconClass,
|
||||
trailingIconClass
|
||||
trailingIconClass,
|
||||
linkProps
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,11 +32,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof buttonGroupConfig & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof buttonGroupConfig> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
@@ -52,20 +52,7 @@ export default defineComponent({
|
||||
), props.class)
|
||||
})
|
||||
|
||||
const rounded = computed(() => {
|
||||
const roundedMap = {
|
||||
'rounded-none': { horizontal: { start: 'rounded-s-none', end: 'rounded-e-none' }, vertical: { start: 'rounded-t-none', end: 'rounded-b-none' } },
|
||||
'rounded-sm': { horizontal: { start: 'rounded-s-sm', end: 'rounded-e-sm' }, vertical: { start: 'rounded-t-sm', end: 'rounded-b-sm' } },
|
||||
rounded: { horizontal: { start: 'rounded-s', end: 'rounded-e' }, vertical: { start: 'rounded-t', end: 'rounded-b' } },
|
||||
'rounded-md': { horizontal: { start: 'rounded-s-md', end: 'rounded-e-md' }, vertical: { start: 'rounded-t-md', end: 'rounded-b-md' } },
|
||||
'rounded-lg': { horizontal: { start: 'rounded-s-lg', end: 'rounded-e-lg' }, vertical: { start: 'rounded-t-lg', end: 'rounded-b-lg' } },
|
||||
'rounded-xl': { horizontal: { start: 'rounded-s-xl', end: 'rounded-e-xl' }, vertical: { start: 'rounded-t-xl', end: 'rounded-b-xl' } },
|
||||
'rounded-2xl': { horizontal: { start: 'rounded-s-2xl', end: 'rounded-e-2xl' }, vertical: { start: 'rounded-t-2xl', end: 'rounded-b-2xl' } },
|
||||
'rounded-3xl': { horizontal: { start: 'rounded-s-3xl', end: 'rounded-e-3xl' }, vertical: { start: 'rounded-t-3xl', end: 'rounded-b-3xl' } },
|
||||
'rounded-full': { horizontal: { start: 'rounded-s-full', end: 'rounded-e-full' }, vertical: { start: 'rounded-t-full', end: 'rounded-b-full' } }
|
||||
}
|
||||
return roundedMap[ui.value.rounded][props.orientation]
|
||||
})
|
||||
const rounded = computed(() => ui.value.orientation[ui.value.rounded][props.orientation])
|
||||
|
||||
useProvideButtonGroup({ orientation: toRef(props, 'orientation'), size: toRef(props, 'size'), ui, rounded })
|
||||
|
||||
|
||||
@@ -61,11 +61,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<!-- eslint-disable-next-line vue/no-template-shadow -->
|
||||
<HMenu v-slot="{ open }" as="div" :class="ui.wrapper" v-bind="attrs" @mouseleave="onMouseLeave">
|
||||
<HMenuButton
|
||||
ref="trigger"
|
||||
as="div"
|
||||
:disabled="disabled"
|
||||
class="inline-flex w-full"
|
||||
:class="ui.trigger"
|
||||
role="button"
|
||||
@mouseover="onMouseOver"
|
||||
>
|
||||
@@ -18,24 +19,25 @@
|
||||
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseover="onMouseOver">
|
||||
<Transition appear v-bind="ui.transition">
|
||||
<div>
|
||||
<div v-if="popper.arrow" data-popper-arrow :class="['invisible before:visible before:block before:rotate-45 before:z-[-1]', Object.values(ui.arrow)]" />
|
||||
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(ui.arrow)" />
|
||||
|
||||
<HMenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.height]" static>
|
||||
<div v-for="(subItems, index) of items" :key="index" :class="ui.padding">
|
||||
<NuxtLink v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ href, target, rel, navigate, isExternal }" v-bind="omit(item, ['label', 'slot', 'icon', 'iconClass', 'avatar', 'shortcuts', 'disabled', 'click'])" custom>
|
||||
<NuxtLink v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ href, target, rel, navigate, isExternal }" v-bind="getNuxtLinkProps(item)" custom>
|
||||
<HMenuItem v-slot="{ active, disabled: itemDisabled, close }" :disabled="item.disabled">
|
||||
<component
|
||||
:is="!!href ? 'a' : 'button'"
|
||||
:href="!itemDisabled ? href : undefined"
|
||||
:rel="rel"
|
||||
:target="target"
|
||||
:class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]"
|
||||
:class="twMerge(twJoin(ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled), item.class)"
|
||||
@click="onClick($event, item, { href, navigate, close, isExternal })"
|
||||
>
|
||||
<slot :name="item.slot || 'item'" :item="item">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="[ui.item.icon.base, active ? ui.item.icon.active : ui.item.icon.inactive, item.iconClass]" />
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="twMerge(twJoin(ui.item.icon.base, active ? ui.item.icon.active : ui.item.icon.inactive), item.iconClass)" />
|
||||
<UAvatar v-else-if="item.avatar" v-bind="{ size: ui.item.avatar.size, ...item.avatar }" :class="ui.item.avatar.base" />
|
||||
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
<span :class="twMerge(ui.item.label, item.labelClass)">{{ item.label }}</span>
|
||||
|
||||
<span v-if="item.shortcuts?.length" :class="ui.item.shortcuts">
|
||||
<UKbd v-for="shortcut of item.shortcuts" :key="shortcut">{{ shortcut }}</UKbd>
|
||||
@@ -53,16 +55,17 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, toRef, onMounted, resolveComponent } from 'vue'
|
||||
import { defineComponent, ref, computed, watch, toRef, onMounted, resolveComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue'
|
||||
import { defu } from 'defu'
|
||||
import { twMerge, twJoin } from 'tailwind-merge'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import UKbd from '../elements/Kbd.vue'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import { mergeConfig, omit } from '../../utils'
|
||||
import { mergeConfig, getNuxtLinkProps } from '../../utils'
|
||||
import type { DropdownItem, PopperOptions, Strategy } from '../../types'
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
@@ -91,6 +94,10 @@ export default defineComponent({
|
||||
default: 'click',
|
||||
validator: (value: string) => ['click', 'hover'].includes(value)
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -109,14 +116,15 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
emits: ['update:open'],
|
||||
setup (props, { emit }) {
|
||||
const { ui, attrs } = useUI('dropdown', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
|
||||
@@ -130,21 +138,46 @@ export default defineComponent({
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const menuProvides = trigger.value?.$.provides
|
||||
if (!menuProvides) {
|
||||
return
|
||||
}
|
||||
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
|
||||
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
|
||||
}, 200)
|
||||
// @ts-expect-error internals
|
||||
const menuProvides = trigger.value?.$.provides
|
||||
if (!menuProvides) {
|
||||
return
|
||||
}
|
||||
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
|
||||
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
|
||||
|
||||
if (props.open) {
|
||||
menuApi.value?.openMenu()
|
||||
}
|
||||
})
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8
|
||||
if (props.mode !== 'hover') {
|
||||
return {}
|
||||
}
|
||||
|
||||
return props.mode === 'hover' ? { paddingTop: `${offsetDistance}px`, paddingBottom: `${offsetDistance}px` } : {}
|
||||
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8
|
||||
const placement = popper.value.placement?.split('-')[0]
|
||||
const padding = `${offsetDistance}px`
|
||||
|
||||
if (placement === 'top' || placement === 'bottom') {
|
||||
return {
|
||||
paddingTop: padding,
|
||||
paddingBottom: padding
|
||||
}
|
||||
} else if (placement === 'left' || placement === 'right') {
|
||||
return {
|
||||
paddingLeft: padding,
|
||||
paddingRight: padding
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
paddingTop: padding,
|
||||
paddingBottom: padding,
|
||||
paddingLeft: padding,
|
||||
paddingRight: padding
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function onMouseOver () {
|
||||
@@ -199,6 +232,23 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.open, (newValue: boolean, oldValue: boolean) => {
|
||||
if (!menuApi.value) return
|
||||
if (oldValue === undefined || newValue === oldValue) return
|
||||
|
||||
if (newValue) {
|
||||
menuApi.value.openMenu()
|
||||
} else {
|
||||
menuApi.value.closeMenu()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => menuApi.value?.menuState, (newValue: number, oldValue: number) => {
|
||||
if (oldValue === undefined || newValue === oldValue) return
|
||||
|
||||
emit('update:open', newValue === 0)
|
||||
})
|
||||
|
||||
const NuxtLink = resolveComponent('NuxtLink')
|
||||
|
||||
return {
|
||||
@@ -213,7 +263,9 @@ export default defineComponent({
|
||||
onMouseOver,
|
||||
onMouseLeave,
|
||||
onClick,
|
||||
omit,
|
||||
getNuxtLinkProps,
|
||||
twMerge,
|
||||
twJoin,
|
||||
NuxtLink
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<component
|
||||
:is="as"
|
||||
v-if="!to"
|
||||
:type="type"
|
||||
:disabled="disabled"
|
||||
v-bind="$attrs"
|
||||
:class="active ? activeClass : inactiveClass"
|
||||
@@ -21,7 +22,7 @@
|
||||
:role="disabled ? 'link' : undefined"
|
||||
:rel="rel"
|
||||
:target="target"
|
||||
:class="active ? activeClass : resolveLinkClass(route, $route, { isActive, isExactActive })"
|
||||
:class="active !== undefined ? (active ? activeClass : inactiveClass) : resolveLinkClass(route, $route, { isActive, isExactActive })"
|
||||
@click="(e) => !isExternal && navigate(e)"
|
||||
>
|
||||
<slot v-bind="{ isActive: exact ? isExactActive : isActive }" />
|
||||
@@ -32,23 +33,27 @@
|
||||
<script lang="ts">
|
||||
import { isEqual } from 'ohash'
|
||||
import { defineComponent } from 'vue'
|
||||
import { NuxtLink } from '#components'
|
||||
import { nuxtLinkProps } from '../../utils'
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...NuxtLink.props,
|
||||
...nuxtLinkProps,
|
||||
as: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: undefined
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -87,11 +87,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
|
||||
@@ -44,15 +44,15 @@ export default defineComponent({
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'i-heroicons-minus'
|
||||
default: () => meterGroupConfig.default.icon
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof meterGroupConfig & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof meterGroupConfig> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
@@ -70,21 +70,7 @@ export default defineComponent({
|
||||
|
||||
const children = computed(() => getSlotsChildren(slots))
|
||||
|
||||
const rounded = computed(() => {
|
||||
const roundedMap = {
|
||||
'rounded-none': { left: 'rounded-s-none', right: 'rounded-e-none' },
|
||||
'rounded-sm': { left: 'rounded-s-sm', right: 'rounded-e-sm' },
|
||||
rounded: { left: 'rounded-s', right: 'rounded-e' },
|
||||
'rounded-md': { left: 'rounded-s-md', right: 'rounded-e-md' },
|
||||
'rounded-lg': { left: 'rounded-s-lg', right: 'rounded-e-lg' },
|
||||
'rounded-xl': { left: 'rounded-s-xl', right: 'rounded-e-xl' },
|
||||
'rounded-2xl': { left: 'rounded-s-2xl', right: 'rounded-e-2xl' },
|
||||
'rounded-3xl': { left: 'rounded-s-3xl', right: 'rounded-e-3xl' },
|
||||
'rounded-full': { left: 'rounded-s-full', right: 'rounded-e-full' }
|
||||
}
|
||||
|
||||
return roundedMap[ui.value.rounded]
|
||||
})
|
||||
const rounded = computed(() => ui.value.orientation[ui.value.rounded])
|
||||
|
||||
function clampPercent (value: number, min: number, max: number): number {
|
||||
if (min == max) {
|
||||
@@ -128,7 +114,7 @@ export default defineComponent({
|
||||
vProps.ui.wrapper = node.props?.ui?.wrapper || ''
|
||||
vProps.ui.wrapper += [
|
||||
node.props?.ui?.wrapper,
|
||||
props.ui?.meter?.background || ui.value.background,
|
||||
ui.value.background,
|
||||
ui.value.transition
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
@@ -153,8 +139,8 @@ export default defineComponent({
|
||||
|
||||
// @ts-expect-error
|
||||
delete(clone.children?.label)
|
||||
delete(clone.props.indicator)
|
||||
delete(clone.props.label)
|
||||
delete(clone.props?.indicator)
|
||||
delete(clone.props?.label)
|
||||
|
||||
return clone
|
||||
}))
|
||||
@@ -198,7 +184,7 @@ export default defineComponent({
|
||||
vNodeSlots[0] = slots.indicator({ percent: percent.value })
|
||||
}
|
||||
|
||||
vNodeSlots[2] = h('ol', { class: 'list-disc list-inside' }, labels.value.map((label, key) => {
|
||||
vNodeSlots[2] = h('ol', { class: ui.value.list }, labels.value.map((label, key) => {
|
||||
const labelClass = computed(() => {
|
||||
return twJoin(
|
||||
uiMeter.value.label.base,
|
||||
|
||||
@@ -39,7 +39,7 @@ export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: {
|
||||
type: [Number, null, undefined],
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
max: {
|
||||
@@ -73,11 +73,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
@@ -173,7 +173,7 @@ export default defineComponent({
|
||||
return classes.join(' ')
|
||||
}
|
||||
|
||||
const isIndeterminate = computed(() => [undefined, null].includes(props.value))
|
||||
const isIndeterminate = computed(() => props.value === undefined || props.value === null)
|
||||
const isSteps = computed(() => Array.isArray(props.max))
|
||||
|
||||
const realMax = computed(() => {
|
||||
@@ -191,8 +191,8 @@ export default defineComponent({
|
||||
const percent = computed(() => {
|
||||
switch (true) {
|
||||
case props.value < 0: return 0
|
||||
case props.value > realMax.value: return 100
|
||||
default: return (props.value / realMax.value) * 100
|
||||
case props.value > (realMax.value as number): return 100
|
||||
default: return (props.value / (realMax.value as number)) * 100
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper">
|
||||
<div class="flex items-center h-5">
|
||||
<div :class="ui.container">
|
||||
<input
|
||||
:id="inputId"
|
||||
v-model="toggle"
|
||||
@@ -11,13 +11,12 @@
|
||||
:checked="checked"
|
||||
:indeterminate="indeterminate"
|
||||
type="checkbox"
|
||||
class="form-checkbox"
|
||||
:class="inputClass"
|
||||
v-bind="attrs"
|
||||
@change="onChange"
|
||||
>
|
||||
</div>
|
||||
<div v-if="label || $slots.label" class="ms-3 flex flex-col">
|
||||
<div v-if="label || $slots.label" :class="ui.inner">
|
||||
<label :for="inputId" :class="ui.label">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
<span v-if="required" :class="ui.required">*</span>
|
||||
@@ -100,11 +99,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
@@ -130,11 +129,12 @@ export default defineComponent({
|
||||
const inputClass = computed(() => {
|
||||
return twMerge(twJoin(
|
||||
ui.value.base,
|
||||
ui.value.form,
|
||||
ui.value.rounded,
|
||||
ui.value.background,
|
||||
ui.value.border,
|
||||
ui.value.ring.replaceAll('{color}', color.value),
|
||||
ui.value.color.replaceAll('{color}', color.value)
|
||||
color.value && ui.value.ring.replaceAll('{color}', color.value),
|
||||
color.value && ui.value.color.replaceAll('{color}', color.value)
|
||||
), props.inputClass)
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { provide, ref, type PropType, defineComponent } from 'vue'
|
||||
import { provide, ref, type PropType, defineComponent, onUnmounted, onMounted } from 'vue'
|
||||
import { useEventBus } from '@vueuse/core'
|
||||
import type { ZodSchema } from 'zod'
|
||||
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
|
||||
@@ -51,10 +51,16 @@ export default defineComponent({
|
||||
setup (props, { expose, emit }) {
|
||||
const bus = useEventBus<FormEvent>(`form-${uid()}`)
|
||||
|
||||
bus.on(async (event) => {
|
||||
if (event.type !== 'submit' && props.validateOn?.includes(event.type)) {
|
||||
await validate(event.path, { silent: true })
|
||||
}
|
||||
onMounted(() => {
|
||||
bus.on(async (event) => {
|
||||
if (event.type !== 'submit' && props.validateOn?.includes(event.type)) {
|
||||
await validate(event.path, { silent: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
bus.reset()
|
||||
})
|
||||
|
||||
const errors = ref<FormError[]>([])
|
||||
@@ -104,7 +110,8 @@ export default defineComponent({
|
||||
return props.state
|
||||
}
|
||||
|
||||
async function onSubmit (event: SubmitEvent) {
|
||||
async function onSubmit (payload: Event) {
|
||||
const event = payload as SubmitEvent
|
||||
try {
|
||||
if (props.validateOn?.includes('submit')) {
|
||||
await validate()
|
||||
@@ -143,6 +150,9 @@ export default defineComponent({
|
||||
errors.value = errs
|
||||
}
|
||||
},
|
||||
async submit () {
|
||||
await onSubmit(new Event('submit'))
|
||||
},
|
||||
getErrors (path?: string) {
|
||||
if (path) {
|
||||
return errors.value.filter((err) => err.path === path)
|
||||
@@ -151,7 +161,7 @@ export default defineComponent({
|
||||
},
|
||||
clear (path?: string) {
|
||||
if (path) {
|
||||
errors.value = errors.value.filter((err) => err.path === path)
|
||||
errors.value = errors.value.filter((err) => err.path !== path)
|
||||
} else {
|
||||
errors.value = []
|
||||
}
|
||||
@@ -247,7 +257,7 @@ async function getValibotError (
|
||||
const result = await schema._parse(state)
|
||||
if (result.issues) {
|
||||
return result.issues.map((issue) => ({
|
||||
path: issue.path.map(p => p.key).join('.'),
|
||||
path: issue.path?.map(p => p.key).join('.') || '',
|
||||
message: issue.message
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -89,11 +89,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
},
|
||||
eagerValidation: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
:type="type"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled || loading"
|
||||
class="form-input"
|
||||
:disabled="disabled"
|
||||
:class="inputClass"
|
||||
v-bind="attrs"
|
||||
@input="onInput"
|
||||
@@ -153,11 +152,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
},
|
||||
modelModifiers: {
|
||||
type: Object as PropType<{ trim?: boolean, lazy?: boolean, number?: boolean }>,
|
||||
@@ -199,13 +198,13 @@ export default defineComponent({
|
||||
emitFormInput()
|
||||
}
|
||||
|
||||
const onInput = (event: InputEvent) => {
|
||||
const onInput = (event: Event) => {
|
||||
if (!modelModifiers.value.lazy) {
|
||||
updateInput((event.target as HTMLInputElement).value)
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = (event: InputEvent) => {
|
||||
const onChange = (event: Event) => {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
|
||||
if (modelModifiers.value.lazy) {
|
||||
@@ -234,6 +233,7 @@ export default defineComponent({
|
||||
|
||||
return twMerge(twJoin(
|
||||
ui.value.base,
|
||||
ui.value.form,
|
||||
rounded.value,
|
||||
ui.value.placeholder,
|
||||
ui.value.size[size.value],
|
||||
@@ -279,9 +279,9 @@ export default defineComponent({
|
||||
const leadingIconClass = computed(() => {
|
||||
return twJoin(
|
||||
ui.value.icon.base,
|
||||
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
color.value && appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && 'animate-spin'
|
||||
props.loading && ui.value.icon.loading
|
||||
)
|
||||
})
|
||||
|
||||
@@ -296,9 +296,9 @@ export default defineComponent({
|
||||
const trailingIconClass = computed(() => {
|
||||
return twJoin(
|
||||
ui.value.icon.base,
|
||||
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
color.value && appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
props.loading && !isLeading.value && ui.value.icon.loading
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
454
src/runtime/components/forms/InputMenu.vue
Normal file
454
src/runtime/components/forms/InputMenu.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<HCombobox
|
||||
v-slot="{ open }"
|
||||
:by="by"
|
||||
:name="name"
|
||||
:model-value="modelValue"
|
||||
:disabled="disabled"
|
||||
as="div"
|
||||
:class="ui.wrapper"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<div :class="uiMenu.trigger">
|
||||
<HComboboxInput
|
||||
:id="inputId"
|
||||
:name="name"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="inputClass"
|
||||
autocomplete="off"
|
||||
v-bind="attrs"
|
||||
:display-value="() => query ? query : label"
|
||||
@change="onChange"
|
||||
/>
|
||||
|
||||
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
|
||||
<slot name="leading" :disabled="disabled" :loading="loading">
|
||||
<UIcon :name="leadingIconName" :class="leadingIconClass" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<HComboboxButton v-if="(isTrailing && trailingIconName) || $slots.trailing" ref="trigger" :class="trailingWrapperIconClass">
|
||||
<slot name="trailing" :disabled="disabled" :loading="loading">
|
||||
<UIcon :name="trailingIconName" :class="trailingIconClass" />
|
||||
</slot>
|
||||
</HComboboxButton>
|
||||
</div>
|
||||
|
||||
<div v-if="open" ref="container" :class="[uiMenu.container, uiMenu.width]">
|
||||
<Transition appear v-bind="uiMenu.transition">
|
||||
<div>
|
||||
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(uiMenu.arrow)" />
|
||||
|
||||
<HComboboxOptions static :class="[uiMenu.base, uiMenu.ring, uiMenu.rounded, uiMenu.shadow, uiMenu.background, uiMenu.padding, uiMenu.height]">
|
||||
<HComboboxOption
|
||||
v-for="(option, index) in filteredOptions"
|
||||
v-slot="{ active, selected, disabled: optionDisabled }"
|
||||
:key="index"
|
||||
as="template"
|
||||
:value="valueAttribute ? option[valueAttribute] : option"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive, selected && uiMenu.option.selected, optionDisabled && uiMenu.option.disabled]">
|
||||
<div :class="uiMenu.option.container">
|
||||
<slot name="option" :option="option" :active="active" :selected="selected">
|
||||
<UIcon v-if="option.icon" :name="option.icon" :class="[uiMenu.option.icon.base, active ? uiMenu.option.icon.active : uiMenu.option.icon.inactive, option.iconClass]" aria-hidden="true" />
|
||||
<UAvatar
|
||||
v-else-if="option.avatar"
|
||||
v-bind="{ size: uiMenu.option.avatar.size, ...option.avatar }"
|
||||
:class="uiMenu.option.avatar.base"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span v-else-if="option.chip" :class="uiMenu.option.chip.base" :style="{ background: `#${option.chip}` }" />
|
||||
|
||||
<span class="truncate">{{ ['string', 'number'].includes(typeof option) ? option : option[optionAttribute] }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<span v-if="selected" :class="[uiMenu.option.selectedIcon.wrapper, uiMenu.option.selectedIcon.padding]">
|
||||
<UIcon :name="selectedIcon" :class="uiMenu.option.selectedIcon.base" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</HComboboxOption>
|
||||
|
||||
<p v-if="query && !filteredOptions.length" :class="uiMenu.option.empty">
|
||||
<slot name="option-empty" :query="query">
|
||||
No results for "{{ query }}".
|
||||
</slot>
|
||||
</p>
|
||||
<p v-else-if="!filteredOptions.length" :class="uiMenu.empty">
|
||||
<slot name="empty" :query="query">
|
||||
No options.
|
||||
</slot>
|
||||
</p>
|
||||
</HComboboxOptions>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</HCombobox>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, toRef, watch, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import {
|
||||
Combobox as HCombobox,
|
||||
ComboboxButton as HComboboxButton,
|
||||
ComboboxOptions as HComboboxOptions,
|
||||
ComboboxOption as HComboboxOption,
|
||||
ComboboxInput as HComboboxInput
|
||||
} from '@headlessui/vue'
|
||||
import { computedAsync, useDebounceFn } from '@vueuse/core'
|
||||
import { defu } from 'defu'
|
||||
import { twMerge, twJoin } from 'tailwind-merge'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
import UAvatar from '../elements/Avatar.vue'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import { useFormGroup } from '../../composables/useFormGroup'
|
||||
import { get, mergeConfig } from '../../utils'
|
||||
import { useInjectButtonGroup } from '../../composables/useButtonGroup'
|
||||
import type { InputSize, InputColor, InputVariant, PopperOptions, Strategy } from '../../types'
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
import { input, inputMenu } from '#ui/ui.config'
|
||||
|
||||
const config = mergeConfig<typeof input>(appConfig.ui.strategy, appConfig.ui.input, input)
|
||||
|
||||
const configMenu = mergeConfig<typeof inputMenu>(appConfig.ui.strategy, appConfig.ui.inputMenu, inputMenu)
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
HCombobox,
|
||||
HComboboxButton,
|
||||
HComboboxOptions,
|
||||
HComboboxOption,
|
||||
HComboboxInput,
|
||||
UIcon,
|
||||
UAvatar
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
by: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[] | string[]>,
|
||||
default: () => []
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => config.default.loadingIcon
|
||||
},
|
||||
leadingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailingIcon: {
|
||||
type: String,
|
||||
default: () => configMenu.default.trailingIcon
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedIcon: {
|
||||
type: String,
|
||||
default: () => configMenu.default.selectedIcon
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
padded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<InputSize>,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys(config.size).includes(value)
|
||||
}
|
||||
},
|
||||
color: {
|
||||
type: String as PropType<InputColor>,
|
||||
default: () => config.default.color,
|
||||
validator (value: string) {
|
||||
return [...appConfig.ui.colors, ...Object.keys(config.color)].includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String as PropType<InputVariant>,
|
||||
default: () => config.default.variant,
|
||||
validator (value: string) {
|
||||
return [
|
||||
...Object.keys(config.variant),
|
||||
...Object.values(config.color).flatMap(value => Object.keys(value))
|
||||
].includes(value)
|
||||
}
|
||||
},
|
||||
optionAttribute: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
valueAttribute: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
search: {
|
||||
type: Function as PropType<((query: string) => Promise<any[]> | any[])>,
|
||||
default: undefined
|
||||
},
|
||||
searchAttributes: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
debounce: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
popper: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
inputClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
},
|
||||
uiMenu: {
|
||||
type: Object as PropType<Partial<typeof configMenu> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'update:query', 'open', 'close', 'change'],
|
||||
setup (props, { emit, slots }) {
|
||||
const { ui, attrs } = useUI('input', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const { ui: uiMenu } = useUI('inputMenu', toRef(props, 'uiMenu'), configMenu)
|
||||
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, uiMenu.value.popper as PopperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popper.value)
|
||||
|
||||
const { size: sizeButtonGroup, rounded } = useInjectButtonGroup({ ui, props })
|
||||
const { emitFormBlur, emitFormChange, inputId, color, size: sizeFormGroup, name } = useFormGroup(props, config)
|
||||
|
||||
const size = computed(() => sizeButtonGroup.value || sizeFormGroup.value)
|
||||
|
||||
const internalQuery = ref('')
|
||||
const query = computed({
|
||||
get () {
|
||||
return props.query ?? internalQuery.value
|
||||
},
|
||||
set (value) {
|
||||
internalQuery.value = value
|
||||
emit('update:query', value)
|
||||
}
|
||||
})
|
||||
|
||||
const label = computed(() => {
|
||||
if (props.valueAttribute) {
|
||||
const option = props.options.find(option => option[props.valueAttribute] === props.modelValue)
|
||||
return option ? option[props.optionAttribute] : null
|
||||
} else {
|
||||
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : props.modelValue[props.optionAttribute]
|
||||
}
|
||||
})
|
||||
|
||||
const inputClass = computed(() => {
|
||||
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
|
||||
return twMerge(twJoin(
|
||||
ui.value.base,
|
||||
ui.value.form,
|
||||
rounded.value,
|
||||
ui.value.placeholder,
|
||||
ui.value.size[size.value],
|
||||
props.padded ? ui.value.padding[size.value] : 'p-0',
|
||||
variant?.replaceAll('{color}', color.value),
|
||||
(isLeading.value || slots.leading) && ui.value.leading.padding[size.value],
|
||||
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[size.value]
|
||||
), props.inputClass)
|
||||
})
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
|
||||
})
|
||||
|
||||
const leadingIconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.leadingIcon || props.icon
|
||||
})
|
||||
|
||||
const trailingIconName = computed(() => {
|
||||
if (props.loading && !isLeading.value) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.trailingIcon || props.icon
|
||||
})
|
||||
|
||||
const leadingWrapperIconClass = computed(() => {
|
||||
return twJoin(
|
||||
ui.value.icon.leading.wrapper,
|
||||
ui.value.icon.leading.pointer,
|
||||
ui.value.icon.leading.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return twJoin(
|
||||
ui.value.icon.base,
|
||||
color.value && appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && ui.value.icon.loading
|
||||
)
|
||||
})
|
||||
|
||||
const trailingWrapperIconClass = computed(() => {
|
||||
return twJoin(
|
||||
ui.value.icon.trailing.wrapper,
|
||||
ui.value.icon.trailing.padding[size.value]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return twJoin(
|
||||
ui.value.icon.base,
|
||||
color.value && appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
|
||||
ui.value.icon.size[size.value],
|
||||
props.loading && !isLeading.value && ui.value.icon.loading
|
||||
)
|
||||
})
|
||||
|
||||
const debouncedSearch = props.search && typeof props.search === 'function' ? useDebounceFn(props.search, props.debounce) : undefined
|
||||
|
||||
const filteredOptions = computedAsync(async () => {
|
||||
if (debouncedSearch) {
|
||||
return await debouncedSearch(query.value)
|
||||
}
|
||||
|
||||
if (query.value === '') {
|
||||
return props.options
|
||||
}
|
||||
|
||||
return (props.options as any[]).filter((option: any) => {
|
||||
return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => {
|
||||
if (['string', 'number'].includes(typeof option)) {
|
||||
return String(option).search(new RegExp(query.value, 'i')) !== -1
|
||||
}
|
||||
|
||||
const child = get(option, searchAttribute)
|
||||
|
||||
return child !== null && child !== undefined && String(child).search(new RegExp(query.value, 'i')) !== -1
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
watch(container, (value) => {
|
||||
if (value) {
|
||||
emit('open')
|
||||
} else {
|
||||
emit('close')
|
||||
emitFormBlur()
|
||||
}
|
||||
})
|
||||
|
||||
function onUpdate (event: any) {
|
||||
query.value = ''
|
||||
emit('update:modelValue', event)
|
||||
emit('change', event)
|
||||
emitFormChange()
|
||||
}
|
||||
|
||||
function onChange (event: any) {
|
||||
query.value = event.target.value
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
uiMenu,
|
||||
attrs,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
name,
|
||||
inputId,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
popper,
|
||||
trigger,
|
||||
container,
|
||||
label,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
inputClass,
|
||||
leadingIconName,
|
||||
leadingIconClass,
|
||||
leadingWrapperIconClass,
|
||||
trailingIconName,
|
||||
trailingIconClass,
|
||||
trailingWrapperIconClass,
|
||||
filteredOptions,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
query,
|
||||
onUpdate,
|
||||
onChange
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper">
|
||||
<div class="flex items-center h-5">
|
||||
<div :class="ui.container">
|
||||
<input
|
||||
:id="inputId"
|
||||
v-model="pick"
|
||||
@@ -9,12 +9,11 @@
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
type="radio"
|
||||
class="form-radio"
|
||||
:class="inputClass"
|
||||
v-bind="attrs"
|
||||
>
|
||||
</div>
|
||||
<div v-if="label || $slots.label" class="ms-3 flex flex-col">
|
||||
<div v-if="label || $slots.label" :class="ui.inner">
|
||||
<label :for="inputId" :class="ui.label">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
<span v-if="required" :class="ui.required">*</span>
|
||||
@@ -90,11 +89,11 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
@@ -102,7 +101,7 @@ export default defineComponent({
|
||||
const { ui, attrs } = useUI('radio', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const radioGroup = inject('radio-group', null)
|
||||
const { emitFormChange, color, name } = radioGroup ?? useFormGroup(props, config)
|
||||
const { emitFormChange, color, name } = radioGroup ?? useFormGroup(props, config)
|
||||
const inputId = ref(props.id)
|
||||
|
||||
onMounted(() => {
|
||||
@@ -128,10 +127,11 @@ export default defineComponent({
|
||||
const inputClass = computed(() => {
|
||||
return twMerge(twJoin(
|
||||
ui.value.base,
|
||||
ui.value.form,
|
||||
ui.value.background,
|
||||
ui.value.border,
|
||||
ui.value.ring.replaceAll('{color}', color.value),
|
||||
ui.value.color.replaceAll('{color}', color.value)
|
||||
color.value && ui.value.ring.replaceAll('{color}', color.value),
|
||||
color.value && ui.value.color.replaceAll('{color}', color.value)
|
||||
), props.inputClass)
|
||||
})
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
:label="option.label"
|
||||
:model-value="modelValue"
|
||||
:value="option.value"
|
||||
:disabled="disabled"
|
||||
:disabled="option.disabled || disabled"
|
||||
:ui="uiRadio"
|
||||
@change="onUpdate(option.value)"
|
||||
>
|
||||
@@ -83,15 +83,15 @@ export default defineComponent({
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: undefined
|
||||
default: () => ''
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
},
|
||||
uiRadio: {
|
||||
type: Object as PropType<Partial<typeof configRadio & { strategy?: Strategy }>>,
|
||||
default: undefined
|
||||
type: Object as PropType<Partial<typeof configRadio> & { strategy?: Strategy }>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
@@ -99,7 +99,7 @@ export default defineComponent({
|
||||
const { ui, attrs } = useUI('radioGroup', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
const { ui: uiRadio } = useUI('radio', toRef(props, 'uiRadio'), configRadio)
|
||||
|
||||
const { emitFormChange, color, name } = useFormGroup({ ...props, isFieldset: true }, config)
|
||||
const { emitFormChange, color, name } = useFormGroup(props, config)
|
||||
provide('radio-group', { color, name })
|
||||
|
||||
const onUpdate = (value: any) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user