mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-19 14:31:47 +01:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e05b0f072 | ||
|
|
87e98f038a | ||
|
|
f7e2082983 | ||
|
|
f719111abb | ||
|
|
3bac0874f1 | ||
|
|
457b7a9fb7 | ||
|
|
4023fbec29 | ||
|
|
a274a0cdbb | ||
|
|
717a514451 | ||
|
|
786d7765f5 | ||
|
|
a733c13866 | ||
|
|
88c1930845 | ||
|
|
c3f5c44461 | ||
|
|
2cfa1f8d03 | ||
|
|
5f7de8e595 | ||
|
|
cdce519742 | ||
|
|
ccd9ca5106 | ||
|
|
9031742acc | ||
|
|
9559d0b3bc | ||
|
|
0e6550ec45 | ||
|
|
20fa4d2317 | ||
|
|
e12e9740c9 | ||
|
|
cbc8ef13cc | ||
|
|
652af93f5c | ||
|
|
b4a96a8b01 | ||
|
|
bc81d45b2b | ||
|
|
429791dab0 | ||
|
|
fe833eb2b2 | ||
|
|
be5f352296 | ||
|
|
47415322ea | ||
|
|
d20983d355 | ||
|
|
f0b24ba25d | ||
|
|
f7a34c8fee | ||
|
|
4e5e614eb4 | ||
|
|
07f7855a26 | ||
|
|
57f95102e2 | ||
|
|
3f8d927438 | ||
|
|
d91c0bb894 | ||
|
|
a6176720c7 | ||
|
|
a6903df58f | ||
|
|
19b149518e | ||
|
|
c66a99a60f | ||
|
|
4a7c6035b6 | ||
|
|
207444fdea | ||
|
|
60eea0e46b | ||
|
|
5e50eb9eb8 | ||
|
|
af65683123 | ||
|
|
2c673f5377 | ||
|
|
192b0e6301 | ||
|
|
71edb91c4f | ||
|
|
f9b935f5f5 | ||
|
|
23833e92cb | ||
|
|
241df7f05e | ||
|
|
130a1f2c54 | ||
|
|
c63981e31c | ||
|
|
687f0c6f63 | ||
|
|
f59a92ca15 | ||
|
|
01fa85c7a3 | ||
|
|
3434bc7f2b | ||
|
|
9b1aacb1da | ||
|
|
8951923a11 | ||
|
|
e200d4cc74 | ||
|
|
e05619f8c8 | ||
|
|
5ea43ab4e4 | ||
|
|
ba44c58a80 | ||
|
|
490025a981 | ||
|
|
2966373a86 | ||
|
|
8bdb8c45f7 | ||
|
|
9827de0b58 | ||
|
|
23f01fde41 | ||
|
|
f680318e44 | ||
|
|
cd2d1eb1fa | ||
|
|
3ba0aedcba | ||
|
|
40b6884424 | ||
|
|
a2638c6057 | ||
|
|
6bd5142a37 | ||
|
|
bc1d653857 | ||
|
|
6c215e07a6 | ||
|
|
272af9d24c | ||
|
|
cce000ab2b | ||
|
|
4a99d6a7bb | ||
|
|
4458656be5 | ||
|
|
daca46371c | ||
|
|
8ee2ac10e7 | ||
|
|
1ebaa5aa00 | ||
|
|
cb43548305 | ||
|
|
360084af7c |
22
.release-it.json
Normal file
22
.release-it.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"git": {
|
||||||
|
"commitMessage": "chore(release): ${version}"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"publish": false
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"release": true,
|
||||||
|
"web": true
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"before:init": ["pnpm lint"]
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"@release-it/conventional-changelog": {
|
||||||
|
"preset": "conventionalcommits",
|
||||||
|
"infile": "CHANGELOG.md",
|
||||||
|
"ignoreRecommendedBump": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
CHANGELOG.md
93
CHANGELOG.md
@@ -1,7 +1,98 @@
|
|||||||
|
|
||||||
|
|
||||||
|
### [2.4.1](https://github.com/nuxtlabs/ui/compare/v2.4.0...v2.4.1) (2023-06-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **forms:** precise type assertion for `onInput` event handler ([#293](https://github.com/nuxtlabs/ui/issues/293)) ([457b7a9](https://github.com/nuxtlabs/ui/commit/457b7a9fb72e6469014b6ca18e7034dd5c6f44b8))
|
||||||
|
* **module:** let `tailwindcss` viewer enabled by default ([4023fbe](https://github.com/nuxtlabs/ui/commit/4023fbec29e5b4d40fd23e8c2ae3d0cf23addc64)), closes [#292](https://github.com/nuxtlabs/ui/issues/292)
|
||||||
|
* **module:** safelist aliases for input ([f719111](https://github.com/nuxtlabs/ui/commit/f719111abb94c81f3932927a0154b3e1bed73a9a))
|
||||||
|
* **module:** safelist regex when a `:` was present before color ([f7e2082](https://github.com/nuxtlabs/ui/commit/f7e2082983c2eb650e95a9040aafde4ce2c88c54))
|
||||||
|
* **Radio/Checkbox:** remove legacy `custom` ([3bac087](https://github.com/nuxtlabs/ui/commit/3bac0874f106a8ff7436b541f9d064c1c7c27464))
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.4.0](https://github.com/nuxtlabs/ui/compare/v2.3.0...v2.4.0) (2023-06-13)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* **forms:** bind `$attrs` to elements (#279)
|
||||||
|
* **Select:** rename `text-attribute` to `option-attribute` and defaults to `label`
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **CommandPalette:** handle `empty-state` ([#271](https://github.com/nuxtlabs/ui/issues/271)) ([652af93](https://github.com/nuxtlabs/ui/commit/652af93f5c7cd4b34044a5597f3c14441ed6d998))
|
||||||
|
* **module:** smart safelisting ([#268](https://github.com/nuxtlabs/ui/issues/268)) ([20fa4d2](https://github.com/nuxtlabs/ui/commit/20fa4d2317fc1e14fe87fa273957b92e63668945))
|
||||||
|
* **Pagination:** new component ([#257](https://github.com/nuxtlabs/ui/issues/257)) ([f0b24ba](https://github.com/nuxtlabs/ui/commit/f0b24ba25d52184b8683e364016ed8fb800fc96b))
|
||||||
|
* **table:** add loading state ([#259](https://github.com/nuxtlabs/ui/issues/259)) ([4741532](https://github.com/nuxtlabs/ui/commit/47415322ea56b5388e55c404c901531e807a9f00))
|
||||||
|
* **table:** add slot for empty state ([#260](https://github.com/nuxtlabs/ui/issues/260)) ([f7a34c8](https://github.com/nuxtlabs/ui/commit/f7a34c8feeda6a4e1e1daff87b37b375aaa0c90d))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ButtonGroup:** invalid `size` validator ([a617672](https://github.com/nuxtlabs/ui/commit/a6176720c75b26768ba91efcab50689a932931ad))
|
||||||
|
* **ButtonGroup:** use `-space-x-px` on wrapper ([d91c0bb](https://github.com/nuxtlabs/ui/commit/d91c0bb8944224d4e8eb62f99a33a6be94e5cd92))
|
||||||
|
* **Button:** same size when no label + uniformize form elements ([a6903df](https://github.com/nuxtlabs/ui/commit/a6903df58fb91da44e6f83cc2bd9c963827fe5dd))
|
||||||
|
* **CommandPalette:** input focus after be5f352 ([cbc8ef1](https://github.com/nuxtlabs/ui/commit/cbc8ef13cc3253690c22c32d90ea9746970c345a))
|
||||||
|
* **deps:** move `@tailwindcss/container-queries` to dependencies ([9559d0b](https://github.com/nuxtlabs/ui/commit/9559d0b3bc09956d7fe17ee0deeef03599d02d45))
|
||||||
|
* **forms:** `padded` prop with `p-0` class ([207444f](https://github.com/nuxtlabs/ui/commit/207444fdea773b8ee64dd4f80b4f70b76462a9d6))
|
||||||
|
* **forms:** bind `$attrs` to elements ([#279](https://github.com/nuxtlabs/ui/issues/279)) ([e12e974](https://github.com/nuxtlabs/ui/commit/e12e9740c97b75d3b7b70c38978e249b5e26eead))
|
||||||
|
* **module:** deduplicate default safelist as components may share same rules ([2cfa1f8](https://github.com/nuxtlabs/ui/commit/2cfa1f8d0355d4c9cec5d4294d63e043d223cd64))
|
||||||
|
* **module:** hardcode `gray` safelist instead of deduplicate complex logic ([a733c13](https://github.com/nuxtlabs/ui/commit/a733c13866cdb74398f3e6f022cc63223e269e19))
|
||||||
|
* **module:** only safelist known colors ([cdce519](https://github.com/nuxtlabs/ui/commit/cdce519742b86ff29460aa50264d7bb34ad24bd0))
|
||||||
|
* **module:** prevent safelisting dynamic `:color` variables ([ccd9ca5](https://github.com/nuxtlabs/ui/commit/ccd9ca5106d0b81aed6591097f121eb81dcc9b47))
|
||||||
|
* **module:** transform `vue` files to detect multi-line components ([88c1930](https://github.com/nuxtlabs/ui/commit/88c1930845d26c66c2fbd32f99f52dbd23244341))
|
||||||
|
* **module:** use `@tailwindcss/forms` class strategy ([#278](https://github.com/nuxtlabs/ui/issues/278)) ([be5f352](https://github.com/nuxtlabs/ui/commit/be5f352296cf4e0c9099cf468ed905283b31007d))
|
||||||
|
* **Notification:** class priority for icon color ([07f7855](https://github.com/nuxtlabs/ui/commit/07f7855a263e516250f62d0730afc69753d0322c))
|
||||||
|
* **Radio/Checkbox:** split preset as `indeterminate` is checkbox only ([429791d](https://github.com/nuxtlabs/ui/commit/429791dab0fbb84bae1e1e13e7e688708f0b5c98))
|
||||||
|
* **SelectMenu:** input focus after `be5f352` ([717a514](https://github.com/nuxtlabs/ui/commit/717a5144511c4db013a57869ac06421accf51e38))
|
||||||
|
* **Table:** colspan of `empty` and `loading` is wrong when selection enabled ([#284](https://github.com/nuxtlabs/ui/issues/284)) ([786d776](https://github.com/nuxtlabs/ui/commit/786d7765f5517a7e8cdd718ce93fd9fecc427ba7))
|
||||||
|
* **Toggle:** missing `disabled` prop ([fe833eb](https://github.com/nuxtlabs/ui/commit/fe833eb2b2b4d1d32eb9e082b437a0259b6f75c6))
|
||||||
|
|
||||||
|
|
||||||
|
* **Select:** rename `text-attribute` to `option-attribute` and defaults to `label` ([b4a96a8](https://github.com/nuxtlabs/ui/commit/b4a96a8b01b52751c9a9c6609ed8cf7ccf516a04))
|
||||||
|
|
||||||
|
## [2.3.0](https://github.com/nuxtlabs/ui/compare/v2.2.1...v2.3.0) (2023-06-05)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* **Input:** move pointer class inside its own preset class
|
||||||
|
* **SelectMenu:** remove `inline-flex` from wrapper to behave like other form elements
|
||||||
|
* **Notification:** rename to `closeButton` and `actionButton` for consistency
|
||||||
|
* **CommandPalette:** rename props to `emptyState` and `closeButton` for consistency
|
||||||
|
* **Toggle:** rename icons to `onIcon` / `offIcon` for consistency
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add `Table` component ([#237](https://github.com/nuxtlabs/ui/issues/237)) ([cce000a](https://github.com/nuxtlabs/ui/commit/cce000ab2b2af1079216e0e79769703fc4d9933e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Avatar:** placeholder font size ([71edb91](https://github.com/nuxtlabs/ui/commit/71edb91c4ff17a258d6229ed6c6fa6a4b54bdd53))
|
||||||
|
* **Badge:** remove `console.log` in validator ([f9b935f](https://github.com/nuxtlabs/ui/commit/f9b935f5f59b872fd952a2739d305d6574bf7cf8))
|
||||||
|
* **Button:** invalid padding when using `square` prop ([1ebaa5a](https://github.com/nuxtlabs/ui/commit/1ebaa5aa00752cd276f7c754d64ac7f85b14dc26))
|
||||||
|
* **CommandPalette:** override of `closeButton` and `emptyState` props ([2c673f5](https://github.com/nuxtlabs/ui/commit/2c673f5377dbbcdefa6b57eddba2c19d065d5f1f))
|
||||||
|
* **defineShortcuts:** err with input autocomplete that triggers `keydown` ([01fa85c](https://github.com/nuxtlabs/ui/commit/01fa85c7a3e476d4f710ed3a36c1e815fc986a94))
|
||||||
|
* **SelectMenu:** disable on loading ([8951923](https://github.com/nuxtlabs/ui/commit/8951923a11d533ebf53dbec5f852800555af253c))
|
||||||
|
* **Table:** add missing `text-left` in `th.base` ([6bd5142](https://github.com/nuxtlabs/ui/commit/6bd5142a377694599952e0f9b53fde0d0132b61b))
|
||||||
|
* **Table:** missing `ref` import from `vue` ([272af9d](https://github.com/nuxtlabs/ui/commit/272af9d24c7cda8341e66b57f76acdb9f46ea23e))
|
||||||
|
* **Table:** override of `sortButton` and `emptyState` props ([192b0e6](https://github.com/nuxtlabs/ui/commit/192b0e63018ae73e8acaa8b4b1771cda2b59bdb6))
|
||||||
|
* **Table:** type `sort` prop ([3ba0aed](https://github.com/nuxtlabs/ui/commit/3ba0aedcba578350e2fdd9c180505ed8920e0404))
|
||||||
|
* use `cloneVNode` when altering props in render functions ([5e50eb9](https://github.com/nuxtlabs/ui/commit/5e50eb9eb82571d22e0a2f1a2fe985addf7efe18)), closes [#252](https://github.com/nuxtlabs/ui/issues/252)
|
||||||
|
|
||||||
|
|
||||||
|
* **CommandPalette:** rename props to `emptyState` and `closeButton` for consistency ([daca463](https://github.com/nuxtlabs/ui/commit/daca46371cab1344bd87ffb0abe0f7e9cdb08609))
|
||||||
|
* **Input:** move pointer class inside its own preset class ([f59a92c](https://github.com/nuxtlabs/ui/commit/f59a92ca1533a44e17fbc8b7945bdaa9a83e805a))
|
||||||
|
* **Notification:** rename to `closeButton` and `actionButton` for consistency ([4458656](https://github.com/nuxtlabs/ui/commit/4458656be5547fc9505a5c4758bea4818ada408b))
|
||||||
|
* **SelectMenu:** remove `inline-flex` from wrapper to behave like other form elements ([ba44c58](https://github.com/nuxtlabs/ui/commit/ba44c58a80252a4394fcf2f84611ea2696883120))
|
||||||
|
* **Toggle:** rename icons to `onIcon` / `offIcon` for consistency ([8ee2ac1](https://github.com/nuxtlabs/ui/commit/8ee2ac10e7eda4c54418f613a5ef87dd89e1f7eb))
|
||||||
|
|
||||||
### [2.2.1](https://github.com/nuxtlabs/ui/compare/v2.2.0...v2.2.1) (2023-05-27)
|
### [2.2.1](https://github.com/nuxtlabs/ui/compare/v2.2.0...v2.2.1) (2023-05-27)
|
||||||
|
|
||||||
|
|
||||||
@@ -541,4 +632,4 @@ All notable changes to this project will be documented in this file. See [standa
|
|||||||
* **Toggle:** add missing `computed` import ([0f09c9b](https://github.com/nuxtlabs/ui/commit/0f09c9baae501458af029f853c78b1c10a3ac133))
|
* **Toggle:** add missing `computed` import ([0f09c9b](https://github.com/nuxtlabs/ui/commit/0f09c9baae501458af029f853c78b1c10a3ac133))
|
||||||
* **Tooltip:** missing `ref` import ([b08a8cc](https://github.com/nuxtlabs/ui/commit/b08a8cc0ac79e89817e338281a81c477d5ec645a))
|
* **Tooltip:** missing `ref` import ([b08a8cc](https://github.com/nuxtlabs/ui/commit/b08a8cc0ac79e89817e338281a81c477d5ec645a))
|
||||||
* **useTimer:** remove log ([c6dcbd1](https://github.com/nuxtlabs/ui/commit/c6dcbd1b2b542dab1850504a60451a485e2d4004))
|
* **useTimer:** remove log ([c6dcbd1](https://github.com/nuxtlabs/ui/commit/c6dcbd1b2b542dab1850504a60451a485e2d4004))
|
||||||
* **VerticalNavigation:** add `v-if` on label ([79d8e08](https://github.com/nuxtlabs/ui/commit/79d8e086f0c61887c52da6fe4a13f1bdf7077227))
|
* **VerticalNavigation:** add `v-if` on label ([79d8e08](https://github.com/nuxtlabs/ui/commit/79d8e086f0c61887c52da6fe4a13f1bdf7077227))
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center -mr-1.5">
|
<div class="flex items-center -mr-1.5 gap-1.5">
|
||||||
<div class="mr-1.5 hidden lg:block">
|
<div class="hidden lg:block">
|
||||||
<ThemeSelect />
|
<ThemeSelect />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center shadow-sm">
|
<ClientOnly>
|
||||||
<ClientOnly>
|
<div class="inline-flex shadow-sm rounded-md">
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-model="primary"
|
v-model="primary"
|
||||||
name="primary"
|
name="primary"
|
||||||
class="w-full [&>div>button]:!rounded-r-none"
|
class="!rounded-r-none !shadow-none focus:z-[1]"
|
||||||
color="gray"
|
color="gray"
|
||||||
:ui="{ width: 'w-[194px]' }"
|
:ui="{ width: 'w-[194px]' }"
|
||||||
:popper="{ placement: 'bottom-start' }"
|
:popper="{ placement: 'bottom-start' }"
|
||||||
@@ -22,15 +22,13 @@
|
|||||||
{{ option.text }}
|
{{ option.text }}
|
||||||
</template>
|
</template>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-model="gray"
|
v-model="gray"
|
||||||
name="gray"
|
name="gray"
|
||||||
class="w-full [&>div>button]:!rounded-l-none [&>div>button]:-ml-px"
|
class="!rounded-l-none !shadow-none"
|
||||||
color="gray"
|
color="gray"
|
||||||
:ui="{ width: 'w-[194px]' }"
|
:ui="{ width: 'w-[194px]', wrapper: '-ml-px' }"
|
||||||
:popper="{ placement: 'bottom-end' }"
|
:popper="{ placement: 'bottom-end' }"
|
||||||
:options="grayOptions"
|
:options="grayOptions"
|
||||||
>
|
>
|
||||||
@@ -46,8 +44,8 @@
|
|||||||
{{ option.text }}
|
{{ option.text }}
|
||||||
</template>
|
</template>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</ClientOnly>
|
</div>
|
||||||
</div>
|
</ClientOnly>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -72,7 +70,7 @@ watch(grayCookie, (gray) => {
|
|||||||
const primaryOptions = computed(() => useWithout(appConfig.ui.colors, 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
|
const primaryOptions = computed(() => useWithout(appConfig.ui.colors, 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
|
||||||
const primary = computed({
|
const primary = computed({
|
||||||
get () {
|
get () {
|
||||||
return primaryOptions.value.find(option => option.value === primaryCookie.value)
|
return primaryOptions.value.find(option => option.value === primaryCookie.value) || primaryOptions.value.find(option => option.value === 'green')
|
||||||
},
|
},
|
||||||
set (option) {
|
set (option) {
|
||||||
primaryCookie.value = option.value
|
primaryCookie.value = option.value
|
||||||
@@ -82,7 +80,7 @@ const primary = computed({
|
|||||||
const grayOptions = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
|
const grayOptions = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
|
||||||
const gray = computed({
|
const gray = computed({
|
||||||
get () {
|
get () {
|
||||||
return grayOptions.value.find(option => option.value === grayCookie.value)
|
return grayOptions.value.find(option => option.value === grayCookie.value) || grayOptions.value.find(option => option.value === 'cool')
|
||||||
},
|
},
|
||||||
set (option) {
|
set (option) {
|
||||||
grayCookie.value = option.value
|
grayCookie.value = option.value
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<component
|
<component
|
||||||
:is="to ? NuxtLink : 'div'"
|
:is="to ? NuxtLink : 'div'"
|
||||||
:to="to"
|
:to="to"
|
||||||
class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm leading-6 my-5 last:mb-0 font-normal group relative"
|
class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm leading-6 my-5 last:mb-0 font-normal group relative prose-code:bg-white dark:prose-code:bg-gray-900"
|
||||||
:class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed hover:text-gray-800 dark:hover:text-gray-200' : '']"
|
:class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed hover:text-gray-800 dark:hover:text-gray-200' : '']"
|
||||||
>
|
>
|
||||||
<UIcon v-if="!!to" name="i-heroicons-link-20-solid" class="w-3 h-3 absolute right-2 top-2 text-gray-400 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400" />
|
<UIcon v-if="!!to" name="i-heroicons-link-20-solid" class="w-3 h-3 absolute right-2 top-2 text-gray-400 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400" />
|
||||||
|
|||||||
@@ -2,42 +2,46 @@
|
|||||||
<div>
|
<div>
|
||||||
<div v-if="propsToSelect.length" class="relative flex border border-gray-200 dark:border-gray-700 rounded-t-md overflow-hidden not-prose">
|
<div v-if="propsToSelect.length" class="relative flex border border-gray-200 dark:border-gray-700 rounded-t-md overflow-hidden not-prose">
|
||||||
<div v-for="prop in propsToSelect" :key="prop.name" class="flex flex-col gap-0.5 justify-between py-1.5 font-medium bg-gray-50 dark:bg-gray-800 border-r border-r-gray-200 dark:border-r-gray-700">
|
<div v-for="prop in propsToSelect" :key="prop.name" class="flex flex-col gap-0.5 justify-between py-1.5 font-medium bg-gray-50 dark:bg-gray-800 border-r border-r-gray-200 dark:border-r-gray-700">
|
||||||
<label :for="prop.name" class="block text-xs px-3 font-medium text-gray-400 dark:text-gray-500 -my-px">{{ prop.label }}</label>
|
<label :for="`prop-${prop.name}`" class="block text-xs px-3 font-medium text-gray-400 dark:text-gray-500 -my-px">{{ prop.label }}</label>
|
||||||
<UCheckbox
|
<UCheckbox
|
||||||
v-if="prop.type === 'boolean'"
|
v-if="prop.type === 'boolean'"
|
||||||
v-model="componentProps[prop.name]"
|
v-model="componentProps[prop.name]"
|
||||||
:name="prop.name"
|
:name="`prop-${prop.name}`"
|
||||||
variant="none"
|
variant="none"
|
||||||
class="justify-center"
|
:ui="{ wrapper: 'relative flex items-start justify-center' }"
|
||||||
/>
|
/>
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-else-if="prop.type === 'string' && prop.options.length"
|
v-else-if="prop.type === 'string' && prop.options.length"
|
||||||
v-model="componentProps[prop.name]"
|
v-model="componentProps[prop.name]"
|
||||||
:options="prop.options"
|
:options="prop.options"
|
||||||
:name="prop.name"
|
:name="`prop-${prop.name}`"
|
||||||
:label="componentProps[prop.name]"
|
|
||||||
variant="none"
|
variant="none"
|
||||||
class="inline-flex"
|
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md', wrapper: 'relative inline-flex' }"
|
||||||
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md' }"
|
class="!py-0"
|
||||||
:ui-select="{ custom: '!py-0' }"
|
|
||||||
:popper="{ strategy: 'fixed', placement: 'bottom-start' }"
|
:popper="{ strategy: 'fixed', placement: 'bottom-start' }"
|
||||||
/>
|
/>
|
||||||
<UInput
|
<UInput
|
||||||
v-else
|
v-else
|
||||||
:model-value="componentProps[prop.name]"
|
:model-value="componentProps[prop.name]"
|
||||||
:type="prop.type === 'number' ? 'number' : 'text'"
|
:type="prop.type === 'number' ? 'number' : 'text'"
|
||||||
:name="prop.name"
|
:name="`prop-${prop.name}`"
|
||||||
variant="none"
|
variant="none"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:ui="{ custom: '!py-0' }"
|
class="!py-0"
|
||||||
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
|
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex border border-b-0 border-gray-200 dark:border-gray-700 relative not-prose" :class="[{ 'p-4': padding }, propsToSelect.length ? 'border-t-0' : 'rounded-t-md', backgroundClass]">
|
<div class="flex border border-b-0 border-gray-200 dark:border-gray-700 relative not-prose" :class="[{ 'p-4': padding }, propsToSelect.length ? 'border-t-0' : 'rounded-t-md', backgroundClass, overflowClass]">
|
||||||
<component :is="name" v-model="vModel" v-bind="fullProps">
|
<component :is="name" v-model="vModel" v-bind="fullProps">
|
||||||
<ContentSlot v-if="$slots.default" :use="$slots.default" />
|
<ContentSlot v-if="$slots.default" :use="$slots.default" />
|
||||||
|
|
||||||
|
<template v-for="slot in Object.keys(slots || {})" :key="slot" #[slot]>
|
||||||
|
<ClientOnly>
|
||||||
|
<ContentSlot v-if="$slots[slot]" :use="$slots[slot]" />
|
||||||
|
</ClientOnly>
|
||||||
|
</template>
|
||||||
</component>
|
</component>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -67,6 +71,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
slots: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
baseProps: {
|
baseProps: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
@@ -86,6 +94,10 @@ const props = defineProps({
|
|||||||
backgroundClass: {
|
backgroundClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'bg-white dark:bg-gray-900'
|
default: 'bg-white dark:bg-gray-900'
|
||||||
|
},
|
||||||
|
overflowClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -107,7 +119,7 @@ const meta = await fetchComponentMeta(name)
|
|||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
const ui = computed(() => ({ ...appConfig.ui[camelName], ...props.ui }))
|
const ui = computed(() => ({ ...appConfig.ui[camelName], ...props.ui }))
|
||||||
|
|
||||||
const fullProps = computed(() => ({ ...props.baseProps, ...componentProps }))
|
const fullProps = computed(() => ({ ...baseProps, ...componentProps }))
|
||||||
const vModel = computed({
|
const vModel = computed({
|
||||||
get: () => baseProps.modelValue,
|
get: () => baseProps.modelValue,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
@@ -150,7 +162,14 @@ const code = computed(() => {
|
|||||||
|
|
||||||
code += ` ${(prop?.type === 'boolean' && value !== true) || typeof value === 'object' ? ':' : ''}${key === 'modelValue' ? 'value' : useKebabCase(key)}${prop?.type === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}`
|
code += ` ${(prop?.type === 'boolean' && value !== true) || typeof value === 'object' ? ':' : ''}${key === 'modelValue' ? 'value' : useKebabCase(key)}${prop?.type === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}`
|
||||||
}
|
}
|
||||||
if (props.code) {
|
|
||||||
|
if (props.slots) {
|
||||||
|
code += `>
|
||||||
|
${Object.entries(props.slots).map(([key, value]) => `<template #${key}>
|
||||||
|
${value}
|
||||||
|
</template>`).join('\n ')}
|
||||||
|
</${name}>`
|
||||||
|
} else if (props.code) {
|
||||||
const lineBreaks = (props.code.match(/\n/g) || []).length
|
const lineBreaks = (props.code.match(/\n/g) || []).length
|
||||||
if (lineBreaks > 1) {
|
if (lineBreaks > 1) {
|
||||||
code += `>
|
code += `>
|
||||||
@@ -183,7 +202,7 @@ function renderObject (obj: any) {
|
|||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify(componentProps)}`, () => transformContent('content:_markdown.md', code.value, {
|
const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify(props)}`, () => transformContent('content:_markdown.md', code.value, {
|
||||||
highlight: {
|
highlight: {
|
||||||
theme: {
|
theme: {
|
||||||
light: 'material-lighter',
|
light: 'material-lighter',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="[&>div>pre]:!rounded-t-none">
|
<div class="[&>div>pre]:!rounded-t-none">
|
||||||
<div class="flex border border-gray-200 dark:border-gray-700 relative not-prose rounded-t-md" :class="[{ 'p-4': padding, 'rounded-b-md': !$slots.code, 'border-b-0': !!$slots.code }, backgroundClass]">
|
<div class="flex border border-gray-200 dark:border-gray-700 relative not-prose rounded-t-md" :class="[{ 'p-4': padding, 'rounded-b-md': !$slots.code, 'border-b-0': !!$slots.code }, backgroundClass, overflowClass]">
|
||||||
<ContentSlot v-if="$slots.default" :use="$slots.default" />
|
<ContentSlot v-if="$slots.default" :use="$slots.default" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -17,6 +17,10 @@ defineProps({
|
|||||||
backgroundClass: {
|
backgroundClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'bg-white dark:bg-gray-900'
|
default: 'bg-white dark:bg-gray-900'
|
||||||
|
},
|
||||||
|
overflowClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
7
docs/components/content/examples/CheckboxExample.vue
Normal file
7
docs/components/content/examples/CheckboxExample.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup>
|
||||||
|
const selected = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UCheckbox v-model="selected" name="notifications" label="Notifications" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<UCommandPalette>
|
||||||
|
<template #empty-state>
|
||||||
|
<div class="flex flex-col items-center justify-center py-6 gap-3">
|
||||||
|
<span class="italic text-sm">Nothing here!</span>
|
||||||
|
<UButton label="Add item" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCommandPalette>
|
||||||
|
</template>
|
||||||
16
docs/components/content/examples/DropdownExampleMode.vue
Normal file
16
docs/components/content/examples/DropdownExampleMode.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup>
|
||||||
|
const items = [
|
||||||
|
[{
|
||||||
|
label: 'Profile',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDropdown :items="items" mode="hover" :popper="{ placement: 'bottom-start' }">
|
||||||
|
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||||
|
</UDropdown>
|
||||||
|
</template>
|
||||||
7
docs/components/content/examples/InputExample.vue
Normal file
7
docs/components/content/examples/InputExample.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup>
|
||||||
|
const value = ref('')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UInput v-model="value" />
|
||||||
|
</template>
|
||||||
18
docs/components/content/examples/InputExampleClearable.vue
Normal file
18
docs/components/content/examples/InputExampleClearable.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<UInput v-model="q" name="q" placeholder="Search..." icon="i-heroicons-magnifying-glass-20-solid" :ui="{ icon: { trailing: { pointer: '' } } }">
|
||||||
|
<template #trailing>
|
||||||
|
<UButton
|
||||||
|
v-show="q !== ''"
|
||||||
|
color="gray"
|
||||||
|
variant="link"
|
||||||
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
|
:padded="false"
|
||||||
|
@click="q = ''"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const q = ref('')
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<script setup>
|
||||||
|
const page = ref(1)
|
||||||
|
const items = ref(Array(55))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UPagination v-model="page" :page-count="5" :total="items.length" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup>
|
||||||
|
const page = ref(1)
|
||||||
|
const items = ref(Array(55))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UPagination v-model="page" :total="items.length" :ui="{ rounded: 'first-of-type:rounded-l-md last-of-type:rounded-r-md' }">
|
||||||
|
<template #prev="{ onClick }">
|
||||||
|
<UTooltip text="Previous page">
|
||||||
|
<UButton icon="i-heroicons-arrow-small-left-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="mr-2" @click="onClick" />
|
||||||
|
</UTooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #next="{ onClick }">
|
||||||
|
<UTooltip text="Next page">
|
||||||
|
<UButton icon="i-heroicons-arrow-small-right-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="ml-2" @click="onClick" />
|
||||||
|
</UTooltip>
|
||||||
|
</template>
|
||||||
|
</UPagination>
|
||||||
|
</template>
|
||||||
23
docs/components/content/examples/RadioExample.vue
Normal file
23
docs/components/content/examples/RadioExample.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup>
|
||||||
|
const methods = [{
|
||||||
|
name: 'email',
|
||||||
|
value: 'email',
|
||||||
|
label: 'Email'
|
||||||
|
}, {
|
||||||
|
name: 'sms',
|
||||||
|
value: 'sms',
|
||||||
|
label: 'Phone (SMS)'
|
||||||
|
}, {
|
||||||
|
name: 'push',
|
||||||
|
value: 'push',
|
||||||
|
label: 'Push notification'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const selected = ref('sms')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<URadio v-for="method of methods" :key="method.name" v-model="selected" v-bind="method" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
9
docs/components/content/examples/SelectExample.vue
Normal file
9
docs/components/content/examples/SelectExample.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script setup>
|
||||||
|
const countries = ['United States', 'Canada', 'Mexico']
|
||||||
|
|
||||||
|
const country = ref(countries[0])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelect v-model="country" :options="countries" />
|
||||||
|
</template>
|
||||||
18
docs/components/content/examples/SelectExampleObjects.vue
Normal file
18
docs/components/content/examples/SelectExampleObjects.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
const countries = [{
|
||||||
|
name: 'United States',
|
||||||
|
value: 'US'
|
||||||
|
}, {
|
||||||
|
name: 'Canada',
|
||||||
|
value: 'CA'
|
||||||
|
}, {
|
||||||
|
name: 'Mexico',
|
||||||
|
value: 'MX'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const country = ref('CA')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelect v-model="country" :options="countries" option-attribute="name" />
|
||||||
|
</template>
|
||||||
@@ -6,10 +6,10 @@ const selected = ref(people[3])
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
|
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
|
||||||
<UButton>
|
<UButton color="gray">
|
||||||
{{ selected }}
|
{{ selected }}
|
||||||
|
|
||||||
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
|
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="[open && 'transform rotate-90']" />
|
||||||
</UButton>
|
</UButton>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,10 +5,5 @@ const selected = ref([])
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<USelectMenu v-model="selected" :options="people" multiple>
|
<USelectMenu v-model="selected" :options="people" multiple placeholder="Select people" />
|
||||||
<template #label>
|
|
||||||
<span v-if="selected.length" class="font-medium truncate">{{ selected.join(', ') }}</span>
|
|
||||||
<span v-else class="block truncate text-gray-400 dark:text-gray-500">Select people</span>
|
|
||||||
</template>
|
|
||||||
</USelectMenu>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup>
|
||||||
|
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||||
|
|
||||||
|
const selected = ref([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelectMenu v-model="selected" :options="people" multiple>
|
||||||
|
<template #label>
|
||||||
|
<span v-if="selected.length" class="truncate">{{ selected.join(', ') }}</span>
|
||||||
|
<span v-else>Select people</span>
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
</template>
|
||||||
43
docs/components/content/examples/TableExampleBasic.vue
Normal file
43
docs/components/content/examples/TableExampleBasic.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
const people = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
title: 'Designer',
|
||||||
|
email: 'courtney.henry@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Tom Cook',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: 'tom.cook@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Whitney Francis',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: 'whitney.francis@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Leonard Krasner',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: 'leonard.krasner@example.com',
|
||||||
|
role: 'Owner'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Floyd Miles',
|
||||||
|
title: 'Principal Designer',
|
||||||
|
email: 'floyd.miles@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :rows="people" />
|
||||||
|
</template>
|
||||||
59
docs/components/content/examples/TableExampleColumns.vue
Normal file
59
docs/components/content/examples/TableExampleColumns.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<script setup>
|
||||||
|
const columns = [{
|
||||||
|
key: 'id',
|
||||||
|
label: 'ID'
|
||||||
|
}, {
|
||||||
|
key: 'name',
|
||||||
|
label: 'User name'
|
||||||
|
}, {
|
||||||
|
key: 'title',
|
||||||
|
label: 'Job position'
|
||||||
|
}, {
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email'
|
||||||
|
}, {
|
||||||
|
key: 'role'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const people = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
title: 'Designer',
|
||||||
|
email: 'courtney.henry@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Tom Cook',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: 'tom.cook@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Whitney Francis',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: 'whitney.francis@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Leonard Krasner',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: 'leonard.krasner@example.com',
|
||||||
|
role: 'Owner'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Floyd Miles',
|
||||||
|
title: 'Principal Designer',
|
||||||
|
email: 'floyd.miles@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :columns="columns" :rows="people" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup>
|
||||||
|
const columns = [{
|
||||||
|
key: 'id',
|
||||||
|
label: 'ID'
|
||||||
|
}, {
|
||||||
|
key: 'name',
|
||||||
|
label: 'Name'
|
||||||
|
}, {
|
||||||
|
key: 'title',
|
||||||
|
label: 'Title'
|
||||||
|
}, {
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email'
|
||||||
|
}, {
|
||||||
|
key: 'role',
|
||||||
|
label: 'Role'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const selectedColumns = ref([...columns])
|
||||||
|
|
||||||
|
const people = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
title: 'Designer',
|
||||||
|
email: 'courtney.henry@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Tom Cook',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: 'tom.cook@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Whitney Francis',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: 'whitney.francis@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Leonard Krasner',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: 'leonard.krasner@example.com',
|
||||||
|
role: 'Owner'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Floyd Miles',
|
||||||
|
title: 'Principal Designer',
|
||||||
|
email: 'floyd.miles@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex px-3 py-3.5 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<USelectMenu v-model="selectedColumns" :options="columns" multiple placeholder="Columns" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UTable :columns="selectedColumns" :rows="people" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup>
|
||||||
|
const columns = [{
|
||||||
|
key: 'id',
|
||||||
|
label: 'ID'
|
||||||
|
}, {
|
||||||
|
key: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
sortable: true
|
||||||
|
}, {
|
||||||
|
key: 'title',
|
||||||
|
label: 'Title',
|
||||||
|
sortable: true
|
||||||
|
}, {
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email',
|
||||||
|
sortable: true,
|
||||||
|
direction: 'desc'
|
||||||
|
}, {
|
||||||
|
key: 'role',
|
||||||
|
label: 'Role'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const people = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
title: 'Designer',
|
||||||
|
email: 'courtney.henry@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Tom Cook',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: 'tom.cook@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Whitney Francis',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: 'whitney.francis@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Leonard Krasner',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: 'leonard.krasner@example.com',
|
||||||
|
role: 'Owner'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Floyd Miles',
|
||||||
|
title: 'Principal Designer',
|
||||||
|
email: 'floyd.miles@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :columns="columns" :rows="people" :sort="{ column: 'title' }" />
|
||||||
|
</template>
|
||||||
30
docs/components/content/examples/TableExampleEmptySlot.vue
Normal file
30
docs/components/content/examples/TableExampleEmptySlot.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
const columns = [{
|
||||||
|
key: 'name',
|
||||||
|
label: 'Name'
|
||||||
|
}, {
|
||||||
|
key: 'title',
|
||||||
|
label: 'Title'
|
||||||
|
}, {
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email'
|
||||||
|
}, {
|
||||||
|
key: 'role',
|
||||||
|
label: 'Role'
|
||||||
|
}, {
|
||||||
|
key: 'actions'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const people = []
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :rows="people" :columns="columns">
|
||||||
|
<template #empty-state>
|
||||||
|
<div class="flex flex-col items-center justify-center py-6 gap-3">
|
||||||
|
<span class="italic text-sm">No one here!</span>
|
||||||
|
<UButton label="Add people" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</template>
|
||||||
86
docs/components/content/examples/TableExampleLoadingSlot.vue
Normal file
86
docs/components/content/examples/TableExampleLoadingSlot.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<script setup>
|
||||||
|
const columns = [{
|
||||||
|
key: 'name',
|
||||||
|
label: 'Name'
|
||||||
|
}, {
|
||||||
|
key: 'title',
|
||||||
|
label: 'Title'
|
||||||
|
}, {
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email'
|
||||||
|
}, {
|
||||||
|
key: 'role',
|
||||||
|
label: 'Role'
|
||||||
|
}, {
|
||||||
|
key: 'actions'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const people = []
|
||||||
|
|
||||||
|
const pending = ref(true)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :rows="people" :columns="columns" :loading="pending">
|
||||||
|
<template #loading-state>
|
||||||
|
<div class="flex items-center justify-center h-32">
|
||||||
|
<i class="loader --6" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* https://codepen.io/jenning/pen/YzNmzaV */
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
--color: rgb(var(--color-primary-400));
|
||||||
|
--size-mid: 6vmin;
|
||||||
|
--size-dot: 1.5vmin;
|
||||||
|
--size-bar: 0.4vmin;
|
||||||
|
--size-square: 3vmin;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 50%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader::before,
|
||||||
|
.loader::after {
|
||||||
|
content: '';
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
loader --6
|
||||||
|
**/
|
||||||
|
.loader.--6::before {
|
||||||
|
width: var(--size-square);
|
||||||
|
height: var(--size-square);
|
||||||
|
background-color: var(--color);
|
||||||
|
top: calc(50% - var(--size-square));
|
||||||
|
left: calc(50% - var(--size-square));
|
||||||
|
animation: loader-6 2.4s cubic-bezier(0, 0, 0.24, 1.21) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loader-6 {
|
||||||
|
0%, 100% {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateX(100%) translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
98
docs/components/content/examples/TableExamplePaginable.vue
Normal file
98
docs/components/content/examples/TableExamplePaginable.vue
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup>
|
||||||
|
const people = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
title: 'Designer',
|
||||||
|
email: 'courtney.henry@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Tom Cook',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: 'tom.cook@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Whitney Francis',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: 'whitney.francis@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Leonard Krasner',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: 'leonard.krasner@example.com',
|
||||||
|
role: 'Owner'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Floyd Miles',
|
||||||
|
title: 'Principal Designer',
|
||||||
|
email: 'floyd.miles@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 7,
|
||||||
|
name: 'Emily Selman',
|
||||||
|
title: 'VP, User Experience',
|
||||||
|
email: '',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 8,
|
||||||
|
name: 'Kristin Watson',
|
||||||
|
title: 'VP, Human Resources',
|
||||||
|
email: '',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 9,
|
||||||
|
name: 'Emma Watson',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: '',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 10,
|
||||||
|
name: 'John Doe',
|
||||||
|
title: 'Designer',
|
||||||
|
email: '',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 11,
|
||||||
|
name: 'Jane Doe',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: '',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 12,
|
||||||
|
name: 'John Smith',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: '',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 13,
|
||||||
|
name: 'Jane Smith',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: '',
|
||||||
|
role: 'Owner'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const pageCount = 5
|
||||||
|
|
||||||
|
const rows = computed(() => {
|
||||||
|
return people.slice((page.value - 1) * pageCount, (page.value) * pageCount)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<UTable :rows="rows" />
|
||||||
|
|
||||||
|
<div class="flex justify-end px-3 py-3.5 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<UPagination v-model="page" :page-count="pageCount" :total="people.length" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
80
docs/components/content/examples/TableExampleSearchable.vue
Normal file
80
docs/components/content/examples/TableExampleSearchable.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<script setup>
|
||||||
|
const columns = [{
|
||||||
|
key: 'id',
|
||||||
|
label: 'ID'
|
||||||
|
}, {
|
||||||
|
key: 'name',
|
||||||
|
label: 'Name'
|
||||||
|
}, {
|
||||||
|
key: 'title',
|
||||||
|
label: 'Title'
|
||||||
|
}, {
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email'
|
||||||
|
}, {
|
||||||
|
key: 'role',
|
||||||
|
label: 'Role'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const people = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
title: 'Designer',
|
||||||
|
email: 'courtney.henry@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Tom Cook',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: 'tom.cook@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Whitney Francis',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: 'whitney.francis@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Leonard Krasner',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: 'leonard.krasner@example.com',
|
||||||
|
role: 'Owner'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Floyd Miles',
|
||||||
|
title: 'Principal Designer',
|
||||||
|
email: 'floyd.miles@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const q = ref('')
|
||||||
|
|
||||||
|
const filteredRows = computed(() => {
|
||||||
|
if (!q.value) {
|
||||||
|
return people
|
||||||
|
}
|
||||||
|
|
||||||
|
return people.filter((person) => {
|
||||||
|
return Object.values(person).some((value) => {
|
||||||
|
return String(value).toLowerCase().includes(q.value.toLowerCase())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex px-3 py-3.5 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<UInput v-model="q" placeholder="Filter people..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UTable :rows="filteredRows" :columns="columns" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
45
docs/components/content/examples/TableExampleSelectable.vue
Normal file
45
docs/components/content/examples/TableExampleSelectable.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup>
|
||||||
|
const people = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
title: 'Designer',
|
||||||
|
email: 'courtney.henry@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Tom Cook',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: 'tom.cook@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Whitney Francis',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: 'whitney.francis@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Leonard Krasner',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: 'leonard.krasner@example.com',
|
||||||
|
role: 'Owner'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Floyd Miles',
|
||||||
|
title: 'Principal Designer',
|
||||||
|
email: 'floyd.miles@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const selected = ref([people[1]])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable v-model="selected" :rows="people" />
|
||||||
|
</template>
|
||||||
91
docs/components/content/examples/TableExampleSlots.vue
Normal file
91
docs/components/content/examples/TableExampleSlots.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<script setup>
|
||||||
|
const columns = [{
|
||||||
|
key: 'name',
|
||||||
|
label: 'Name'
|
||||||
|
}, {
|
||||||
|
key: 'title',
|
||||||
|
label: 'Title'
|
||||||
|
}, {
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email'
|
||||||
|
}, {
|
||||||
|
key: 'role',
|
||||||
|
label: 'Role'
|
||||||
|
}, {
|
||||||
|
key: 'actions'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const people = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
title: 'Designer',
|
||||||
|
email: 'courtney.henry@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Tom Cook',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: 'tom.cook@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Whitney Francis',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: 'whitney.francis@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Leonard Krasner',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: 'leonard.krasner@example.com',
|
||||||
|
role: 'Owner'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Floyd Miles',
|
||||||
|
title: 'Principal Designer',
|
||||||
|
email: 'floyd.miles@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const items = (row) => [
|
||||||
|
[{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: 'i-heroicons-pencil-square-20-solid',
|
||||||
|
click: () => console.log('Edit', row.id)
|
||||||
|
}, {
|
||||||
|
label: 'Duplicate',
|
||||||
|
icon: 'i-heroicons-document-duplicate-20-solid'
|
||||||
|
}], [{
|
||||||
|
label: 'Archive',
|
||||||
|
icon: 'i-heroicons-archive-box-20-solid'
|
||||||
|
}, {
|
||||||
|
label: 'Move',
|
||||||
|
icon: 'i-heroicons-arrow-right-circle-20-solid'
|
||||||
|
}], [{
|
||||||
|
label: 'Delete',
|
||||||
|
icon: 'i-heroicons-trash-20-solid'
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
|
||||||
|
const selected = ref([people[1]])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable v-model="selected" :rows="people" :columns="columns">
|
||||||
|
<template #name-data="{ row }">
|
||||||
|
<span :class="[selected.find(person => person.id === row.id) && 'text-primary-500 dark:text-primary-400']">{{ row.name }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions-data="{ row }">
|
||||||
|
<UDropdown :items="items(row)">
|
||||||
|
<UButton color="gray" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
|
||||||
|
</UDropdown>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</template>
|
||||||
7
docs/components/content/examples/TextareaExample.vue
Normal file
7
docs/components/content/examples/TextareaExample.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup>
|
||||||
|
const value = ref('')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTextarea v-model="value" />
|
||||||
|
</template>
|
||||||
7
docs/components/content/examples/ToggleExample.vue
Normal file
7
docs/components/content/examples/ToggleExample.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup>
|
||||||
|
const selected = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UToggle v-model="selected" />
|
||||||
|
</template>
|
||||||
@@ -17,8 +17,8 @@ const groups = computed(() => navigation.value.map(item => ({
|
|||||||
}))
|
}))
|
||||||
})))
|
})))
|
||||||
|
|
||||||
const close = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-x-mark', color: 'black', variant: 'ghost', size: 'lg', padded: false }) : null)
|
const closeButton = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-x-mark', color: 'black', variant: 'ghost', size: 'lg', padded: false }) : null)
|
||||||
const empty = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-magnifying-glass', queryLabel: 'No results' }) : ({ icon: '', label: 'No recent searches' }))
|
const emptyState = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-magnifying-glass', queryLabel: 'No results' }) : ({ icon: '', label: 'No recent searches' }))
|
||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
wrapper: 'flex flex-col flex-1 min-h-0 bg-gray-50 dark:bg-gray-800',
|
wrapper: 'flex flex-col flex-1 min-h-0 bg-gray-50 dark:bg-gray-800',
|
||||||
@@ -50,7 +50,7 @@ const ui = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
empty: {
|
emptyState: {
|
||||||
wrapper: 'flex flex-col items-center justify-center flex-1 py-9',
|
wrapper: 'flex flex-col items-center justify-center flex-1 py-9',
|
||||||
label: 'text-sm text-center text-gray-500 dark:text-gray-400',
|
label: 'text-sm text-center text-gray-500 dark:text-gray-400',
|
||||||
queryLabel: 'text-lg text-center text-gray-900 dark:text-white',
|
queryLabel: 'text-lg text-center text-gray-900 dark:text-white',
|
||||||
@@ -64,8 +64,8 @@ const ui = {
|
|||||||
ref="commandPaletteRef"
|
ref="commandPaletteRef"
|
||||||
:groups="groups"
|
:groups="groups"
|
||||||
:ui="ui"
|
:ui="ui"
|
||||||
:close="close"
|
:close-button="closeButton"
|
||||||
:empty="empty"
|
:empty-state="emptyState"
|
||||||
:autoselect="false"
|
:autoselect="false"
|
||||||
command-attribute="title"
|
command-attribute="title"
|
||||||
:fuse="{
|
:fuse="{
|
||||||
|
|||||||
21
docs/components/content/themes/PaginationThemeRounded.vue
Normal file
21
docs/components/content/themes/PaginationThemeRounded.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup>
|
||||||
|
const page = ref(1)
|
||||||
|
const items = ref(Array(55))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UPagination
|
||||||
|
v-model="page"
|
||||||
|
:total="items.length"
|
||||||
|
:ui="{
|
||||||
|
wrapper: 'flex items-center gap-1',
|
||||||
|
rounded: 'rounded-full min-w-[32px] justify-center'
|
||||||
|
}"
|
||||||
|
:prev-button="null"
|
||||||
|
:next-button="{
|
||||||
|
icon: 'i-heroicons-arrow-small-right-20-solid',
|
||||||
|
color: 'primary',
|
||||||
|
variant: 'outline'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -10,15 +10,21 @@
|
|||||||
class="mt-1"
|
class="mt-1"
|
||||||
:ui="{
|
:ui="{
|
||||||
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
|
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
|
||||||
base: 'group block border-l -ml-px lg:leading-6',
|
base: 'group block border-l -ml-px lg:leading-6 flex items-center gap-2',
|
||||||
padding: 'pl-4',
|
padding: 'pl-4',
|
||||||
rounded: '',
|
rounded: '',
|
||||||
font: '',
|
font: '',
|
||||||
ring: '',
|
ring: '',
|
||||||
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
|
active: 'text-primary-500 dark:text-primary-400 border-current',
|
||||||
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
|
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
}"
|
}"
|
||||||
/>
|
>
|
||||||
|
<template #badge="{ link }">
|
||||||
|
<UBadge v-if="link.badge" size="xs" :ui="{ rounded: 'rounded-full' }">
|
||||||
|
{{ link.badge }}
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UVerticalNavigation>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -29,6 +35,6 @@ import type { NavItem } from '@nuxt/content/dist/runtime/types'
|
|||||||
const { navigation } = useContent() as { navigation: NavItem[] }
|
const { navigation } = useContent() as { navigation: NavItem[] }
|
||||||
|
|
||||||
function mapContentLinks (links: NavItem[]) {
|
function mapContentLinks (links: NavItem[]) {
|
||||||
return links?.map(link => ({ label: link.title, icon: link.icon, to: link._path })) || []
|
return links?.map(link => ({ label: link.title, icon: link.icon, to: link._path, badge: link.badge })) || []
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{{ page.title }}
|
{{ page.title }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 mt-4 lg:mt-0">
|
<div v-if="page.headlessui || page.github" class="flex items-center gap-2 mt-4 lg:mt-0">
|
||||||
<UButton
|
<UButton
|
||||||
v-if="page.headlessui"
|
v-if="page.headlessui"
|
||||||
:label="page.headlessui.label"
|
:label="page.headlessui.label"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
label="GitHub"
|
label="GitHub"
|
||||||
icon="i-simple-icons-github"
|
icon="i-simple-icons-github"
|
||||||
color="white"
|
color="white"
|
||||||
:to="`https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/${page._dir}/${page.title.replace(' ', '')}.vue`"
|
:to="`https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/${page._dir}/${page.title.replace(' ', '')}${page.github.suffix || '.vue'}`"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ As this module installs [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/) a
|
|||||||
| `prefix` | `u` | Define the prefix of the imported components. |
|
| `prefix` | `u` | Define the prefix of the imported components. |
|
||||||
| `global` | `false` | Expose components globally. |
|
| `global` | `false` | Expose components globally. |
|
||||||
| `icons` | `['heroicons']` | Icon collections to load. |
|
| `icons` | `['heroicons']` | Icon collections to load. |
|
||||||
|
| `safelistColors` | `['primary']` | Force safelisting of colors. |
|
||||||
|
|
||||||
## Edge
|
## Edge
|
||||||
|
|
||||||
|
|||||||
@@ -33,13 +33,45 @@ Likewise, you can't define a `primary` color in your `tailwind.config.ts` as it
|
|||||||
We'd advise you to use those colors in your components and pages, e.g. `text-primary-500 dark:text-primary-400`, `bg-gray-100 dark:bg-gray-900`, etc. so your app automatically adapts when changing your `app.config.ts`.
|
We'd advise you to use those colors in your components and pages, e.g. `text-primary-500 dark:text-primary-400`, `bg-gray-100 dark:bg-gray-900`, etc. so your app automatically adapts when changing your `app.config.ts`.
|
||||||
::
|
::
|
||||||
|
|
||||||
Components that have a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style) and [Notification](/overlays/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors.
|
Components having a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style), [Input](/elements/input#style) (inherited in [Select](/forms/select) and [SelectMenu](/forms/select-menu)) and [Notification](/overlays/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors.
|
||||||
|
|
||||||
|
Variant classes of those components are defined with a syntax like `bg-{color}-500 dark:bg-{color}-400` so they can be used with any color. However, this means that Tailwind will not find those classes and therefore will not generate the corresponding CSS.
|
||||||
|
|
||||||
|
The module uses the [Tailwind CSS safelist](https://tailwindcss.com/docs/content-configuration#safelisting-classes) feature to force the generation of all the classes for the `primary` color **only** as it is the default color for all the components.
|
||||||
|
|
||||||
|
Then, the module will automatically detect when you use one of those components with a color and will safelist it for you. This means that if you use a `red` color for a Button component, the `red` color classes will be safelisted for the Button component only. This will allow to keep the CSS bundle size as small as possible.
|
||||||
|
|
||||||
|
There is one case where you would want to force the safelisting of a color. For example, if you've set the default color of the Button component to `orange` in your `app.config.ts`.
|
||||||
|
|
||||||
|
```ts [app.config.ts]
|
||||||
|
export default defineAppConfig({
|
||||||
|
ui: {
|
||||||
|
button: {
|
||||||
|
default: {
|
||||||
|
color: 'orange'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This will apply the orange color when using a default `<UButton />`. You'll need to safelist this color manually in your `nuxt.config.ts` ui options as we won't be able to detect it automatically. You can do so through the `safelistColors` option which defaults to `['primary']`.
|
||||||
|
|
||||||
|
```ts [nuxt.config.ts]
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
ui: {
|
||||||
|
safelistColors: ['orange']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This can also happen when you bind a dynamic color to a component: `<UBadge :color="color" />`, `<UAvatar :chip-color="statuses[user.status]" />`, etc. In this case, you'll need to safelist the possible color values manually as well.
|
||||||
|
|
||||||
## Dark mode
|
## Dark mode
|
||||||
|
|
||||||
All the components are styled with dark mode in mind.
|
All the components are styled with dark mode in mind.
|
||||||
|
|
||||||
Thanks to [Tailwind CSS dark mode](https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually) `class` strategy and the [@nuxtjs/color-mode](https://github.com/nuxt-modules/color-mode) module, you literally have nothing to do.
|
Thanks to [Tailwind CSS dark mode](https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually) class strategy and the [@nuxtjs/color-mode](https://github.com/nuxt-modules/color-mode) module, you literally have nothing to do.
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
@@ -166,6 +198,7 @@ export default defineAppConfig({
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
default: {
|
default: {
|
||||||
|
loadingIcon: 'i-octicon-sync-24',
|
||||||
trailingIcon: 'i-octicon-chevron-down-24'
|
trailingIcon: 'i-octicon-chevron-down-24'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -176,7 +209,7 @@ export default defineAppConfig({
|
|||||||
},
|
},
|
||||||
notification: {
|
notification: {
|
||||||
default: {
|
default: {
|
||||||
close: {
|
closeButton: {
|
||||||
icon: 'i-octicon-x-24'
|
icon: 'i-octicon-x-24'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,10 +219,22 @@ export default defineAppConfig({
|
|||||||
icon: 'i-octicon-search-24',
|
icon: 'i-octicon-search-24',
|
||||||
loadingIcon: 'i-octicon-sync-24',
|
loadingIcon: 'i-octicon-sync-24',
|
||||||
selectedIcon: 'i-octicon-check-24',
|
selectedIcon: 'i-octicon-check-24',
|
||||||
empty: {
|
emptyState: {
|
||||||
icon: 'i-octicon-search-24'
|
icon: 'i-octicon-search-24'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
default: {
|
||||||
|
sortAscIcon: 'i-octicon-sort-asc-24',
|
||||||
|
sortDescIcon: 'i-octicon-sort-desc-24',
|
||||||
|
sortButton: {
|
||||||
|
icon: 'i-octicon-arrow-switch-24'
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
icon: 'i-octicon-database-24'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ If there's an `alt` prop initials will be displayed on top of the background, cu
|
|||||||
---
|
---
|
||||||
props:
|
props:
|
||||||
alt: 'Benjamin Canac'
|
alt: 'Benjamin Canac'
|
||||||
|
size: 'sm'
|
||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|||||||
@@ -275,6 +275,48 @@ code: |
|
|||||||
:u-button{icon="i-heroicons-chevron-down-20-solid" color="gray"}
|
:u-button{icon="i-heroicons-chevron-down-20-solid" color="gray"}
|
||||||
::
|
::
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
### `leading`
|
||||||
|
|
||||||
|
Use the `#leading` slot to set the content of the leading icon.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
slots:
|
||||||
|
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
|
||||||
|
baseProps:
|
||||||
|
color: 'gray'
|
||||||
|
props:
|
||||||
|
label: Button
|
||||||
|
color: 'gray'
|
||||||
|
excludedProps:
|
||||||
|
- color
|
||||||
|
---
|
||||||
|
|
||||||
|
#leading
|
||||||
|
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
|
||||||
|
::
|
||||||
|
|
||||||
|
### `trailing`
|
||||||
|
|
||||||
|
Use the `#trailing` slot to set the content of the trailing icon.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
slots:
|
||||||
|
trailing: <UIcon name="i-heroicons-arrow-right-20-solid" />
|
||||||
|
props:
|
||||||
|
label: Button
|
||||||
|
color: 'gray'
|
||||||
|
excludedProps:
|
||||||
|
- color
|
||||||
|
---
|
||||||
|
|
||||||
|
#trailing
|
||||||
|
:u-icon{name="i-heroicons-arrow-right-20-solid"}
|
||||||
|
::
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
:component-props
|
:component-props
|
||||||
|
|||||||
@@ -8,9 +8,20 @@ headlessui:
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
Pass an array of arrays to the `items` prop of the Dropdown component. Each array represents a group of items. Each item can have the following properties:
|
||||||
|
|
||||||
|
- `label` - The label of the item.
|
||||||
|
- `icon` - The icon of the item.
|
||||||
|
- `avatar` - The avatar of the item. You can pass all the props of the [Avatar](/elements/avatar) component.
|
||||||
|
- `shortcuts` - The shortcuts of the item.
|
||||||
|
- `disabled` - Whether the item is disabled.
|
||||||
|
- `click` - The click handler of the item.
|
||||||
|
|
||||||
|
You can also pass properties from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
|
||||||
|
|
||||||
::component-example
|
::component-example
|
||||||
#default
|
#default
|
||||||
:dropdown-example
|
:dropdown-example-basic
|
||||||
|
|
||||||
#code
|
#code
|
||||||
```vue
|
```vue
|
||||||
@@ -55,6 +66,35 @@ const items = [
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Mode
|
||||||
|
|
||||||
|
Use the `mode` prop to switch between `click` and `hover` modes.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:dropdown-example-mode
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const items = [
|
||||||
|
[{
|
||||||
|
label: 'Profile',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDropdown :items="items" mode="hover" :popper="{ placement: 'bottom-start' }">
|
||||||
|
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
|
||||||
|
</UDropdown>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
:component-props
|
:component-props
|
||||||
|
|||||||
@@ -5,11 +5,22 @@ description: Display an input field.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
::component-card
|
Use a `v-model` to make the Input reactive.
|
||||||
---
|
|
||||||
baseProps:
|
::component-example
|
||||||
name: 'input'
|
#default
|
||||||
---
|
:input-example
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const value = ref('')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UInput v-model="value" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Style
|
### Style
|
||||||
@@ -142,6 +153,75 @@ excludedProps:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
### `leading`
|
||||||
|
|
||||||
|
Use the `#leading` slot to set the content of the leading icon.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
slots:
|
||||||
|
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
|
||||||
|
baseProps:
|
||||||
|
name: 'input'
|
||||||
|
placeholder: 'Search...'
|
||||||
|
---
|
||||||
|
|
||||||
|
#leading
|
||||||
|
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
|
||||||
|
::
|
||||||
|
|
||||||
|
### `trailing`
|
||||||
|
|
||||||
|
Use the `#trailing` slot to set the content of the trailing icon.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
slots:
|
||||||
|
trailing: <span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
|
||||||
|
baseProps:
|
||||||
|
name: 'input'
|
||||||
|
placeholder: 'Search...'
|
||||||
|
---
|
||||||
|
|
||||||
|
#trailing
|
||||||
|
[EUR]{class="text-gray-500 dark:text-gray-400 text-xs"}
|
||||||
|
::
|
||||||
|
|
||||||
|
You can for example create a clearable Input by injecting a [Button](/elements/button) in the `trailing` slot that displays when some text is entered.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:input-example-clearable
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<UInput v-model="q" name="q" placeholder="Search..." icon="i-heroicons-magnifying-glass-20-solid" :ui="{ icon: { trailing: { pointer: '' } } }">
|
||||||
|
<template #trailing>
|
||||||
|
<UButton
|
||||||
|
v-show="q !== ''"
|
||||||
|
color="gray"
|
||||||
|
variant="link"
|
||||||
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
|
:padded="false"
|
||||||
|
@click="q = ''"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const q = ref('')
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
::alert{icon="i-heroicons-exclamation-triangle-20-solid"}
|
||||||
|
As leading and trailing icons are wrapped around a `pointer-events-none` class, if you inject a clickable element in the slot, you need to remove this class to make it clickable by adding `:ui="{ icon: { trailing: { pointer: '' } } }"` to the Input.
|
||||||
|
::
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
:component-props
|
:component-props
|
||||||
|
|||||||
@@ -5,11 +5,22 @@ description: Display a textarea field.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
::component-card
|
Use a `v-model` to make the Textarea reactive.
|
||||||
---
|
|
||||||
baseProps:
|
::component-example
|
||||||
name: 'textarea'
|
#default
|
||||||
---
|
:textarea-example
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const value = ref('')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTextarea v-model="value" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Style
|
### Style
|
||||||
@@ -85,6 +96,20 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Rows
|
||||||
|
|
||||||
|
Use the `rows` prop to set the number of rows of the Textarea.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
name: 'input'
|
||||||
|
placeholder: 'Search...'
|
||||||
|
props:
|
||||||
|
rows: 1
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
### Disabled
|
### Disabled
|
||||||
|
|
||||||
Use the `disabled` prop to disable the Textarea.
|
Use the `disabled` prop to disable the Textarea.
|
||||||
@@ -99,6 +124,35 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Autoresize
|
||||||
|
|
||||||
|
Use the `autoresize` prop to enable the autoresize. Writing more lines than the `rows` prop will make the Textarea grow up.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
name: 'input'
|
||||||
|
placeholder: 'Search...'
|
||||||
|
modelValue: 'Here is an autoresize Textarea, write new lines to make the Textarea grow up...'
|
||||||
|
props:
|
||||||
|
autoresize: true
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Resize
|
||||||
|
|
||||||
|
Use the `resize` prop to enable the resize control.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
name: 'input'
|
||||||
|
placeholder: 'Search...'
|
||||||
|
props:
|
||||||
|
resize: true
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
:component-props
|
:component-props
|
||||||
|
|||||||
@@ -7,19 +7,53 @@ description: Display a select field.
|
|||||||
|
|
||||||
The Select component is a wrapper around the native `<select>` HTML element. For more advanced use cases like searching or multiple selection, consider using the [SelectMenu](/forms/select-menu) component.
|
The Select component is a wrapper around the native `<select>` HTML element. For more advanced use cases like searching or multiple selection, consider using the [SelectMenu](/forms/select-menu) component.
|
||||||
|
|
||||||
::component-card
|
Use a `v-model` to make the Select reactive alongside the `options` prop to pass an array of strings or objects.
|
||||||
---
|
|
||||||
baseProps:
|
::component-example
|
||||||
name: 'select'
|
#default
|
||||||
modelValue: 'United States'
|
:select-example
|
||||||
props:
|
|
||||||
options:
|
#code
|
||||||
- 'United States'
|
```vue
|
||||||
- 'Canada'
|
<script setup>
|
||||||
- 'Mexico'
|
const countries = ['United States', 'Canada', 'Mexico']
|
||||||
excludedProps:
|
|
||||||
- options
|
const country = ref(countries[0])
|
||||||
---
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelect v-model="country" :options="countries" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
When using objects, you can configure which field will be used for display through the `option-attribute` prop that defaults to `label` and which field will be used for comparison through the `value-attribute` prop that defaults to `value`.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:select-example-objects
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const countries = [{
|
||||||
|
name: 'United States',
|
||||||
|
value: 'US'
|
||||||
|
}, {
|
||||||
|
name: 'Canada',
|
||||||
|
value: 'CA'
|
||||||
|
}, {
|
||||||
|
name: 'Mexico',
|
||||||
|
value: 'MX'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const country = ref('CA')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelect v-model="country" :options="countries" option-attribute="name" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Style
|
### Style
|
||||||
@@ -157,6 +191,69 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Loading
|
||||||
|
|
||||||
|
Use the `loading` prop to show a loading icon and disable the Input.
|
||||||
|
|
||||||
|
Use the `loading-icon` prop to set a different icon or change it globally in `ui.select.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
name: 'select'
|
||||||
|
options:
|
||||||
|
- 'United States'
|
||||||
|
- 'Canada'
|
||||||
|
- 'Mexico'
|
||||||
|
placeholder: 'Search...'
|
||||||
|
props:
|
||||||
|
loading: true
|
||||||
|
icon: 'i-heroicons-magnifying-glass-20-solid'
|
||||||
|
excludedProps:
|
||||||
|
- icon
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
### `leading`
|
||||||
|
|
||||||
|
Use the `#leading` slot to set the content of the leading icon.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
slots:
|
||||||
|
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
|
||||||
|
baseProps:
|
||||||
|
name: 'select'
|
||||||
|
options:
|
||||||
|
- 'United States'
|
||||||
|
- 'Canada'
|
||||||
|
- 'Mexico'
|
||||||
|
placeholder: 'Search...'
|
||||||
|
---
|
||||||
|
|
||||||
|
#leading
|
||||||
|
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
|
||||||
|
::
|
||||||
|
|
||||||
|
### `trailing`
|
||||||
|
|
||||||
|
Use the `#trailing` slot to set the content of the trailing icon.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
slots:
|
||||||
|
trailing: <UIcon name="i-heroicons-arrows-up-down-20-solid" />
|
||||||
|
baseProps:
|
||||||
|
name: 'input'
|
||||||
|
placeholder: 'Search...'
|
||||||
|
---
|
||||||
|
|
||||||
|
#trailing
|
||||||
|
:u-icon{name="i-heroicons-arrows-up-down-20-solid"}
|
||||||
|
::
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
:component-props
|
:component-props
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ const selected = ref(people[0])
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
You can use the `multiple` prop to select multiple values but you have to override the `#label` slot and handle the display yourself.
|
### Multiple
|
||||||
|
|
||||||
|
You can use the `multiple` prop to select multiple values.
|
||||||
|
|
||||||
::component-example
|
::component-example
|
||||||
#default
|
#default
|
||||||
@@ -39,47 +41,18 @@ You can use the `multiple` prop to select multiple values but you have to overri
|
|||||||
#code
|
#code
|
||||||
```vue
|
```vue
|
||||||
<script setup>
|
<script setup>
|
||||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
const people = [...]
|
||||||
|
|
||||||
const selected = ref([])
|
const selected = ref([])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<USelectMenu v-model="selected" :options="people" multiple>
|
<USelectMenu v-model="selected" :options="people" multiple placeholder="Select people" />
|
||||||
<template #label>
|
|
||||||
<span v-if="selected.length" class="font-medium truncate">{{ selected.join(', ') }}</span>
|
|
||||||
<span v-else class="block truncate text-gray-400 dark:text-gray-500">Select people</span>
|
|
||||||
</template>
|
|
||||||
</USelectMenu>
|
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
You can also override the default slot entirely.
|
### Objects
|
||||||
|
|
||||||
::component-example
|
|
||||||
#default
|
|
||||||
:select-menu-example-button{class="max-w-[12rem] w-full"}
|
|
||||||
|
|
||||||
#code
|
|
||||||
```vue
|
|
||||||
<script setup>
|
|
||||||
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
|
||||||
|
|
||||||
const selected = ref(people[3])
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
|
|
||||||
<UButton>
|
|
||||||
{{ selected }}
|
|
||||||
|
|
||||||
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
|
|
||||||
</UButton>
|
|
||||||
</USelectMenu>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
::
|
|
||||||
|
|
||||||
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`.
|
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `option-attribute` prop that defaults to `label`.
|
||||||
|
|
||||||
@@ -134,10 +107,6 @@ const selected = ref(people[0])
|
|||||||
|
|
||||||
### Icon
|
### Icon
|
||||||
|
|
||||||
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
|
|
||||||
|
|
||||||
Use the `trailing-icon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
|
|
||||||
|
|
||||||
Use the `selected-icon` prop to set a different icon or change it globally in `ui.selectMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
|
Use the `selected-icon` prop to set a different icon or change it globally in `ui.selectMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
|
||||||
|
|
||||||
::component-card
|
::component-card
|
||||||
@@ -147,16 +116,16 @@ baseProps:
|
|||||||
placeholder: 'Select a person'
|
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']
|
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
|
||||||
props:
|
props:
|
||||||
icon: 'i-heroicons-magnifying-glass-20-solid'
|
selectedIcon: 'i-heroicons-hand-thumb-up-solid'
|
||||||
color: 'white'
|
|
||||||
extraColors:
|
|
||||||
- white
|
|
||||||
- gray
|
|
||||||
excludedProps:
|
excludedProps:
|
||||||
- icon
|
- selectedIcon
|
||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
::alert{icon="i-heroicons-light-bulb"}
|
||||||
|
Learn how to customize icons from the [Select](/forms/select#icon) component.
|
||||||
|
::
|
||||||
|
|
||||||
### Search
|
### Search
|
||||||
|
|
||||||
Use the `searchable` prop to enable search.
|
Use the `searchable` prop to enable search.
|
||||||
@@ -177,6 +146,63 @@ props:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
### `label`
|
||||||
|
|
||||||
|
You can override the `#label` slot and handle the display yourself.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:select-menu-example-multiple-slot{class="max-w-[12rem] w-full"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const people = [...]
|
||||||
|
|
||||||
|
const selected = ref([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelectMenu v-model="selected" :options="people" multiple>
|
||||||
|
<template #label>
|
||||||
|
<span v-if="selected.length" class="truncate">{{ selected.join(', ') }}</span>
|
||||||
|
<span v-else>Select people</span>
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
### `default`
|
||||||
|
|
||||||
|
You can also override the `#default` slot entirely.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:select-menu-example-button{class="max-w-[12rem] w-full"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const people = [...]
|
||||||
|
|
||||||
|
const selected = ref(people[3])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
|
||||||
|
<UButton color="gray">
|
||||||
|
{{ selected }}
|
||||||
|
|
||||||
|
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
|
||||||
|
</UButton>
|
||||||
|
</USelectMenu>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
:component-props
|
:component-props
|
||||||
|
|||||||
@@ -5,11 +5,22 @@ description: Display a checkbox field.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
::component-card
|
Use a `v-model` to make the Checkbox reactive.
|
||||||
---
|
|
||||||
baseProps:
|
::component-example
|
||||||
name: 'checkbox'
|
#default
|
||||||
---
|
:checkbox-example
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const selected = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UCheckbox v-model="selected" name="notifications" label="Notifications" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Label
|
### Label
|
||||||
|
|||||||
@@ -5,11 +5,36 @@ description: Display a radio field.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
::component-card
|
Use a `v-model` to make the Radio reactive.
|
||||||
---
|
|
||||||
baseProps:
|
::component-example
|
||||||
name: 'radio'
|
#default
|
||||||
---
|
:radio-example
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const methods = [{
|
||||||
|
name: 'email',
|
||||||
|
value: 'email',
|
||||||
|
label: 'Email'
|
||||||
|
}, {
|
||||||
|
name: 'sms',
|
||||||
|
value: 'sms',
|
||||||
|
label: 'Phone (SMS)'
|
||||||
|
}, {
|
||||||
|
name: 'push',
|
||||||
|
value: 'push',
|
||||||
|
label: 'Push notification'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const selected = ref('sms')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<URadio v-for="method of methods" :key="method.name" v-model="selected" v-bind="method" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Label
|
### Label
|
||||||
|
|||||||
@@ -8,24 +8,51 @@ headlessui:
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
::component-card
|
Use a `v-model` to make the Toggle reactive.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:toggle-example
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const selected = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UToggle v-model="selected" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Icon
|
### Icon
|
||||||
|
|
||||||
Use any icon from [Iconify](https://icones.js.org) by setting the `icon-on` and `icon-off` props by using this pattern: `i-{collection_name}-{icon_name}`.
|
Use any icon from [Iconify](https://icones.js.org) by setting the `on-icon` and `off-icon` props by using this pattern: `i-{collection_name}-{icon_name}` or change it globally in `ui.toggle.default.onIcon` and `ui.toggle.default.offIcon`.
|
||||||
|
|
||||||
::component-card
|
::component-card
|
||||||
---
|
---
|
||||||
props:
|
props:
|
||||||
iconOn: 'i-heroicons-check-20-solid'
|
onIcon: 'i-heroicons-check-20-solid'
|
||||||
iconOff: 'i-heroicons-x-mark-20-solid'
|
offIcon: 'i-heroicons-x-mark-20-solid'
|
||||||
excludedProps:
|
excludedProps:
|
||||||
- iconOn
|
- onIcon
|
||||||
- iconOff
|
- offIcon
|
||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Disabled
|
||||||
|
|
||||||
|
Use the `disabled` prop to disable the Toggle.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
props:
|
||||||
|
disabled: true
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
:component-props
|
:component-props
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
github: true
|
github:
|
||||||
|
suffix: .ts
|
||||||
description: Display a label and additional informations around a form element.
|
description: Display a label and additional informations around a form element.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
647
docs/content/4.data/1.table.md
Normal file
647
docs/content/4.data/1.table.md
Normal file
@@ -0,0 +1,647 @@
|
|||||||
|
---
|
||||||
|
github: true
|
||||||
|
description: 'Display data in a table.'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Use the `rows` prop to set the data to display in the table. By default, the table will display all the fields of the rows.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
---
|
||||||
|
|
||||||
|
#default
|
||||||
|
:table-example-basic{class="flex-1"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const people = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
title: 'Designer',
|
||||||
|
email: 'courtney.henry@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Tom Cook',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: 'tom.cook@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Whitney Francis',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: 'whitney.francis@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Leonard Krasner',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: 'leonard.krasner@example.com',
|
||||||
|
role: 'Owner'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Floyd Miles',
|
||||||
|
title: 'Principal Designer',
|
||||||
|
email: 'floyd.miles@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :rows="people" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
### Columns
|
||||||
|
|
||||||
|
Use the `columns` prop to configure which columns to display. It's an array of objects with the following properties:
|
||||||
|
|
||||||
|
- `label` - The label to display in the table header. Can be changed through the `column-attribute` prop.
|
||||||
|
- `key` - The field to display from the row data.
|
||||||
|
- `sortable` - Whether the column is sortable. Defaults to `false`.
|
||||||
|
- `direction` - The sort direction to use on first click. Defaults to `asc`.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
---
|
||||||
|
|
||||||
|
#default
|
||||||
|
:table-example-columns{class="flex-1"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const columns = [{
|
||||||
|
key: 'id',
|
||||||
|
label: 'ID'
|
||||||
|
}, {
|
||||||
|
key: 'name',
|
||||||
|
label: 'User name'
|
||||||
|
}, {
|
||||||
|
key: 'title',
|
||||||
|
label: 'Job position'
|
||||||
|
}, {
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email'
|
||||||
|
}, {
|
||||||
|
key: 'role'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const people = [...]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :columns="columns" :rows="people" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
You can easily use the [SelectMenu](/forms/select-menu) component to change the columns to display.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
---
|
||||||
|
|
||||||
|
#default
|
||||||
|
:table-example-columns-selectable{class="flex-1"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const columns = [{
|
||||||
|
key: 'id',
|
||||||
|
label: 'ID'
|
||||||
|
}, {
|
||||||
|
key: 'name',
|
||||||
|
label: 'Name'
|
||||||
|
}, {
|
||||||
|
key: 'title',
|
||||||
|
label: 'Title'
|
||||||
|
}, {
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email'
|
||||||
|
}, {
|
||||||
|
key: 'role',
|
||||||
|
label: 'Role'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const selectedColumns = ref([...columns])
|
||||||
|
|
||||||
|
const people = [...]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<USelectMenu v-model="selectedColumns" :options="columns" multiple placeholder="Columns" />
|
||||||
|
|
||||||
|
<UTable :columns="selectedColumns" :rows="people" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
### Sortable
|
||||||
|
|
||||||
|
You can make the columns sortable by setting the `sortable` property to `true` in the column configuration.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
---
|
||||||
|
|
||||||
|
#default
|
||||||
|
:table-example-columns-sortable{class="flex-1"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const columns = [{
|
||||||
|
key: 'id',
|
||||||
|
label: 'ID'
|
||||||
|
}, {
|
||||||
|
key: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
sortable: true
|
||||||
|
}, {
|
||||||
|
key: 'title',
|
||||||
|
label: 'Title',
|
||||||
|
sortable: true
|
||||||
|
}, {
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email',
|
||||||
|
sortable: true,
|
||||||
|
direction: 'desc'
|
||||||
|
}, {
|
||||||
|
key: 'role',
|
||||||
|
label: 'Role'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const people = [...]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :columns="columns" :rows="people" :sort="{ column: 'title' }" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
You can specify the default direction of each column through the `direction` property. It can be either `asc` or `desc` and defaults to `asc`.
|
||||||
|
|
||||||
|
You can specify a default sort for the table through the `sort` prop. It's an object with the following properties:
|
||||||
|
|
||||||
|
- `column` - The column to sort by.
|
||||||
|
- `direction` - The sort direction. Can be either `asc` or `desc` and defaults to `asc`.
|
||||||
|
|
||||||
|
::alert{icon="i-heroicons-light-bulb"}
|
||||||
|
This will set the default sort and will work even if no column is set as `sortable`.
|
||||||
|
::
|
||||||
|
|
||||||
|
Use the `sort-button` prop to customize the sort button in the header. You can pass all the props of the [Button](/elements/button) component to customize it through this prop or globally through `ui.table.default.sortButton`. Its icon defaults to `i-heroicons-arrows-up-down-20-solid`.
|
||||||
|
|
||||||
|
::component-card{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
baseProps:
|
||||||
|
class: 'w-full'
|
||||||
|
columns:
|
||||||
|
- key: 'id'
|
||||||
|
label: 'ID'
|
||||||
|
- key: 'name'
|
||||||
|
label: 'Name'
|
||||||
|
sortable: true
|
||||||
|
- key: 'title'
|
||||||
|
label: 'Title'
|
||||||
|
sortable: true
|
||||||
|
- key: 'email'
|
||||||
|
label: 'Email'
|
||||||
|
sortable: true
|
||||||
|
- key: 'role'
|
||||||
|
label: 'Role'
|
||||||
|
rows:
|
||||||
|
- id: 1
|
||||||
|
name: 'Lindsay Walton'
|
||||||
|
title: 'Front-end Developer'
|
||||||
|
email: 'lindsay.walton@example.com'
|
||||||
|
role: 'Member'
|
||||||
|
- id: 2
|
||||||
|
name: 'Courtney Henry'
|
||||||
|
title: 'Designer'
|
||||||
|
email: 'courtney.henry@example.com'
|
||||||
|
role: 'Admin'
|
||||||
|
- id: 3
|
||||||
|
name: 'Tom Cook'
|
||||||
|
title: 'Director of Product'
|
||||||
|
email: 'tom.cook@example.com'
|
||||||
|
role: 'Member'
|
||||||
|
- id: 4
|
||||||
|
name: 'Whitney Francis'
|
||||||
|
title: 'Copywriter'
|
||||||
|
email: 'whitney.francis@example.com'
|
||||||
|
role: 'Admin'
|
||||||
|
- id: 5
|
||||||
|
name: 'Leonard Krasner'
|
||||||
|
title: 'Senior Designer'
|
||||||
|
email: 'leonard.krasner@example.com'
|
||||||
|
role: 'Owner'
|
||||||
|
- id: 6
|
||||||
|
name: 'Floyd Miles'
|
||||||
|
title: 'Principal Designer'
|
||||||
|
email: 'floyd.miles@example.com'
|
||||||
|
role: 'Member'
|
||||||
|
props:
|
||||||
|
sortAscIcon: 'i-heroicons-arrow-up-20-solid'
|
||||||
|
sortDescIcon: 'i-heroicons-arrow-down-20-solid'
|
||||||
|
sortButton:
|
||||||
|
icon: 'i-heroicons-sparkles-20-solid'
|
||||||
|
color: 'primary'
|
||||||
|
variant: 'outline'
|
||||||
|
size: '2xs'
|
||||||
|
square: false
|
||||||
|
ui:
|
||||||
|
rounded: 'rounded-full'
|
||||||
|
excludedProps:
|
||||||
|
- sortButton
|
||||||
|
- sortAscIcon
|
||||||
|
- sortDescIcon
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
Use the `sort-asc-icon` prop to set a different icon or change it globally in `ui.table.default.sortAscIcon`. Defaults to `i-heroicons-bars-arrow-up-20-solid`.
|
||||||
|
|
||||||
|
Use the `sort-desc-icon` prop to set a different icon or change it globally in `ui.table.default.sortDescIcon`. Defaults to `i-heroicons-bars-arrow-down-20-solid`.
|
||||||
|
|
||||||
|
::alert{icon="i-heroicons-light-bulb"}
|
||||||
|
You can also customize the entire header cell, read more in the [Slots](#slots) section.
|
||||||
|
::
|
||||||
|
|
||||||
|
### Selectable
|
||||||
|
|
||||||
|
Use a `v-model` to make the table selectable. The `v-model` will be an array of the selected rows.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
---
|
||||||
|
|
||||||
|
#default
|
||||||
|
:table-example-selectable{class="flex-1"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const people = [...]
|
||||||
|
|
||||||
|
const selected = ref([people[1]])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable v-model="selected" :rows="people" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
::alert{icon="i-heroicons-light-bulb"}
|
||||||
|
You can use the `by` prop to compare objects by a field instead of comparing object instances. We've replicated the behavior of Headless UI [Combobox](https://headlessui.com/vue/combobox#binding-objects-as-values).
|
||||||
|
::
|
||||||
|
|
||||||
|
### Searchable
|
||||||
|
|
||||||
|
You can easily use the [Input](/forms/input) component to filter the rows.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
---
|
||||||
|
|
||||||
|
#default
|
||||||
|
:table-example-searchable{class="flex-1"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const people = [...]
|
||||||
|
|
||||||
|
const q = ref('')
|
||||||
|
|
||||||
|
const filteredRows = computed(() => {
|
||||||
|
if (!q.value) {
|
||||||
|
return people
|
||||||
|
}
|
||||||
|
|
||||||
|
return people.filter((person) => {
|
||||||
|
return Object.values(person).some((value) => {
|
||||||
|
return String(value).toLowerCase().includes(q.value.toLowerCase())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<UInput v-model="q" placeholder="Filter people..." />
|
||||||
|
|
||||||
|
<UTable :rows="filteredRows" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
### Paginable
|
||||||
|
|
||||||
|
You can easily use the [Pagination](/navigation/pagination) component to paginate the rows.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
---
|
||||||
|
|
||||||
|
#default
|
||||||
|
:table-example-paginable{class="w-full"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const people = [...]
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const pageCount = 5
|
||||||
|
|
||||||
|
const rows = computed(() => {
|
||||||
|
return people.slice((page.value - 1) * pageCount, (page.value) * pageCount)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<UTable :rows="rows" />
|
||||||
|
|
||||||
|
<UPagination v-model="page" :page-count="pageCount" :total="people.length" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
### Loading :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full"}
|
||||||
|
|
||||||
|
Use the `loading` prop to display a loading state.
|
||||||
|
|
||||||
|
Use the `loading-state` prop to customize the `icon` and `label` or change them globally in `ui.table.default.loadingState`.
|
||||||
|
|
||||||
|
You can also set it to `null` to hide the loading state.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
baseProps:
|
||||||
|
class: 'w-full'
|
||||||
|
columns:
|
||||||
|
- key: 'id'
|
||||||
|
label: 'ID'
|
||||||
|
- key: 'name'
|
||||||
|
label: 'Name'
|
||||||
|
- key: 'title'
|
||||||
|
label: 'Title'
|
||||||
|
- key: 'email'
|
||||||
|
label: 'Email'
|
||||||
|
- key: 'role'
|
||||||
|
label: 'Role'
|
||||||
|
props:
|
||||||
|
loading: true
|
||||||
|
loadingState:
|
||||||
|
icon: 'i-heroicons-arrow-path-20-solid'
|
||||||
|
label: "Loading..."
|
||||||
|
excludedProps:
|
||||||
|
- loadingState
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
This can be easily used with Nuxt `useAsyncData` composable.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const columns = [...]
|
||||||
|
|
||||||
|
const { pending, data: people } = await useLazyAsyncData('people', () => $fetch('/api/people'))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :rows="people" :columns="columns" :loading="pending" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Empty
|
||||||
|
|
||||||
|
An empty state will be displayed when there are no results.
|
||||||
|
|
||||||
|
Use the `empty-state` prop to customize the `icon` and `label` or change them globally in `ui.table.default.emptyState`.
|
||||||
|
|
||||||
|
You can also set it to `null` to hide the empty state.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
baseProps:
|
||||||
|
class: 'w-full'
|
||||||
|
columns:
|
||||||
|
- key: 'id'
|
||||||
|
label: 'ID'
|
||||||
|
- key: 'name'
|
||||||
|
label: 'Name'
|
||||||
|
- key: 'title'
|
||||||
|
label: 'Title'
|
||||||
|
- key: 'email'
|
||||||
|
label: 'Email'
|
||||||
|
- key: 'role'
|
||||||
|
label: 'Role'
|
||||||
|
props:
|
||||||
|
emptyState:
|
||||||
|
icon: 'i-heroicons-circle-stack-20-solid'
|
||||||
|
label: "No items."
|
||||||
|
excludedProps:
|
||||||
|
- emptyState
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
You can use slots to customize the header and data cells of the table.
|
||||||
|
|
||||||
|
### `<column>-header`
|
||||||
|
|
||||||
|
Use the `#<column>-header` slot to customize the header cell of a column. You will have access to the `column`, `sort` and `on-sort` properties in the slot scope.
|
||||||
|
|
||||||
|
The `sort` property is an object with the following properties:
|
||||||
|
|
||||||
|
- `field` - The field to sort by.
|
||||||
|
- `direction` - The direction to sort by. Can be `asc` or `desc`.
|
||||||
|
|
||||||
|
The `on-sort` property is a function that you can call to sort the table and accepts the column as parameter.
|
||||||
|
|
||||||
|
::alert{icon="i-heroicons-light-bulb"}
|
||||||
|
Even though you can customize the sort button as mentioned in the [Sortable](#sortable) section, you can use this slot to completely override its behavior, with a custom dropdown for example.
|
||||||
|
::
|
||||||
|
|
||||||
|
### `<column>-data`
|
||||||
|
|
||||||
|
Use the `#<column>-data` slot to customize the data cell of a column. You will have access to the `row` and `column` properties in the slot scope.
|
||||||
|
|
||||||
|
You can for example create an extra column for actions with a [Dropdown](/elements/dropdown) component inside or change the color of the rows based on a selection.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
---
|
||||||
|
|
||||||
|
#default
|
||||||
|
:table-example-slots{class="flex-1"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const columns = [..., {
|
||||||
|
key: 'actions'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const people = [...]
|
||||||
|
|
||||||
|
const items = (row) => [
|
||||||
|
[{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: 'i-heroicons-pencil-square-20-solid',
|
||||||
|
click: () => console.log('Edit', row.id)
|
||||||
|
}, {
|
||||||
|
label: 'Duplicate',
|
||||||
|
icon: 'i-heroicons-document-duplicate-20-solid'
|
||||||
|
}], [{
|
||||||
|
label: 'Archive',
|
||||||
|
icon: 'i-heroicons-archive-box-20-solid'
|
||||||
|
}, {
|
||||||
|
label: 'Move',
|
||||||
|
icon: 'i-heroicons-arrow-right-circle-20-solid'
|
||||||
|
}], [{
|
||||||
|
label: 'Delete',
|
||||||
|
icon: 'i-heroicons-trash-20-solid'
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
|
||||||
|
const selected = ref([people[1]])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable v-model="selected" :rows="people" :columns="columns">
|
||||||
|
<template #name-data="{ row }">
|
||||||
|
<span :class="[selected.find(person => person.id === row.id) && 'text-primary-500 dark:text-primary-400']">{{ row.name }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions-data="{ row }">
|
||||||
|
<UDropdown :items="items(row)">
|
||||||
|
<UButton color="gray" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
|
||||||
|
</UDropdown>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
### `loading-state` :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full"}
|
||||||
|
|
||||||
|
Use the `#loading-state` slot to customize the loading state.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
---
|
||||||
|
|
||||||
|
#default
|
||||||
|
:table-example-loading-slot{class="flex-1"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const columns = [...]
|
||||||
|
|
||||||
|
const people = []
|
||||||
|
|
||||||
|
const pending = ref(true)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :rows="people" :columns="columns" :loading="pending">
|
||||||
|
<template #loading-state>
|
||||||
|
<div class="flex items-center justify-center h-32">
|
||||||
|
<i class="loader --6" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* https://codepen.io/jenning/pen/YzNmzaV */
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
### `empty-state` :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full"}
|
||||||
|
|
||||||
|
Use the `#empty-state` slot to customize the empty state.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
---
|
||||||
|
|
||||||
|
#default
|
||||||
|
:table-example-empty-slot{class="flex-1"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const columns = [...]
|
||||||
|
const people = [...]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :rows="people" :columns="columns">
|
||||||
|
<template #empty-state>
|
||||||
|
<div class="flex flex-col items-center justify-center py-6 gap-3">
|
||||||
|
<span class="italic text-sm">No one here!</span>
|
||||||
|
<UButton label="Add people" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
:component-props
|
||||||
|
|
||||||
|
## Preset
|
||||||
|
|
||||||
|
:component-preset
|
||||||
@@ -39,11 +39,9 @@ const links = [{
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
## Themes
|
## Theme
|
||||||
|
|
||||||
Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do.
|
Our theming system provides a lot of flexibility to customize the component. Here is an example of what you can do.
|
||||||
|
|
||||||
### Tailwind
|
|
||||||
|
|
||||||
::component-example
|
::component-example
|
||||||
#default
|
#default
|
||||||
@@ -166,7 +166,7 @@ Use the `selected-icon` prop to set a different icon or change it globally in `u
|
|||||||
---
|
---
|
||||||
padding: false
|
padding: false
|
||||||
baseProps:
|
baseProps:
|
||||||
empty: null
|
emptyState: null
|
||||||
props:
|
props:
|
||||||
icon: 'i-heroicons-command-line'
|
icon: 'i-heroicons-command-line'
|
||||||
excludedProps:
|
excludedProps:
|
||||||
@@ -184,7 +184,7 @@ Use the `loading-icon` prop to set a different icon or change it globally in `ui
|
|||||||
---
|
---
|
||||||
padding: false
|
padding: false
|
||||||
baseProps:
|
baseProps:
|
||||||
empty: null
|
emptyState: null
|
||||||
props:
|
props:
|
||||||
loading: true
|
loading: true
|
||||||
excludedProps:
|
excludedProps:
|
||||||
@@ -200,7 +200,7 @@ Use the `placeholder` prop to change the input placeholder
|
|||||||
---
|
---
|
||||||
padding: false
|
padding: false
|
||||||
baseProps:
|
baseProps:
|
||||||
empty: null
|
emptyState: null
|
||||||
props:
|
props:
|
||||||
placeholder: 'Type a command...'
|
placeholder: 'Type a command...'
|
||||||
excludedProps:
|
excludedProps:
|
||||||
@@ -210,33 +210,33 @@ excludedProps:
|
|||||||
|
|
||||||
### Close
|
### Close
|
||||||
|
|
||||||
Use the `close` prop to display a close button on the right side of the input.
|
Use the `close-button` prop to display a close button on the right side of the input.
|
||||||
|
|
||||||
You can pass all the props of the [Button](/elements/button) component to customize it through the `close` prop or globally through `ui.commandPalette.default.close`.
|
You can pass all the props of the [Button](/elements/button) component to customize it through the `close-button` prop or globally through `ui.commandPalette.default.closeButton`.
|
||||||
|
|
||||||
::component-card
|
::component-card
|
||||||
---
|
---
|
||||||
padding: false
|
padding: false
|
||||||
baseProps:
|
baseProps:
|
||||||
empty: null
|
emptyState: null
|
||||||
props:
|
props:
|
||||||
close:
|
closeButton:
|
||||||
icon: 'i-heroicons-x-mark-20-solid'
|
icon: 'i-heroicons-x-mark-20-solid'
|
||||||
color: 'gray'
|
color: 'gray'
|
||||||
variant: 'link'
|
variant: 'link'
|
||||||
padded: false
|
padded: false
|
||||||
excludedProps:
|
excludedProps:
|
||||||
- close
|
- closeButton
|
||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
### Empty
|
### Empty
|
||||||
|
|
||||||
Use the `empty` prop to display a message when there are no results.
|
An empty state will be displayed when there are no results.
|
||||||
|
|
||||||
You can pass an `object` through the `empty` prop or globally through `ui.commandPalette.default.empty`. Here is the default:
|
Use the `empty-state` prop to customize the `icon` and `label` or change them globally in `ui.commandPalette.default.emptyState`.
|
||||||
|
|
||||||
You can also set it to `null` to hide the empty label.
|
You can also set it to `null` to hide the empty state.
|
||||||
|
|
||||||
::component-card
|
::component-card
|
||||||
---
|
---
|
||||||
@@ -244,12 +244,12 @@ padding: false
|
|||||||
baseProps:
|
baseProps:
|
||||||
placeholder: 'Type something to see the empty label change'
|
placeholder: 'Type something to see the empty label change'
|
||||||
props:
|
props:
|
||||||
empty:
|
emptyState:
|
||||||
icon: 'i-heroicons-magnifying-glass-20-solid'
|
icon: 'i-heroicons-magnifying-glass-20-solid'
|
||||||
label: "We couldn't find any items."
|
label: "We couldn't find any items."
|
||||||
queryLabel: "We couldn't find any items with that term. Please try again."
|
queryLabel: "We couldn't find any items with that term. Please try again."
|
||||||
excludedProps:
|
excludedProps:
|
||||||
- empty
|
- emptyState
|
||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ excludedProps:
|
|||||||
|
|
||||||
The CommandPalette component takes care of the full-text search for you with [Fuse.js](https://fusejs.io). You can pass all the options of Fuse.js through the `fuse` prop.
|
The CommandPalette component takes care of the full-text search for you with [Fuse.js](https://fusejs.io). You can pass all the options of Fuse.js through the `fuse` prop.
|
||||||
|
|
||||||
When searching for a command, the component will look for a `label` property on the command by default. You can customize this behaviour by overriding the `command-attribute` prop. This will also affect the display of the command.
|
When searching for a command, the component will look for a `label` property on the command by default. You can customize this behavior by overriding the `command-attribute` prop. This will also affect the display of the command.
|
||||||
|
|
||||||
You can also highlight the matches in the command by setting the `fuse.fuseOptions.includeMatches` to `true`. The CommandPalette component automatically takes care of the highlighting for you.
|
You can also highlight the matches in the command by setting the `fuse.fuseOptions.includeMatches` to `true`. The CommandPalette component automatically takes care of the highlighting for you.
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ const groups = computed(() => {
|
|||||||
::
|
::
|
||||||
|
|
||||||
::alert{icon="i-heroicons-light-bulb"}
|
::alert{icon="i-heroicons-light-bulb"}
|
||||||
The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behaviour by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`.
|
The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behavior by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`.
|
||||||
::
|
::
|
||||||
|
|
||||||
## Themes
|
## Themes
|
||||||
@@ -357,6 +357,40 @@ padding: false
|
|||||||
Take a look at the component!
|
Take a look at the component!
|
||||||
::
|
::
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
### `empty-state` :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full"}
|
||||||
|
|
||||||
|
Use the `#empty-state` slot to customize the empty state.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
overflowClass: 'overflow-x-auto'
|
||||||
|
---
|
||||||
|
|
||||||
|
#default
|
||||||
|
:command-palette-example-empty-slot{class="flex-1"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const groups = [...]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UCommandPalette :groups="groups">
|
||||||
|
<template #empty-state>
|
||||||
|
<div class="flex flex-col items-center justify-center py-6 gap-3">
|
||||||
|
<span class="italic text-sm">Nothing here!</span>
|
||||||
|
<UButton label="Add item" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCommandPalette>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
:component-props
|
:component-props
|
||||||
188
docs/content/5.navigation/3.pagination.md
Normal file
188
docs/content/5.navigation/3.pagination.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
---
|
||||||
|
github: true
|
||||||
|
description: Add a pagination to handle pages.
|
||||||
|
navigation:
|
||||||
|
badge: 'Edge'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Use a `v-model` to get a reactive page alongside a `total` which represents the total of items. You can also use the `page-count` prop to define the number of items per page which defaults to `10`.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:pagination-example-basic
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const page = ref(1)
|
||||||
|
const items = ref(Array(55))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UPagination v-model="page" :page-count="5" :total="items.length" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
### Max
|
||||||
|
|
||||||
|
Use the `max` prop to set a maximum of displayed pages. Defaults to `7`, being the minimum.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
modelValue: 1
|
||||||
|
props:
|
||||||
|
max: 5
|
||||||
|
pageCount: 5
|
||||||
|
total: 100
|
||||||
|
excludedProps:
|
||||||
|
- pageCount
|
||||||
|
- total
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Size
|
||||||
|
|
||||||
|
Use the `size` prop to change the size of the buttons.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
modelValue: 1
|
||||||
|
total: 100
|
||||||
|
props:
|
||||||
|
size: 'sm'
|
||||||
|
ui:
|
||||||
|
size:
|
||||||
|
2xs: true
|
||||||
|
xs: true
|
||||||
|
sm: true
|
||||||
|
md: true
|
||||||
|
lg: true
|
||||||
|
xl: true
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Active / Inactive
|
||||||
|
|
||||||
|
Use the `active-button` and `inactive-button` props to customize the active and inactive buttons of the Pagination.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
modelValue: 1
|
||||||
|
total: 100
|
||||||
|
props:
|
||||||
|
activeButton:
|
||||||
|
variant: 'outline'
|
||||||
|
inactiveButton:
|
||||||
|
color: 'gray'
|
||||||
|
excludedProps:
|
||||||
|
- activeButton
|
||||||
|
- inactiveButton
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Prev / Next
|
||||||
|
|
||||||
|
Use the `prev-button` and `next-button` props to customize the prev and next buttons of the Pagination.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
modelValue: 1
|
||||||
|
total: 100
|
||||||
|
props:
|
||||||
|
prevButton:
|
||||||
|
icon: 'i-heroicons-arrow-small-left-20-solid'
|
||||||
|
label: Prev
|
||||||
|
color: 'gray'
|
||||||
|
nextButton:
|
||||||
|
icon: 'i-heroicons-arrow-small-right-20-solid'
|
||||||
|
trailing: true
|
||||||
|
label: Next
|
||||||
|
color: 'gray'
|
||||||
|
excludedProps:
|
||||||
|
- prevButton
|
||||||
|
- nextButton
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
## Theme
|
||||||
|
|
||||||
|
Our theming system provides a lot of flexibility to customize the component. Here is an example of what you can do.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:pagination-theme-rounded
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const page = ref(1)
|
||||||
|
const items = ref(Array(55))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UPagination
|
||||||
|
v-model="page"
|
||||||
|
:total="items.length"
|
||||||
|
:ui="{
|
||||||
|
wrapper: 'flex items-center gap-1',
|
||||||
|
rounded: 'rounded-full min-w-[32px] justify-center'
|
||||||
|
}"
|
||||||
|
:prev-button="null"
|
||||||
|
:next-button="{
|
||||||
|
icon: 'i-heroicons-arrow-small-right-20-solid',
|
||||||
|
color: 'primary',
|
||||||
|
variant: 'outline'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
### `prev` / `next`
|
||||||
|
|
||||||
|
Use the `#prev` and `#next` slots to set the content of the previous and next buttons.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:pagination-example-prev-next-slots
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const page = ref(1);
|
||||||
|
const items = ref(Array(55));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UPagination v-model="page" :total="items.length" :ui="{ rounded: 'first-of-type:rounded-l-md last-of-type:rounded-r-md' }">
|
||||||
|
<template #prev="{ onClick }">
|
||||||
|
<UTooltip text="Previous page">
|
||||||
|
<UButton icon="i-heroicons-arrow-small-left-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="mr-2" @click="onClick" />
|
||||||
|
</UTooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #next="{ onClick }">
|
||||||
|
<UTooltip text="Next page">
|
||||||
|
<UButton icon="i-heroicons-arrow-small-right-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="ml-2" @click="onClick" />
|
||||||
|
</UTooltip>
|
||||||
|
</template>
|
||||||
|
</UPagination>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
:component-props
|
||||||
|
|
||||||
|
## Preset
|
||||||
|
|
||||||
|
:component-preset
|
||||||
@@ -36,7 +36,7 @@ const toast = useToast()
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
This component will render by default the notifications at the bottom right of the screen. You can configure its behaviour in the `app.config.ts` through `ui.notifications`:
|
This component will render by default the notifications at the bottom right of the screen. You can configure its behavior in the `app.config.ts` through `ui.notifications`:
|
||||||
|
|
||||||
```ts [app.config.ts]
|
```ts [app.config.ts]
|
||||||
export default defineAppConfig({
|
export default defineAppConfig({
|
||||||
@@ -130,8 +130,8 @@ baseProps:
|
|||||||
description: 'This is a notification.'
|
description: 'This is a notification.'
|
||||||
timeout: 600000
|
timeout: 600000
|
||||||
props:
|
props:
|
||||||
icon: 'i-heroicons-x-circle'
|
icon: 'i-heroicons-check-badge'
|
||||||
color: 'red'
|
color: 'primary'
|
||||||
extraColors:
|
extraColors:
|
||||||
- gray
|
- gray
|
||||||
excludedProps:
|
excludedProps:
|
||||||
@@ -187,9 +187,9 @@ function onCallback () {
|
|||||||
|
|
||||||
### Close
|
### Close
|
||||||
|
|
||||||
Use the `close` prop to hide or customize the close button on the Notification.
|
Use the `close-button` prop to hide or customize the close button on the Notification.
|
||||||
|
|
||||||
You can pass all the props of the [Button](/elements/button) component to customize it through the `close` prop or globally through `ui.notifications.default.close`.
|
You can pass all the props of the [Button](/elements/button) component to customize it through the `close-button` prop or globally through `ui.notification.default.closeButton`.
|
||||||
|
|
||||||
::component-card
|
::component-card
|
||||||
---
|
---
|
||||||
@@ -198,7 +198,7 @@ baseProps:
|
|||||||
title: 'Notification'
|
title: 'Notification'
|
||||||
timeout: 0
|
timeout: 0
|
||||||
props:
|
props:
|
||||||
close:
|
closeButton:
|
||||||
icon: 'i-heroicons-archive-box-x-mark'
|
icon: 'i-heroicons-archive-box-x-mark'
|
||||||
color: 'primary'
|
color: 'primary'
|
||||||
variant: 'outline'
|
variant: 'outline'
|
||||||
@@ -207,7 +207,7 @@ props:
|
|||||||
ui:
|
ui:
|
||||||
rounded: 'rounded-full'
|
rounded: 'rounded-full'
|
||||||
excludedProps:
|
excludedProps:
|
||||||
- close
|
- closeButton
|
||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ const toast = useToast()
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
Like for `close`, you can pass all the props of the [Button](/elements/button) component inside the action or globally through `ui.notifications.default.action`.
|
Like for `closeButton`, you can pass all the props of the [Button](/elements/button) component inside the action or globally through `ui.notification.default.actionButton`.
|
||||||
|
|
||||||
::component-card
|
::component-card
|
||||||
---
|
---
|
||||||
@@ -27,10 +27,6 @@ description: Display a card for content with a header, body and footer.
|
|||||||
|
|
||||||
:component-props
|
:component-props
|
||||||
|
|
||||||
## Slots
|
|
||||||
|
|
||||||
:component-slots
|
|
||||||
|
|
||||||
## Preset
|
## Preset
|
||||||
|
|
||||||
:component-preset
|
:component-preset
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import ui from '../src/module'
|
import ui from '../src/module'
|
||||||
|
import { excludeColors } from '../src/colors'
|
||||||
|
import colors from 'tailwindcss/colors'
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -25,7 +27,8 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
global: true,
|
global: true,
|
||||||
icons: ['heroicons', 'simple-icons']
|
icons: ['heroicons', 'simple-icons'],
|
||||||
|
safelistColors: excludeColors(colors)
|
||||||
},
|
},
|
||||||
typescript: {
|
typescript: {
|
||||||
strict: false,
|
strict: false,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default <Partial<Config>> {
|
|||||||
borderRadius: '0.375rem',
|
borderRadius: '0.375rem',
|
||||||
border: '1px solid var(--tw-prose-pre-border)',
|
border: '1px solid var(--tw-prose-pre-border)',
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
wordBreak: 'break-words'
|
wordBreak: 'break-word'
|
||||||
},
|
},
|
||||||
code: {
|
code: {
|
||||||
backgroundColor: 'var(--tw-prose-pre-bg)',
|
backgroundColor: 'var(--tw-prose-pre-bg)',
|
||||||
|
|||||||
27
package.json
27
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nuxthq/ui",
|
"name": "@nuxthq/ui",
|
||||||
"version": "2.2.1",
|
"version": "2.4.1",
|
||||||
"repository": "https://github.com/nuxtlabs/ui",
|
"repository": "https://github.com/nuxtlabs/ui",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
"node": ">=16.14.0"
|
"node": ">=16.14.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
|
||||||
"build": "nuxt-module-build",
|
"build": "nuxt-module-build",
|
||||||
"prepack": "pnpm build",
|
"prepack": "pnpm build",
|
||||||
"dev": "nuxi dev docs",
|
"dev": "nuxi dev docs",
|
||||||
@@ -26,17 +25,18 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"typecheck": "nuxi typecheck",
|
"typecheck": "nuxi typecheck",
|
||||||
"prepare": "nuxi prepare docs",
|
"prepare": "nuxi prepare docs",
|
||||||
"release": "pnpm lint && standard-version && git push --follow-tags"
|
"release": "release-it"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@egoist/tailwindcss-icons": "^1.0.7",
|
"@egoist/tailwindcss-icons": "^1.1.0",
|
||||||
"@headlessui/vue": "1.7.10",
|
"@headlessui/vue": "1.7.10",
|
||||||
"@iconify-json/heroicons": "^1.1.10",
|
"@iconify-json/heroicons": "^1.1.11",
|
||||||
"@nuxt/kit": "^3.5.1",
|
"@nuxt/kit": "^3.5.3",
|
||||||
"@nuxtjs/color-mode": "^3.2.0",
|
"@nuxtjs/color-mode": "^3.2.0",
|
||||||
"@nuxtjs/tailwindcss": "^6.7.0",
|
"@nuxtjs/tailwindcss": "^6.7.2",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@vueuse/core": "^10.1.2",
|
"@vueuse/core": "^10.1.2",
|
||||||
@@ -48,21 +48,22 @@
|
|||||||
"tailwindcss": "^3.3.2"
|
"tailwindcss": "^3.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/simple-icons": "^1.1.54",
|
"@iconify-json/simple-icons": "^1.1.56",
|
||||||
"@nuxt/content": "^2.6.0",
|
"@nuxt/content": "^2.6.0",
|
||||||
"@nuxt/devtools": "^0.5.5",
|
"@nuxt/devtools": "^0.5.5",
|
||||||
"@nuxt/eslint-config": "^0.1.1",
|
"@nuxt/eslint-config": "^0.1.1",
|
||||||
"@nuxt/module-builder": "^0.4.0",
|
"@nuxt/module-builder": "^0.4.0",
|
||||||
"@nuxthq/studio": "^0.12.1",
|
"@nuxthq/studio": "^0.13.2",
|
||||||
"@nuxtjs/plausible": "^0.2.1",
|
"@nuxtjs/plausible": "^0.2.1",
|
||||||
|
"@release-it/conventional-changelog": "^5.1.1",
|
||||||
"@types/lodash-es": "^4.17.7",
|
"@types/lodash-es": "^4.17.7",
|
||||||
"@types/node": "^20.2.4",
|
"@types/node": "^20.3.1",
|
||||||
"@vueuse/nuxt": "^10.1.2",
|
"@vueuse/nuxt": "^10.1.2",
|
||||||
"eslint": "^8.41.0",
|
"eslint": "^8.42.0",
|
||||||
"nuxt": "^3.5.1",
|
"nuxt": "^3.5.3",
|
||||||
"nuxt-component-meta": "^0.5.3",
|
"nuxt-component-meta": "^0.5.3",
|
||||||
"nuxt-lodash": "^2.4.1",
|
"nuxt-lodash": "^2.4.1",
|
||||||
"standard-version": "^9.5.0",
|
"release-it": "^15.11.0",
|
||||||
"unbuild": "^1.2.1",
|
"unbuild": "^1.2.1",
|
||||||
"vue-tsc": "1.6.3"
|
"vue-tsc": "1.6.3"
|
||||||
}
|
}
|
||||||
|
|||||||
3518
pnpm-lock.yaml
generated
3518
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
180
src/colors.ts
Normal file
180
src/colors.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
const colorsToExclude = [
|
||||||
|
'inherit',
|
||||||
|
'transparent',
|
||||||
|
'current',
|
||||||
|
'white',
|
||||||
|
'black',
|
||||||
|
'slate',
|
||||||
|
'gray',
|
||||||
|
'zinc',
|
||||||
|
'neutral',
|
||||||
|
'stone',
|
||||||
|
'cool'
|
||||||
|
]
|
||||||
|
|
||||||
|
const omit = (obj: object, keys: string[]) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(obj).filter(([key]) => !keys.includes(key))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const kebabCase = (str: string) => {
|
||||||
|
return str
|
||||||
|
?.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
|
||||||
|
?.map(x => x.toLowerCase())
|
||||||
|
?.join('-')
|
||||||
|
}
|
||||||
|
|
||||||
|
const safelistByComponent = {
|
||||||
|
avatar: (colorsAsRegex) => [{
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark']
|
||||||
|
}],
|
||||||
|
badge: (colorsAsRegex) => [{
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-50`)
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`ring-(${colorsAsRegex})-500`)
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark']
|
||||||
|
}],
|
||||||
|
button: (colorsAsRegex) => [{
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-50`),
|
||||||
|
variants: ['hover']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-100`),
|
||||||
|
variants: ['hover']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark', 'dark:disabled']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-500`),
|
||||||
|
variants: ['disabled', 'dark:hover']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-600`),
|
||||||
|
variants: ['hover']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-900`),
|
||||||
|
variants: ['dark:hover']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-950`),
|
||||||
|
variants: ['dark', 'dark:hover']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`text-(${colorsAsRegex})-500`),
|
||||||
|
variants: ['dark:hover']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`text-(${colorsAsRegex})-600`),
|
||||||
|
variants: ['hover']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`outline-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark:focus-visible']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`outline-(${colorsAsRegex})-500`),
|
||||||
|
variants: ['focus-visible']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark:focus-visible']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
|
||||||
|
variants: ['focus-visible']
|
||||||
|
}],
|
||||||
|
input: (colorsAsRegex) => [{
|
||||||
|
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark', 'dark:focus']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
|
||||||
|
variants: ['focus']
|
||||||
|
}],
|
||||||
|
notification: (colorsAsRegex) => [{
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark']
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
const safelistComponentAliasesMap = {
|
||||||
|
'USelect': 'UInput',
|
||||||
|
'USelectMenu': 'UInput',
|
||||||
|
'UTextarea': 'UInput'
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorsAsRegex = (colors: string[]): string => colors.join('|')
|
||||||
|
|
||||||
|
export const excludeColors = (colors: object) => Object.keys(omit(colors, colorsToExclude)).map(color => kebabCase(color)) as string[]
|
||||||
|
|
||||||
|
export const generateSafelist = (colors: string[]) => {
|
||||||
|
const safelist = ['avatar', 'badge', 'button', 'input', 'notification'].flatMap(component => safelistByComponent[component](colorsAsRegex(colors)))
|
||||||
|
|
||||||
|
return [
|
||||||
|
...safelist,
|
||||||
|
// Gray safelist for Avatar & Notification
|
||||||
|
'bg-gray-500',
|
||||||
|
'dark:bg-gray-400',
|
||||||
|
'text-gray-500',
|
||||||
|
'dark:text-gray-400'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customSafelistExtractor = (prefix, content: string, colors: string[], safelistColors: string[]) => {
|
||||||
|
const classes = []
|
||||||
|
const regex = /<(\w+)\s+(?![^>]*:color\b)[^>]*\bcolor=["']([^"']+)["'][^>]*>/gs
|
||||||
|
|
||||||
|
const matches = content.matchAll(regex)
|
||||||
|
|
||||||
|
const components = Object.keys(safelistByComponent).map(component => `${prefix}${component.charAt(0).toUpperCase() + component.slice(1)}`)
|
||||||
|
|
||||||
|
for (const match of matches) {
|
||||||
|
const [, component, color] = match
|
||||||
|
|
||||||
|
if (!colors.includes(color) || safelistColors.includes(color)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = safelistComponentAliasesMap[component] ? safelistComponentAliasesMap[component] : component
|
||||||
|
|
||||||
|
if (!components.includes(name)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name = name.replace(prefix, '').toLowerCase()
|
||||||
|
|
||||||
|
const matchClasses = safelistByComponent[name](color).flatMap(group => {
|
||||||
|
return ['', ...(group.variants || [])].flatMap(variant => {
|
||||||
|
const matches = group.pattern.source.match(/\(([^)]+)\)/g)
|
||||||
|
|
||||||
|
return matches.map(match => {
|
||||||
|
const colorOptions = match.substring(1, match.length - 1).split('|')
|
||||||
|
return colorOptions.map(color => `${variant ? variant + ':' : ''}` + group.pattern.source.replace(match, color))
|
||||||
|
}).flat()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
classes.push(...matchClasses)
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes
|
||||||
|
}
|
||||||
135
src/module.ts
135
src/module.ts
@@ -1,22 +1,20 @@
|
|||||||
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, createResolver, addPlugin, resolvePath } from '@nuxt/kit'
|
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, createResolver, addPlugin, resolvePath } from '@nuxt/kit'
|
||||||
import colors from 'tailwindcss/colors.js'
|
import defaultColors from 'tailwindcss/colors.js'
|
||||||
|
import { defaultExtractor as createDefaultExtractor } from 'tailwindcss/lib/lib/defaultExtractor.js'
|
||||||
import { iconsPlugin, getIconCollections } from '@egoist/tailwindcss-icons'
|
import { iconsPlugin, getIconCollections } from '@egoist/tailwindcss-icons'
|
||||||
import { name, version } from '../package.json'
|
import { name, version } from '../package.json'
|
||||||
import { colorsAsRegex, excludeColors } from './runtime/utils/colors'
|
import { generateSafelist, excludeColors, customSafelistExtractor } from './colors'
|
||||||
|
|
||||||
import appConfig from './runtime/app.config'
|
import appConfig from './runtime/app.config'
|
||||||
|
|
||||||
type DeepPartial<T> = Partial<{ [P in keyof T]: DeepPartial<T[P]> | { [key: string]: string } }>
|
type DeepPartial<T> = Partial<{ [P in keyof T]: DeepPartial<T[P]> | { [key: string]: string } }>
|
||||||
|
|
||||||
// @ts-ignore
|
const defaultExtractor = createDefaultExtractor({ tailwindConfig: { separator: ':' } })
|
||||||
delete colors.lightBlue
|
|
||||||
// @ts-ignore
|
delete defaultColors.lightBlue
|
||||||
delete colors.warmGray
|
delete defaultColors.warmGray
|
||||||
// @ts-ignore
|
delete defaultColors.trueGray
|
||||||
delete colors.trueGray
|
delete defaultColors.coolGray
|
||||||
// @ts-ignore
|
delete defaultColors.blueGray
|
||||||
delete colors.coolGray
|
|
||||||
// @ts-ignore
|
|
||||||
delete colors.blueGray
|
|
||||||
|
|
||||||
declare module 'nuxt/schema' {
|
declare module 'nuxt/schema' {
|
||||||
interface AppConfigInput {
|
interface AppConfigInput {
|
||||||
@@ -40,6 +38,8 @@ export interface ModuleOptions {
|
|||||||
global?: boolean
|
global?: boolean
|
||||||
|
|
||||||
icons: string[] | string
|
icons: string[] | string
|
||||||
|
|
||||||
|
safelistColors?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtModule<ModuleOptions>({
|
export default defineNuxtModule<ModuleOptions>({
|
||||||
@@ -52,8 +52,9 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
prefix: 'u',
|
prefix: 'U',
|
||||||
icons: ['heroicons']
|
icons: ['heroicons'],
|
||||||
|
safelistColors: ['primary']
|
||||||
},
|
},
|
||||||
async setup (options, nuxt) {
|
async setup (options, nuxt) {
|
||||||
const { resolve } = createResolver(import.meta.url)
|
const { resolve } = createResolver(import.meta.url)
|
||||||
@@ -70,14 +71,14 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
app.configs.push(appConfigFile)
|
app.configs.push(appConfigFile)
|
||||||
})
|
})
|
||||||
|
|
||||||
// @ts-ignore
|
nuxt.hook('tailwindcss:config', function (tailwindConfig) {
|
||||||
nuxt.hook('tailwindcss:config', function (tailwindConfig: TailwindConfig) {
|
const globalColors: any = {
|
||||||
const globalColors = {
|
...(tailwindConfig.theme.colors || defaultColors),
|
||||||
...(tailwindConfig.theme.colors || colors),
|
|
||||||
...tailwindConfig.theme.extend?.colors
|
...tailwindConfig.theme.extend?.colors
|
||||||
}
|
}
|
||||||
|
|
||||||
tailwindConfig.theme.extend.colors = tailwindConfig.theme.extend.colors || {}
|
tailwindConfig.theme.extend.colors = tailwindConfig.theme.extend.colors || {}
|
||||||
|
// @ts-ignore
|
||||||
globalColors.primary = tailwindConfig.theme.extend.colors.primary = {
|
globalColors.primary = tailwindConfig.theme.extend.colors.primary = {
|
||||||
50: 'rgb(var(--color-primary-50) / <alpha-value>)',
|
50: 'rgb(var(--color-primary-50) / <alpha-value>)',
|
||||||
100: 'rgb(var(--color-primary-100) / <alpha-value>)',
|
100: 'rgb(var(--color-primary-100) / <alpha-value>)',
|
||||||
@@ -93,9 +94,11 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (globalColors.gray) {
|
if (globalColors.gray) {
|
||||||
globalColors.cool = tailwindConfig.theme.extend.colors.cool = colors.gray
|
// @ts-ignore
|
||||||
|
globalColors.cool = tailwindConfig.theme.extend.colors.cool = defaultColors.gray
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
globalColors.gray = tailwindConfig.theme.extend.colors.gray = {
|
globalColors.gray = tailwindConfig.theme.extend.colors.gray = {
|
||||||
50: 'rgb(var(--color-gray-50) / <alpha-value>)',
|
50: 'rgb(var(--color-gray-50) / <alpha-value>)',
|
||||||
100: 'rgb(var(--color-gray-100) / <alpha-value>)',
|
100: 'rgb(var(--color-gray-100) / <alpha-value>)',
|
||||||
@@ -110,87 +113,65 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
950: 'rgb(var(--color-gray-950) / <alpha-value>)'
|
950: 'rgb(var(--color-gray-950) / <alpha-value>)'
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantColors = excludeColors(globalColors)
|
const colors = excludeColors(globalColors)
|
||||||
const safeColorsAsRegex = colorsAsRegex(variantColors)
|
|
||||||
|
|
||||||
nuxt.options.appConfig.ui = {
|
nuxt.options.appConfig.ui = {
|
||||||
...nuxt.options.appConfig.ui,
|
...nuxt.options.appConfig.ui,
|
||||||
primary: 'green',
|
primary: 'green',
|
||||||
gray: 'cool',
|
gray: 'cool',
|
||||||
colors: variantColors
|
colors
|
||||||
}
|
}
|
||||||
|
|
||||||
tailwindConfig.safelist = tailwindConfig.safelist || []
|
tailwindConfig.safelist = tailwindConfig.safelist || []
|
||||||
tailwindConfig.safelist.push(...[
|
tailwindConfig.safelist.push(...generateSafelist(options.safelistColors))
|
||||||
'bg-gray-500',
|
|
||||||
'dark:bg-gray-400',
|
|
||||||
{
|
|
||||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|400|500)`)
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-500`),
|
|
||||||
variants: ['disabled']
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(400|950)`),
|
|
||||||
variants: ['dark']
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(500|900|950)`),
|
|
||||||
variants: ['dark:hover']
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`),
|
|
||||||
variants: ['dark:disabled']
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|100|600)`),
|
|
||||||
variants: ['hover']
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`outline-(${safeColorsAsRegex})-500`),
|
|
||||||
variants: ['focus-visible']
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`outline-(${safeColorsAsRegex})-400`),
|
|
||||||
variants: ['dark:focus-visible']
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`ring-(${safeColorsAsRegex})-500`),
|
|
||||||
variants: ['focus', 'focus-visible']
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`ring-(${safeColorsAsRegex})-400`),
|
|
||||||
variants: ['dark', 'dark:focus', 'dark:focus-visible']
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`text-(${safeColorsAsRegex})-400`),
|
|
||||||
variants: ['dark']
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`text-(${safeColorsAsRegex})-500`),
|
|
||||||
variants: ['dark:hover']
|
|
||||||
}, {
|
|
||||||
pattern: new RegExp(`text-(${safeColorsAsRegex})-600`),
|
|
||||||
variants: ['hover']
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
tailwindConfig.plugins = tailwindConfig.plugins || []
|
tailwindConfig.plugins = tailwindConfig.plugins || []
|
||||||
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
|
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
|
||||||
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
|
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
|
||||||
await installModule('@nuxtjs/tailwindcss', {
|
await installModule('@nuxtjs/tailwindcss', {
|
||||||
viewer: false,
|
|
||||||
exposeConfig: true,
|
exposeConfig: true,
|
||||||
config: {
|
config: {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
plugins: [
|
plugins: [
|
||||||
require('@tailwindcss/forms'),
|
require("@tailwindcss/forms")({ strategy: 'class' }),
|
||||||
require('@tailwindcss/aspect-ratio'),
|
require('@tailwindcss/aspect-ratio'),
|
||||||
require('@tailwindcss/typography')
|
require('@tailwindcss/typography'),
|
||||||
|
require('@tailwindcss/container-queries')
|
||||||
],
|
],
|
||||||
content: [
|
content: {
|
||||||
resolve(runtimeDir, 'components/**/*.{vue,mjs,ts}'),
|
files: [
|
||||||
resolve(runtimeDir, '*.{mjs,js,ts}')
|
resolve(runtimeDir, 'components/**/*.{vue,mjs,ts}'),
|
||||||
]
|
resolve(runtimeDir, '*.{mjs,js,ts}')
|
||||||
|
],
|
||||||
|
transform: {
|
||||||
|
vue: (content) => {
|
||||||
|
return content.replaceAll(/(?:\r\n|\r|\n)/g, ' ')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extract: {
|
||||||
|
vue: (content) => {
|
||||||
|
return [
|
||||||
|
...defaultExtractor(content),
|
||||||
|
...customSafelistExtractor(options.prefix, content, nuxt.options.appConfig.ui.colors, options.safelistColors)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
|
||||||
addPlugin({
|
addPlugin({
|
||||||
src: resolve(runtimeDir, 'plugins', 'colors')
|
src: resolve(runtimeDir, 'plugins', 'colors')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Components
|
||||||
|
|
||||||
addComponentsDir({
|
addComponentsDir({
|
||||||
path: resolve(runtimeDir, 'components', 'elements'),
|
path: resolve(runtimeDir, 'components', 'elements'),
|
||||||
prefix: options.prefix,
|
prefix: options.prefix,
|
||||||
@@ -203,6 +184,12 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
global: options.global,
|
global: options.global,
|
||||||
watch: false
|
watch: false
|
||||||
})
|
})
|
||||||
|
addComponentsDir({
|
||||||
|
path: resolve(runtimeDir, 'components', 'data'),
|
||||||
|
prefix: options.prefix,
|
||||||
|
global: options.global,
|
||||||
|
watch: false
|
||||||
|
})
|
||||||
addComponentsDir({
|
addComponentsDir({
|
||||||
path: resolve(runtimeDir, 'components', 'layout'),
|
path: resolve(runtimeDir, 'components', 'layout'),
|
||||||
prefix: options.prefix,
|
prefix: options.prefix,
|
||||||
@@ -222,6 +209,8 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
watch: false
|
watch: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Composables
|
||||||
|
|
||||||
addImportsDir(resolve(runtimeDir, 'composables'))
|
addImportsDir(resolve(runtimeDir, 'composables'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +1,78 @@
|
|||||||
|
// Data
|
||||||
|
|
||||||
|
const table = {
|
||||||
|
wrapper: 'relative',
|
||||||
|
base: 'min-w-full table-fixed',
|
||||||
|
divide: 'divide-y divide-gray-300 dark:divide-gray-700',
|
||||||
|
thead: '',
|
||||||
|
tbody: 'divide-y divide-gray-200 dark:divide-gray-800',
|
||||||
|
tr: {
|
||||||
|
base: '',
|
||||||
|
selected: 'bg-gray-50 dark:bg-gray-800/50'
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
base: 'text-left',
|
||||||
|
padding: 'px-3 py-3.5',
|
||||||
|
color: 'text-gray-900 dark:text-white',
|
||||||
|
font: 'font-semibold',
|
||||||
|
size: 'text-sm'
|
||||||
|
},
|
||||||
|
td: {
|
||||||
|
base: 'whitespace-nowrap',
|
||||||
|
padding: 'px-3 py-4',
|
||||||
|
color: 'text-gray-500 dark:text-gray-400',
|
||||||
|
font: '',
|
||||||
|
size: 'text-sm'
|
||||||
|
},
|
||||||
|
loadingState: {
|
||||||
|
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
|
||||||
|
label: 'text-sm text-center text-gray-900 dark:text-white',
|
||||||
|
icon: 'w-6 h-6 mx-auto text-gray-400 dark:text-gray-500 mb-4 animate-spin'
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
|
||||||
|
label: 'text-sm text-center text-gray-900 dark:text-white',
|
||||||
|
icon: 'w-6 h-6 mx-auto text-gray-400 dark:text-gray-500 mb-4'
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
sortAscIcon: 'i-heroicons-bars-arrow-up-20-solid',
|
||||||
|
sortDescIcon: 'i-heroicons-bars-arrow-down-20-solid',
|
||||||
|
sortButton: {
|
||||||
|
icon: 'i-heroicons-arrows-up-down-20-solid',
|
||||||
|
trailing: true,
|
||||||
|
square: true,
|
||||||
|
color: 'gray',
|
||||||
|
variant: 'ghost',
|
||||||
|
class: '-m-1.5'
|
||||||
|
},
|
||||||
|
loadingState: {
|
||||||
|
icon: 'i-heroicons-arrow-path-20-solid',
|
||||||
|
label: 'Loading...'
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
icon: 'i-heroicons-circle-stack-20-solid',
|
||||||
|
label: 'No items.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Elements
|
// Elements
|
||||||
|
|
||||||
const avatar = {
|
const avatar = {
|
||||||
wrapper: 'relative inline-flex items-center justify-center',
|
wrapper: 'relative inline-flex items-center justify-center',
|
||||||
background: 'bg-gray-100 dark:bg-gray-800',
|
background: 'bg-gray-100 dark:bg-gray-800',
|
||||||
rounded: 'rounded-full',
|
rounded: 'rounded-full',
|
||||||
placeholder: 'text-xs font-medium leading-none text-gray-900 dark:text-white truncate',
|
placeholder: 'font-medium leading-none text-gray-900 dark:text-white truncate',
|
||||||
size: {
|
size: {
|
||||||
'3xs': 'h-4 w-4 text-xs',
|
'3xs': 'h-4 w-4 text-[8px]',
|
||||||
'2xs': 'h-5 w-5 text-xs',
|
'2xs': 'h-5 w-5 text-[10px]',
|
||||||
xs: 'h-6 w-6 text-xs',
|
xs: 'h-6 w-6 text-[11px]',
|
||||||
sm: 'h-8 w-8 text-sm',
|
sm: 'h-8 w-8 text-xs',
|
||||||
md: 'h-10 w-10 text-md',
|
md: 'h-10 w-10 text-sm',
|
||||||
lg: 'h-12 w-12 text-lg',
|
lg: 'h-12 w-12 text-base',
|
||||||
xl: 'h-14 w-14 text-xl',
|
xl: 'h-14 w-14 text-lg',
|
||||||
'2xl': 'h-16 w-16 text-2xl',
|
'2xl': 'h-16 w-16 text-xl',
|
||||||
'3xl': 'h-20 w-20 text-3xl'
|
'3xl': 'h-20 w-20 text-2xl'
|
||||||
},
|
},
|
||||||
chip: {
|
chip: {
|
||||||
base: 'absolute block rounded-full ring-1 ring-white dark:ring-gray-900',
|
base: 'absolute block rounded-full ring-1 ring-white dark:ring-gray-900',
|
||||||
@@ -60,6 +118,7 @@ const badge = {
|
|||||||
md: 'text-sm px-2 py-1',
|
md: 'text-sm px-2 py-1',
|
||||||
lg: 'text-sm px-2.5 py-1.5'
|
lg: 'text-sm px-2.5 py-1.5'
|
||||||
},
|
},
|
||||||
|
color: {},
|
||||||
variant: {
|
variant: {
|
||||||
solid: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-10 dark:ring-opacity-20'
|
solid: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-10 dark:ring-opacity-20'
|
||||||
},
|
},
|
||||||
@@ -79,32 +138,32 @@ const button = {
|
|||||||
xs: 'text-xs',
|
xs: 'text-xs',
|
||||||
sm: 'text-sm',
|
sm: 'text-sm',
|
||||||
md: 'text-sm',
|
md: 'text-sm',
|
||||||
lg: 'text-base',
|
lg: 'text-sm',
|
||||||
xl: 'text-base'
|
xl: 'text-base'
|
||||||
},
|
},
|
||||||
gap: {
|
gap: {
|
||||||
'2xs': 'gap-x-1',
|
'2xs': 'gap-x-1',
|
||||||
xs: 'gap-x-1.5',
|
xs: 'gap-x-1.5',
|
||||||
sm: 'gap-x-2',
|
sm: 'gap-x-1.5',
|
||||||
md: 'gap-x-2',
|
md: 'gap-x-2',
|
||||||
lg: 'gap-x-2',
|
lg: 'gap-x-2.5',
|
||||||
xl: 'gap-x-2'
|
xl: 'gap-x-2.5'
|
||||||
},
|
},
|
||||||
padding: {
|
padding: {
|
||||||
'2xs': 'px-2 py-1',
|
'2xs': 'px-2 py-1',
|
||||||
xs: 'px-2.5 py-1.5',
|
xs: 'px-2.5 py-1.5',
|
||||||
sm: 'px-3 py-1.5',
|
sm: 'px-2.5 py-1.5',
|
||||||
md: 'px-3 py-2',
|
md: 'px-3 py-2',
|
||||||
lg: 'px-4 py-2',
|
lg: 'px-3.5 py-2.5',
|
||||||
xl: 'px-4 py-3'
|
xl: 'px-3.5 py-2.5'
|
||||||
},
|
},
|
||||||
square: {
|
square: {
|
||||||
'2xs': 'p-[5px]',
|
'2xs': 'p-1',
|
||||||
xs: 'p-1.5',
|
xs: 'p-1.5',
|
||||||
sm: 'p-2',
|
sm: 'p-1.5',
|
||||||
md: 'p-2',
|
md: 'p-2',
|
||||||
lg: 'p-2.5',
|
lg: 'p-2.5',
|
||||||
xl: 'p-3'
|
xl: 'p-2.5'
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
white: {
|
white: {
|
||||||
@@ -131,9 +190,9 @@ const button = {
|
|||||||
icon: {
|
icon: {
|
||||||
base: 'flex-shrink-0',
|
base: 'flex-shrink-0',
|
||||||
size: {
|
size: {
|
||||||
'2xs': 'h-3.5 w-3.5',
|
'2xs': 'h-4 w-4',
|
||||||
xs: 'h-4 w-4',
|
xs: 'h-4 w-4',
|
||||||
sm: 'h-4 w-4',
|
sm: 'h-5 w-5',
|
||||||
md: 'h-5 w-5',
|
md: 'h-5 w-5',
|
||||||
lg: 'h-5 w-5',
|
lg: 'h-5 w-5',
|
||||||
xl: 'h-6 w-6'
|
xl: 'h-6 w-6'
|
||||||
@@ -148,7 +207,7 @@ const button = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buttonGroup = {
|
const buttonGroup = {
|
||||||
wrapper: 'inline-flex',
|
wrapper: 'inline-flex -space-x-px',
|
||||||
rounded: 'rounded-md',
|
rounded: 'rounded-md',
|
||||||
shadow: 'shadow-sm'
|
shadow: 'shadow-sm'
|
||||||
}
|
}
|
||||||
@@ -157,11 +216,12 @@ const dropdown = {
|
|||||||
wrapper: 'relative inline-flex text-left',
|
wrapper: 'relative inline-flex text-left',
|
||||||
container: 'z-20',
|
container: 'z-20',
|
||||||
width: 'w-48',
|
width: 'w-48',
|
||||||
|
height: '',
|
||||||
background: 'bg-white dark:bg-gray-800',
|
background: 'bg-white dark:bg-gray-800',
|
||||||
shadow: 'shadow-lg',
|
shadow: 'shadow-lg',
|
||||||
rounded: 'rounded-md',
|
rounded: 'rounded-md',
|
||||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||||
base: 'focus:outline-none',
|
base: 'relative focus:outline-none overflow-y-auto scroll-py-1',
|
||||||
divide: 'divide-y divide-gray-200 dark:divide-gray-700',
|
divide: 'divide-y divide-gray-200 dark:divide-gray-700',
|
||||||
padding: 'p-1',
|
padding: 'p-1',
|
||||||
item: {
|
item: {
|
||||||
@@ -221,13 +281,12 @@ const input = {
|
|||||||
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0',
|
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0',
|
||||||
rounded: 'rounded-md',
|
rounded: 'rounded-md',
|
||||||
placeholder: 'placeholder-gray-400 dark:placeholder-gray-500',
|
placeholder: 'placeholder-gray-400 dark:placeholder-gray-500',
|
||||||
custom: '',
|
|
||||||
size: {
|
size: {
|
||||||
'2xs': 'text-xs',
|
'2xs': 'text-xs',
|
||||||
xs: 'text-xs',
|
xs: 'text-xs',
|
||||||
sm: 'text-sm',
|
sm: 'text-sm',
|
||||||
md: 'text-sm',
|
md: 'text-sm',
|
||||||
lg: 'text-base',
|
lg: 'text-sm',
|
||||||
xl: 'text-base'
|
xl: 'text-base'
|
||||||
},
|
},
|
||||||
gap: {
|
gap: {
|
||||||
@@ -241,14 +300,14 @@ const input = {
|
|||||||
padding: {
|
padding: {
|
||||||
'2xs': 'px-2 py-1',
|
'2xs': 'px-2 py-1',
|
||||||
xs: 'px-2.5 py-1.5',
|
xs: 'px-2.5 py-1.5',
|
||||||
sm: 'px-3 py-1.5',
|
sm: 'px-2.5 py-1.5',
|
||||||
md: 'px-3 py-2',
|
md: 'px-3 py-2',
|
||||||
lg: 'px-4 py-2',
|
lg: 'px-3.5 py-2.5',
|
||||||
xl: 'px-4 py-3'
|
xl: 'px-3.5 py-2.5'
|
||||||
},
|
},
|
||||||
leading: {
|
leading: {
|
||||||
padding: {
|
padding: {
|
||||||
'2xs': 'pl-[26px]',
|
'2xs': 'pl-7',
|
||||||
xs: 'pl-8',
|
xs: 'pl-8',
|
||||||
sm: 'pl-9',
|
sm: 'pl-9',
|
||||||
md: 'pl-10',
|
md: 'pl-10',
|
||||||
@@ -258,7 +317,7 @@ const input = {
|
|||||||
},
|
},
|
||||||
trailing: {
|
trailing: {
|
||||||
padding: {
|
padding: {
|
||||||
'2xs': 'pr-[26px]',
|
'2xs': 'pr-7',
|
||||||
xs: 'pr-8',
|
xs: 'pr-8',
|
||||||
sm: 'pr-9',
|
sm: 'pr-9',
|
||||||
md: 'pr-10',
|
md: 'pr-10',
|
||||||
@@ -282,33 +341,35 @@ const input = {
|
|||||||
base: 'flex-shrink-0 text-gray-400 dark:text-gray-500',
|
base: 'flex-shrink-0 text-gray-400 dark:text-gray-500',
|
||||||
color: 'text-{color}-500 dark:text-{color}-400',
|
color: 'text-{color}-500 dark:text-{color}-400',
|
||||||
size: {
|
size: {
|
||||||
'2xs': 'h-3.5 w-3.5',
|
'2xs': 'h-4 w-4',
|
||||||
xs: 'h-4 w-4',
|
xs: 'h-4 w-4',
|
||||||
sm: 'h-4 w-4',
|
sm: 'h-5 w-5',
|
||||||
md: 'h-5 w-5',
|
md: 'h-5 w-5',
|
||||||
lg: 'h-5 w-5',
|
lg: 'h-5 w-5',
|
||||||
xl: 'h-6 w-6'
|
xl: 'h-6 w-6'
|
||||||
},
|
},
|
||||||
leading: {
|
leading: {
|
||||||
wrapper: 'absolute inset-y-0 left-0 flex items-center pointer-events-none',
|
wrapper: 'absolute inset-y-0 left-0 flex items-center',
|
||||||
|
pointer: 'pointer-events-none',
|
||||||
padding: {
|
padding: {
|
||||||
'2xs': 'pl-2',
|
'2xs': 'pl-2',
|
||||||
xs: 'pl-2.5',
|
xs: 'pl-2.5',
|
||||||
sm: 'pl-3',
|
sm: 'pl-2.5',
|
||||||
md: 'pl-3',
|
md: 'pl-3',
|
||||||
lg: 'pl-4',
|
lg: 'pl-3.5',
|
||||||
xl: 'pl-4'
|
xl: 'pl-3.5'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trailing: {
|
trailing: {
|
||||||
wrapper: 'absolute inset-y-0 right-0 flex items-center pointer-events-none',
|
wrapper: 'absolute inset-y-0 right-0 flex items-center',
|
||||||
|
pointer: 'pointer-events-none',
|
||||||
padding: {
|
padding: {
|
||||||
'2xs': 'pr-2',
|
'2xs': 'pr-2',
|
||||||
xs: 'pr-2.5',
|
xs: 'pr-2.5',
|
||||||
sm: 'pr-3',
|
sm: 'pr-2.5',
|
||||||
md: 'pr-3',
|
md: 'pr-3',
|
||||||
lg: 'pr-4',
|
lg: 'pr-3.5',
|
||||||
xl: 'pr-4'
|
xl: 'pr-3.5'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -345,16 +406,18 @@ const textarea = {
|
|||||||
|
|
||||||
const select = {
|
const select = {
|
||||||
...input,
|
...input,
|
||||||
|
placeholder: 'text-gray-900 dark:text-white',
|
||||||
default: {
|
default: {
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
variant: 'outline',
|
variant: 'outline',
|
||||||
|
loadingIcon: 'i-heroicons-arrow-path-20-solid',
|
||||||
trailingIcon: 'i-heroicons-chevron-down-20-solid'
|
trailingIcon: 'i-heroicons-chevron-down-20-solid'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectMenu = {
|
const selectMenu = {
|
||||||
wrapper: 'relative inline-flex',
|
wrapper: 'relative',
|
||||||
container: 'z-20',
|
container: 'z-20',
|
||||||
width: 'w-full',
|
width: 'w-full',
|
||||||
height: 'max-h-60',
|
height: 'max-h-60',
|
||||||
@@ -364,7 +427,7 @@ const selectMenu = {
|
|||||||
rounded: 'rounded-md',
|
rounded: 'rounded-md',
|
||||||
padding: 'p-1',
|
padding: 'p-1',
|
||||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||||
input: 'block w-[calc(100%+0.5rem)] focus:ring-transparent text-sm px-3 py-1.5 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border-0 border-b border-gray-200 dark:border-gray-700 focus:border-inherit sticky -top-1 -mt-1 mb-1 -mx-1 z-10 placeholder-gray-400 dark:placeholder-gray-500',
|
input: 'block w-[calc(100%+0.5rem)] focus:ring-transparent text-sm px-3 py-1.5 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border-0 border-b border-gray-200 dark:border-gray-700 focus:border-inherit sticky -top-1 -mt-1 mb-1 -mx-1 z-10 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none',
|
||||||
option: {
|
option: {
|
||||||
base: 'cursor-default select-none relative flex items-center justify-between gap-1',
|
base: 'cursor-default select-none relative flex items-center justify-between gap-1',
|
||||||
rounded: 'rounded-md',
|
rounded: 'rounded-md',
|
||||||
@@ -417,8 +480,12 @@ const radio = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkbox = {
|
const checkbox = {
|
||||||
...radio,
|
wrapper: 'relative flex items-start',
|
||||||
base: radio.base + ' rounded'
|
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent dark:indeterminate:bg-current dark:indeterminate:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
|
||||||
|
rounded: 'rounded',
|
||||||
|
label: 'font-medium text-gray-700 dark:text-gray-200',
|
||||||
|
required: 'text-red-500 dark:text-red-400',
|
||||||
|
help: 'text-gray-500 dark:text-gray-400'
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggle = {
|
const toggle = {
|
||||||
@@ -426,7 +493,7 @@ const toggle = {
|
|||||||
active: 'bg-primary-500 dark:bg-primary-400',
|
active: 'bg-primary-500 dark:bg-primary-400',
|
||||||
inactive: 'bg-gray-200 dark:bg-gray-700',
|
inactive: 'bg-gray-200 dark:bg-gray-700',
|
||||||
container: {
|
container: {
|
||||||
base: 'pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
|
base: 'pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white dark:bg-gray-900 shadow transform ring-0 transition ease-in-out duration-200',
|
||||||
active: 'translate-x-4',
|
active: 'translate-x-4',
|
||||||
inactive: 'translate-x-0'
|
inactive: 'translate-x-0'
|
||||||
},
|
},
|
||||||
@@ -436,6 +503,10 @@ const toggle = {
|
|||||||
inactive: 'opacity-0 ease-out duration-100',
|
inactive: 'opacity-0 ease-out duration-100',
|
||||||
on: 'h-3 w-3 text-primary-500 dark:text-primary-400',
|
on: 'h-3 w-3 text-primary-500 dark:text-primary-400',
|
||||||
off: 'h-3 w-3 text-gray-400 dark:text-gray-500'
|
off: 'h-3 w-3 text-gray-400 dark:text-gray-500'
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
onIcon: null,
|
||||||
|
offIcon: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +583,7 @@ const commandPalette = {
|
|||||||
container: 'relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2',
|
container: 'relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2',
|
||||||
input: {
|
input: {
|
||||||
wrapper: 'relative flex items-center',
|
wrapper: 'relative flex items-center',
|
||||||
base: 'w-full placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 text-gray-900 dark:text-white focus:ring-0',
|
base: 'w-full placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 text-gray-900 dark:text-white focus:ring-0 focus:outline-none',
|
||||||
padding: 'px-4',
|
padding: 'px-4',
|
||||||
height: 'h-12',
|
height: 'h-12',
|
||||||
size: 'sm:text-sm',
|
size: 'sm:text-sm',
|
||||||
@@ -521,9 +592,9 @@ const commandPalette = {
|
|||||||
size: 'h-4 w-4',
|
size: 'h-4 w-4',
|
||||||
padding: 'pl-10'
|
padding: 'pl-10'
|
||||||
},
|
},
|
||||||
close: 'absolute right-4'
|
closeButton: 'absolute right-4'
|
||||||
},
|
},
|
||||||
empty: {
|
emptyState: {
|
||||||
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
|
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
|
||||||
label: 'text-sm text-center text-gray-900 dark:text-white',
|
label: 'text-sm text-center text-gray-900 dark:text-white',
|
||||||
queryLabel: 'text-sm text-center text-gray-900 dark:text-white',
|
queryLabel: 'text-sm text-center text-gray-900 dark:text-white',
|
||||||
@@ -565,16 +636,39 @@ const commandPalette = {
|
|||||||
default: {
|
default: {
|
||||||
icon: 'i-heroicons-magnifying-glass-20-solid',
|
icon: 'i-heroicons-magnifying-glass-20-solid',
|
||||||
loadingIcon: 'i-heroicons-arrow-path-20-solid',
|
loadingIcon: 'i-heroicons-arrow-path-20-solid',
|
||||||
empty: {
|
emptyState: {
|
||||||
icon: 'i-heroicons-magnifying-glass-20-solid',
|
icon: 'i-heroicons-magnifying-glass-20-solid',
|
||||||
label: 'We couldn\'t find any items.',
|
label: 'We couldn\'t find any items.',
|
||||||
queryLabel: 'We couldn\'t find any items with that term. Please try again.'
|
queryLabel: 'We couldn\'t find any items with that term. Please try again.'
|
||||||
},
|
},
|
||||||
close: null,
|
closeButton: null,
|
||||||
selectedIcon: 'i-heroicons-check-20-solid'
|
selectedIcon: 'i-heroicons-check-20-solid'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pagination = {
|
||||||
|
wrapper: 'flex items-center -space-x-px',
|
||||||
|
base: '',
|
||||||
|
rounded: 'first:rounded-l-md last:rounded-r-md',
|
||||||
|
default: {
|
||||||
|
size: 'sm',
|
||||||
|
activeButton: {
|
||||||
|
color: 'primary'
|
||||||
|
},
|
||||||
|
inactiveButton: {
|
||||||
|
color: 'white'
|
||||||
|
},
|
||||||
|
prevButton: {
|
||||||
|
color: 'white',
|
||||||
|
icon: 'i-heroicons-chevron-left-20-solid'
|
||||||
|
},
|
||||||
|
nextButton: {
|
||||||
|
color: 'white',
|
||||||
|
icon: 'i-heroicons-chevron-right-20-solid'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Overlays
|
// Overlays
|
||||||
|
|
||||||
const modal = {
|
const modal = {
|
||||||
@@ -717,7 +811,7 @@ const notification = {
|
|||||||
padding: 'p-4',
|
padding: 'p-4',
|
||||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
|
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
|
||||||
icon: {
|
icon: {
|
||||||
base: 'flex-shrink-0 w-5 h-5 text-gray-400 dark:text-gray-500',
|
base: 'flex-shrink-0 w-5 h-5',
|
||||||
color: 'text-{color}-500 dark:text-{color}-400'
|
color: 'text-{color}-500 dark:text-{color}-400'
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
@@ -739,13 +833,13 @@ const notification = {
|
|||||||
default: {
|
default: {
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
icon: null,
|
icon: null,
|
||||||
close: {
|
closeButton: {
|
||||||
icon: 'i-heroicons-x-mark-20-solid',
|
icon: 'i-heroicons-x-mark-20-solid',
|
||||||
color: 'gray',
|
color: 'gray',
|
||||||
variant: 'link',
|
variant: 'link',
|
||||||
padded: false
|
padded: false
|
||||||
},
|
},
|
||||||
action: {
|
actionButton: {
|
||||||
size: 'xs',
|
size: 'xs',
|
||||||
color: 'white'
|
color: 'white'
|
||||||
}
|
}
|
||||||
@@ -761,6 +855,7 @@ const notifications = {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
ui: {
|
ui: {
|
||||||
|
table,
|
||||||
avatar,
|
avatar,
|
||||||
avatarGroup,
|
avatarGroup,
|
||||||
badge,
|
badge,
|
||||||
@@ -781,6 +876,7 @@ export default {
|
|||||||
skeleton,
|
skeleton,
|
||||||
verticalNavigation,
|
verticalNavigation,
|
||||||
commandPalette,
|
commandPalette,
|
||||||
|
pagination,
|
||||||
modal,
|
modal,
|
||||||
slideover,
|
slideover,
|
||||||
popover,
|
popover,
|
||||||
|
|||||||
215
src/runtime/components/data/Table.vue
Normal file
215
src/runtime/components/data/Table.vue
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="ui.wrapper">
|
||||||
|
<table :class="[ui.base, ui.divide]">
|
||||||
|
<thead :class="ui.thead">
|
||||||
|
<tr :class="ui.tr.base">
|
||||||
|
<th v-if="modelValue" scope="col" class="pl-4">
|
||||||
|
<UCheckbox :checked="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" @change="selected = $event.target.checked ? rows : []" />
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th v-for="(column, index) in columns" :key="index" scope="col" :class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size]">
|
||||||
|
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
|
||||||
|
<UButton
|
||||||
|
v-if="column.sortable"
|
||||||
|
v-bind="{ ...ui.default.sortButton, ...sortButton }"
|
||||||
|
:icon="(!sort.column || sort.column !== column.key) ? sortButton.icon : sort.direction === 'asc' ? sortAscIcon : sortDescIcon"
|
||||||
|
:label="column[columnAttribute]"
|
||||||
|
@click="onSort(column)"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ column[columnAttribute] }}</span>
|
||||||
|
</slot>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody :class="ui.tbody">
|
||||||
|
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected]">
|
||||||
|
<td v-if="modelValue" class="pl-4">
|
||||||
|
<UCheckbox v-model="selected" :value="row" />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]">
|
||||||
|
<slot :name="`${column.key}-data`" :column="column" :row="row">
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr v-if="loadingState && loading">
|
||||||
|
<td :colspan="columns.length + (modelValue ? 1 : 0)">
|
||||||
|
<slot name="loading-state">
|
||||||
|
<div :class="ui.loadingState.wrapper">
|
||||||
|
<UIcon v-if="loadingState.icon" :name="loadingState.icon" :class="ui.loadingState.icon" aria-hidden="true" />
|
||||||
|
<p :class="ui.loadingState.label">
|
||||||
|
{{ loadingState.label }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr v-else-if="emptyState && !rows.length">
|
||||||
|
<td :colspan="columns.length + (modelValue ? 1 : 0)">
|
||||||
|
<slot name="empty-state">
|
||||||
|
<div :class="ui.emptyState.wrapper">
|
||||||
|
<UIcon v-if="emptyState.icon" :name="emptyState.icon" :class="ui.emptyState.icon" aria-hidden="true" />
|
||||||
|
<p :class="ui.emptyState.label">
|
||||||
|
{{ emptyState.label }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref, computed, defineComponent, toRaw } from 'vue'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { capitalize, orderBy } from 'lodash-es'
|
||||||
|
import { defu } from 'defu'
|
||||||
|
import type { Button } from '../../types/button'
|
||||||
|
import { useAppConfig } from '#imports'
|
||||||
|
// TODO: Remove
|
||||||
|
// @ts-expect-error
|
||||||
|
import appConfig from '#build/app.config'
|
||||||
|
|
||||||
|
// const appConfig = useAppConfig()
|
||||||
|
|
||||||
|
function defaultComparator<T>(a: T, z: T): boolean {
|
||||||
|
return a === z
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
by: {
|
||||||
|
type: [String, Function],
|
||||||
|
default: () => defaultComparator
|
||||||
|
},
|
||||||
|
rows: {
|
||||||
|
type: Array as PropType<{ [key: string]: any }[]>,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
type: Array as PropType<{ key: string, sortable?: boolean, [key: string]: any }[]>,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
columnAttribute: {
|
||||||
|
type: String,
|
||||||
|
default: 'label'
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
type: Object as PropType<{ column: string, direction: 'asc' | 'desc' }>,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
sortButton: {
|
||||||
|
type: Object as PropType<Partial<Button>>,
|
||||||
|
default: () => appConfig.ui.table.default.sortButton
|
||||||
|
},
|
||||||
|
sortAscIcon: {
|
||||||
|
type: String,
|
||||||
|
default: () => appConfig.ui.table.default.sortAscIcon
|
||||||
|
},
|
||||||
|
sortDescIcon: {
|
||||||
|
type: String,
|
||||||
|
default: () => appConfig.ui.table.default.sortDescIcon
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
loadingState: {
|
||||||
|
type: Object as PropType<{ icon: string, label: string }>,
|
||||||
|
default: () => appConfig.ui.table.default.loadingState
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
type: Object as PropType<{ icon: string, label: string }>,
|
||||||
|
default: () => appConfig.ui.table.default.emptyState
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
type: Object as PropType<Partial<typeof appConfig.ui.table>>,
|
||||||
|
default: () => appConfig.ui.table
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup (props, { emit }) {
|
||||||
|
// TODO: Remove
|
||||||
|
const appConfig = useAppConfig()
|
||||||
|
|
||||||
|
const ui = computed<Partial<typeof appConfig.ui.table>>(() => defu({}, props.ui, appConfig.ui.table))
|
||||||
|
|
||||||
|
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: capitalize(key), sortable: false })))
|
||||||
|
|
||||||
|
const sort = ref(defu({}, props.sort, { column: null, direction: 'asc' }))
|
||||||
|
|
||||||
|
const rows = computed(() => {
|
||||||
|
if (!sort.value?.column) {
|
||||||
|
return props.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
const { column, direction } = sort.value
|
||||||
|
|
||||||
|
return orderBy(props.rows, column, direction)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selected = computed({
|
||||||
|
get () {
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const indeterminate = computed(() => selected.value && selected.value.length > 0 && selected.value.length < props.rows.length)
|
||||||
|
|
||||||
|
const emptyState = computed(() => ({ ...ui.value.default.emptyState, ...props.emptyState }))
|
||||||
|
|
||||||
|
function compare (a: any, z: any) {
|
||||||
|
if (typeof props.by === 'string') {
|
||||||
|
const property = props.by as unknown as any
|
||||||
|
return a?.[property] === z?.[property]
|
||||||
|
}
|
||||||
|
return props.by(a, z)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected (row) {
|
||||||
|
if (!props.modelValue) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected.value.some((item) => compare(toRaw(item), toRaw(row)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSort (column) {
|
||||||
|
if (sort.value.column === column.key) {
|
||||||
|
sort.value.direction = sort.value.direction === 'asc' ? 'desc' : 'asc'
|
||||||
|
} else {
|
||||||
|
sort.value = { column: column.key, direction: column.direction || 'asc' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
ui,
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
sort,
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
columns,
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
rows,
|
||||||
|
selected,
|
||||||
|
indeterminate,
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
emptyState,
|
||||||
|
isSelected,
|
||||||
|
onSort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { h, computed, defineComponent } from 'vue'
|
import { h, cloneVNode, computed, defineComponent } from 'vue'
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { classNames, getSlotsChildren } from '../../utils'
|
import { classNames, getSlotsChildren } from '../../utils'
|
||||||
@@ -39,18 +39,20 @@ export default defineComponent({
|
|||||||
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max)
|
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max)
|
||||||
|
|
||||||
const clones = computed(() => children.value.map((node, index) => {
|
const clones = computed(() => children.value.map((node, index) => {
|
||||||
|
const vProps: any = {}
|
||||||
|
|
||||||
if (!props.max || (max.value && index < max.value)) {
|
if (!props.max || (max.value && index < max.value)) {
|
||||||
if (props.size) {
|
if (props.size) {
|
||||||
node.props.size = props.size
|
vProps.size = props.size
|
||||||
}
|
}
|
||||||
|
|
||||||
node.props.class = node.props.class || ''
|
vProps.class = node.props.class || ''
|
||||||
node.props.class += ` ${classNames(
|
vProps.class += ` ${classNames(
|
||||||
ui.value.ring,
|
ui.value.ring,
|
||||||
ui.value.margin
|
ui.value.margin
|
||||||
)}`
|
)}`
|
||||||
|
|
||||||
return node
|
return cloneVNode(node, vProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (max.value !== undefined && index === max.value) {
|
if (max.value !== undefined && index === max.value) {
|
||||||
|
|||||||
@@ -29,14 +29,17 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: () => appConfig.ui.badge.default.color,
|
default: () => appConfig.ui.badge.default.color,
|
||||||
validator (value: string) {
|
validator (value: string) {
|
||||||
return appConfig.ui.colors.includes(value)
|
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.badge.color)].includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
type: String,
|
type: String,
|
||||||
default: () => appConfig.ui.badge.default.variant,
|
default: () => appConfig.ui.badge.default.variant,
|
||||||
validator (value: string) {
|
validator (value: string) {
|
||||||
return Object.keys(appConfig.ui.badge.variant).includes(value)
|
return [
|
||||||
|
...Object.keys(appConfig.ui.badge.variant),
|
||||||
|
...Object.values(appConfig.ui.badge.color).flatMap(value => Object.keys(value))
|
||||||
|
].includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
@@ -55,12 +58,14 @@ export default defineComponent({
|
|||||||
const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defu({}, props.ui, appConfig.ui.badge))
|
const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defu({}, props.ui, appConfig.ui.badge))
|
||||||
|
|
||||||
const badgeClass = computed(() => {
|
const badgeClass = computed(() => {
|
||||||
|
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||||
|
|
||||||
return classNames(
|
return classNames(
|
||||||
ui.value.base,
|
ui.value.base,
|
||||||
ui.value.font,
|
ui.value.font,
|
||||||
ui.value.rounded,
|
ui.value.rounded,
|
||||||
ui.value.size[props.size],
|
ui.value.size[props.size],
|
||||||
ui.value.variant[props.variant]?.replaceAll('{color}', props.color)
|
variant?.replaceAll('{color}', props.color)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,19 @@
|
|||||||
:aria-label="ariaLabel"
|
:aria-label="ariaLabel"
|
||||||
v-bind="buttonProps"
|
v-bind="buttonProps"
|
||||||
>
|
>
|
||||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
|
<slot name="leading" :disabled="disabled" :loading="loading">
|
||||||
|
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
|
||||||
|
</slot>
|
||||||
|
|
||||||
<slot>
|
<slot>
|
||||||
<span v-if="label" :class="[truncate ? 'text-left break-all line-clamp-1' : '']">
|
<span v-if="label" :class="[truncate ? 'text-left break-all line-clamp-1' : '']">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</span>
|
</span>
|
||||||
</slot>
|
</slot>
|
||||||
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
|
|
||||||
|
<slot name="trailing" :disabled="disabled" :loading="loading">
|
||||||
|
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
|
||||||
|
</slot>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { h, computed, defineComponent } from 'vue'
|
import { h, cloneVNode, computed, defineComponent } from 'vue'
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { getSlotsChildren } from '../../utils'
|
import { getSlotsChildren } from '../../utils'
|
||||||
@@ -15,7 +15,7 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
validator (value: string) {
|
validator (value: string) {
|
||||||
return Object.keys(appConfig.ui.avatar.size).includes(value)
|
return Object.keys(appConfig.ui.button.size).includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
@@ -44,28 +44,26 @@ export default defineComponent({
|
|||||||
}[ui.value.rounded]))
|
}[ui.value.rounded]))
|
||||||
|
|
||||||
const clones = computed(() => children.value.map((node, index) => {
|
const clones = computed(() => children.value.map((node, index) => {
|
||||||
|
const vProps: any = {}
|
||||||
|
|
||||||
if (props.size) {
|
if (props.size) {
|
||||||
node.props.size = props.size
|
vProps.size = props.size
|
||||||
}
|
}
|
||||||
|
|
||||||
node.props.class = node.props.class || ''
|
vProps.class = node.props.class || ''
|
||||||
node.props.class += ' !shadow-none'
|
vProps.class += ' !shadow-none'
|
||||||
node.props.ui = node.props.ui || {}
|
vProps.ui = node.props.ui || {}
|
||||||
node.props.ui.rounded = ''
|
vProps.ui.rounded = ''
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
node.props.ui.rounded = rounded.value.left
|
vProps.ui.rounded = rounded.value.left
|
||||||
}
|
|
||||||
|
|
||||||
if (index > 0) {
|
|
||||||
node.props.class += ' -ml-px'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index === children.value.length - 1) {
|
if (index === children.value.length - 1) {
|
||||||
node.props.ui.rounded = rounded.value.right
|
vProps.ui.rounded = rounded.value.right
|
||||||
}
|
}
|
||||||
|
|
||||||
return node
|
return cloneVNode(node, vProps)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return () => h('div', { class: [ui.value.wrapper, ui.value.rounded, ui.value.shadow] }, clones.value)
|
return () => h('div', { class: [ui.value.wrapper, ui.value.rounded, ui.value.shadow] }, clones.value)
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
|
|
||||||
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" @mouseover="onMouseOver">
|
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseover="onMouseOver">
|
||||||
<transition appear v-bind="ui.transition">
|
<transition appear v-bind="ui.transition">
|
||||||
<MenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background]" static>
|
<MenuItems :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">
|
<div v-for="(subItems, index) of items" :key="index" :class="ui.padding">
|
||||||
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
|
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
|
||||||
<ULinkCustom
|
<ULinkCustom
|
||||||
@@ -50,11 +50,11 @@ import type { PropType } from 'vue'
|
|||||||
import type { RouteLocationRaw } from 'vue-router'
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
import { defineComponent, ref, computed, onMounted } from 'vue'
|
import { defineComponent, ref, computed, onMounted } from 'vue'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
|
import { omit } from 'lodash-es'
|
||||||
import UIcon from '../elements/Icon.vue'
|
import UIcon from '../elements/Icon.vue'
|
||||||
import UAvatar from '../elements/Avatar.vue'
|
import UAvatar from '../elements/Avatar.vue'
|
||||||
import UKbd from '../elements/Kbd.vue'
|
import UKbd from '../elements/Kbd.vue'
|
||||||
import ULinkCustom from '../elements/LinkCustom.vue'
|
import ULinkCustom from '../elements/LinkCustom.vue'
|
||||||
import { omit } from '../../utils'
|
|
||||||
import { usePopper } from '../../composables/usePopper'
|
import { usePopper } from '../../composables/usePopper'
|
||||||
import type { Avatar } from '../../types/avatar'
|
import type { Avatar } from '../../types/avatar'
|
||||||
import type { PopperOptions } from '../../types'
|
import type { PopperOptions } from '../../types'
|
||||||
@@ -127,7 +127,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const ui = computed<Partial<typeof appConfig.ui.dropdown>>(() => defu({}, props.ui, appConfig.ui.dropdown))
|
const ui = computed<Partial<typeof appConfig.ui.dropdown>>(() => defu({}, props.ui, appConfig.ui.dropdown))
|
||||||
|
|
||||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
|
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
|
||||||
|
|
||||||
const [trigger, container] = usePopper(popper.value)
|
const [trigger, container] = usePopper(popper.value)
|
||||||
|
|
||||||
@@ -149,6 +149,12 @@ export default defineComponent({
|
|||||||
}, 200)
|
}, 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const containerStyle = computed(() => {
|
||||||
|
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8
|
||||||
|
|
||||||
|
return props.mode === 'hover' ? { paddingTop: `${offsetDistance}px`, paddingBottom: `${offsetDistance}px` } : {}
|
||||||
|
})
|
||||||
|
|
||||||
function onMouseOver () {
|
function onMouseOver () {
|
||||||
if (props.mode !== 'hover' || !menuApi.value) {
|
if (props.mode !== 'hover' || !menuApi.value) {
|
||||||
return
|
return
|
||||||
@@ -194,6 +200,7 @@ export default defineComponent({
|
|||||||
ui,
|
ui,
|
||||||
trigger,
|
trigger,
|
||||||
container,
|
container,
|
||||||
|
containerStyle,
|
||||||
onMouseOver,
|
onMouseOver,
|
||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
omit
|
omit
|
||||||
|
|||||||
@@ -8,8 +8,12 @@
|
|||||||
:required="required"
|
:required="required"
|
||||||
:value="value"
|
:value="value"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
:checked="checked"
|
||||||
|
:indeterminate="indeterminate"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:class="[ui.base, ui.custom]"
|
class="form-checkbox"
|
||||||
|
:class="[ui.base, ui.rounded]"
|
||||||
|
v-bind="$attrs"
|
||||||
@focus="$emit('focus', $event)"
|
@focus="$emit('focus', $event)"
|
||||||
@blur="$emit('blur', $event)"
|
@blur="$emit('blur', $event)"
|
||||||
>
|
>
|
||||||
@@ -38,9 +42,10 @@ import appConfig from '#build/app.config'
|
|||||||
// const appConfig = useAppConfig()
|
// const appConfig = useAppConfig()
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: [String, Number, Boolean],
|
type: [String, Number, Boolean, Object],
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -55,6 +60,14 @@ export default defineComponent({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
checked: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
indeterminate: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
help: {
|
help: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { h, computed, defineComponent } from 'vue'
|
import { h, cloneVNode, computed, defineComponent } from 'vue'
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { getSlotsChildren } from '../../utils'
|
import { getSlotsChildren } from '../../utils'
|
||||||
@@ -53,18 +53,20 @@ export default defineComponent({
|
|||||||
const children = computed(() => getSlotsChildren(slots))
|
const children = computed(() => getSlotsChildren(slots))
|
||||||
|
|
||||||
const clones = computed(() => children.value.map((node) => {
|
const clones = computed(() => children.value.map((node) => {
|
||||||
|
const vProps: any = {}
|
||||||
|
|
||||||
if (props.error) {
|
if (props.error) {
|
||||||
node.props.oldColor = node.props.color
|
vProps.oldColor = node.props.color
|
||||||
node.props.color = 'red'
|
vProps.color = 'red'
|
||||||
} else {
|
} else {
|
||||||
node.props.color = node.props.oldColor
|
vProps.color = vProps.oldColor
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.name) {
|
if (props.name) {
|
||||||
node.props.name = props.name
|
vProps.name = props.name
|
||||||
}
|
}
|
||||||
|
|
||||||
return node
|
return cloneVNode(node, vProps)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return () => h('div', { class: [ui.value.wrapper] }, [
|
return () => h('div', { class: [ui.value.wrapper] }, [
|
||||||
|
|||||||
@@ -9,21 +9,26 @@
|
|||||||
:required="required"
|
:required="required"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled || loading"
|
:disabled="disabled || loading"
|
||||||
:readonly="readonly"
|
class="form-input"
|
||||||
:autocomplete="autocomplete"
|
|
||||||
:spellcheck="spellcheck"
|
|
||||||
:class="inputClass"
|
:class="inputClass"
|
||||||
|
v-bind="$attrs"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
@focus="$emit('focus', $event)"
|
@focus="$emit('focus', $event)"
|
||||||
@blur="$emit('blur', $event)"
|
@blur="$emit('blur', $event)"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<div v-if="isLeading && leadingIconName" :class="leadingIconClass">
|
|
||||||
<UIcon :name="leadingIconName" :class="iconClass" />
|
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
|
||||||
</div>
|
<slot name="leading" :disabled="disabled" :loading="loading">
|
||||||
<div v-if="isTrailing && trailingIconName" :class="trailingIconClass">
|
<UIcon :name="leadingIconName" :class="leadingIconClass" />
|
||||||
<UIcon :name="trailingIconName" :class="iconClass" />
|
</slot>
|
||||||
</div>
|
</span>
|
||||||
|
|
||||||
|
<span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
|
||||||
|
<slot name="trailing" :disabled="disabled" :loading="loading">
|
||||||
|
<UIcon :name="trailingIconName" :class="trailingIconClass" />
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -44,6 +49,7 @@ export default defineComponent({
|
|||||||
components: {
|
components: {
|
||||||
UIcon
|
UIcon
|
||||||
},
|
},
|
||||||
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
@@ -69,22 +75,10 @@ export default defineComponent({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
readonly: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
autofocus: {
|
autofocus: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
autocomplete: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
spellcheck: {
|
|
||||||
type: Boolean,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
@@ -147,7 +141,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'focus', 'blur'],
|
emits: ['update:modelValue', 'focus', 'blur'],
|
||||||
setup (props, { emit }) {
|
setup (props, { emit, slots }) {
|
||||||
// TODO: Remove
|
// TODO: Remove
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
|
|
||||||
@@ -162,7 +156,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onInput = (event: InputEvent) => {
|
const onInput = (event: InputEvent) => {
|
||||||
emit('update:modelValue', (event.target as any).value)
|
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -179,11 +173,10 @@ export default defineComponent({
|
|||||||
ui.value.rounded,
|
ui.value.rounded,
|
||||||
ui.value.placeholder,
|
ui.value.placeholder,
|
||||||
ui.value.size[props.size],
|
ui.value.size[props.size],
|
||||||
props.padded && ui.value.padding[props.size],
|
props.padded ? ui.value.padding[props.size] : 'p-0',
|
||||||
variant?.replaceAll('{color}', props.color),
|
variant?.replaceAll('{color}', props.color),
|
||||||
isLeading.value && ui.value.leading.padding[props.size],
|
(isLeading.value || slots.leading) && ui.value.leading.padding[props.size],
|
||||||
isTrailing.value && ui.value.trailing.padding[props.size],
|
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[props.size]
|
||||||
ui.value.custom
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -211,7 +204,15 @@ export default defineComponent({
|
|||||||
return props.trailingIcon || props.icon
|
return props.trailingIcon || props.icon
|
||||||
})
|
})
|
||||||
|
|
||||||
const iconClass = computed(() => {
|
const leadingWrapperIconClass = computed(() => {
|
||||||
|
return classNames(
|
||||||
|
ui.value.icon.leading.wrapper,
|
||||||
|
ui.value.icon.leading.pointer,
|
||||||
|
ui.value.icon.leading.padding[props.size]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const leadingIconClass = computed(() => {
|
||||||
return classNames(
|
return classNames(
|
||||||
ui.value.icon.base,
|
ui.value.icon.base,
|
||||||
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||||
@@ -220,17 +221,20 @@ export default defineComponent({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const leadingIconClass = computed(() => {
|
const trailingWrapperIconClass = computed(() => {
|
||||||
return classNames(
|
return classNames(
|
||||||
ui.value.icon.leading.wrapper,
|
ui.value.icon.trailing.wrapper,
|
||||||
ui.value.icon.leading.padding[props.size]
|
ui.value.icon.trailing.pointer,
|
||||||
|
ui.value.icon.trailing.padding[props.size]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const trailingIconClass = computed(() => {
|
const trailingIconClass = computed(() => {
|
||||||
return classNames(
|
return classNames(
|
||||||
ui.value.icon.trailing.wrapper,
|
ui.value.icon.base,
|
||||||
ui.value.icon.trailing.padding[props.size]
|
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||||
|
ui.value.icon.size[props.size],
|
||||||
|
props.loading && !isLeading.value && 'animate-spin'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -241,11 +245,12 @@ export default defineComponent({
|
|||||||
isLeading,
|
isLeading,
|
||||||
isTrailing,
|
isTrailing,
|
||||||
inputClass,
|
inputClass,
|
||||||
iconClass,
|
|
||||||
leadingIconName,
|
leadingIconName,
|
||||||
leadingIconClass,
|
leadingIconClass,
|
||||||
|
leadingWrapperIconClass,
|
||||||
trailingIconName,
|
trailingIconName,
|
||||||
trailingIconClass,
|
trailingIconClass,
|
||||||
|
trailingWrapperIconClass,
|
||||||
onInput
|
onInput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
:value="value"
|
:value="value"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
type="radio"
|
type="radio"
|
||||||
:class="[ui.base, ui.custom]"
|
class="form-radio"
|
||||||
|
:class="[ui.base]"
|
||||||
|
v-bind="$attrs"
|
||||||
@focus="$emit('focus', $event)"
|
@focus="$emit('focus', $event)"
|
||||||
@blur="$emit('blur', $event)"
|
@blur="$emit('blur', $event)"
|
||||||
>
|
>
|
||||||
@@ -38,6 +40,7 @@ import appConfig from '#build/app.config'
|
|||||||
// const appConfig = useAppConfig()
|
// const appConfig = useAppConfig()
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: [String, Number, Boolean],
|
type: [String, Number, Boolean],
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
:name="name"
|
:name="name"
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
:required="required"
|
:required="required"
|
||||||
:disabled="disabled"
|
:disabled="disabled || loading"
|
||||||
|
class="form-select"
|
||||||
:class="selectClass"
|
:class="selectClass"
|
||||||
|
v-bind="$attrs"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
>
|
>
|
||||||
<template v-for="(option, index) in normalizedOptionsWithPlaceholder">
|
<template v-for="(option, index) in normalizedOptionsWithPlaceholder">
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
v-if="option.children"
|
v-if="option.children"
|
||||||
:key="`${option[valueAttribute]}-optgroup-${index}`"
|
:key="`${option[valueAttribute]}-optgroup-${index}`"
|
||||||
:value="option[valueAttribute]"
|
:value="option[valueAttribute]"
|
||||||
:label="option[textAttribute]"
|
:label="option[optionAttribute]"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="(childOption, index2) in option.children"
|
v-for="(childOption, index2) in option.children"
|
||||||
@@ -22,7 +24,7 @@
|
|||||||
:value="childOption[valueAttribute]"
|
:value="childOption[valueAttribute]"
|
||||||
:selected="childOption[valueAttribute] === normalizedValue"
|
:selected="childOption[valueAttribute] === normalizedValue"
|
||||||
:disabled="childOption.disabled"
|
:disabled="childOption.disabled"
|
||||||
v-text="childOption[textAttribute]"
|
v-text="childOption[optionAttribute]"
|
||||||
/>
|
/>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<option
|
<option
|
||||||
@@ -31,17 +33,21 @@
|
|||||||
:value="option[valueAttribute]"
|
:value="option[valueAttribute]"
|
||||||
:selected="option[valueAttribute] === normalizedValue"
|
:selected="option[valueAttribute] === normalizedValue"
|
||||||
:disabled="option.disabled"
|
:disabled="option.disabled"
|
||||||
v-text="option[textAttribute]"
|
v-text="option[optionAttribute]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div v-if="icon" :class="leadingIconClass">
|
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
|
||||||
<UIcon :name="icon" :class="iconClass" />
|
<slot name="leading" :disabled="disabled" :loading="loading">
|
||||||
</div>
|
<UIcon :name="leadingIconName" :class="leadingIconClass" />
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span v-if="trailingIcon" :class="trailingIconClass">
|
<span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
|
||||||
<UIcon :name="trailingIcon" :class="iconClass" aria-hidden="true" />
|
<slot name="trailing" :disabled="disabled" :loading="loading">
|
||||||
|
<UIcon :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
|
||||||
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -64,6 +70,7 @@ export default defineComponent({
|
|||||||
components: {
|
components: {
|
||||||
UIcon
|
UIcon
|
||||||
},
|
},
|
||||||
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: [String, Number, Object],
|
type: [String, Number, Object],
|
||||||
@@ -89,18 +96,38 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
loadingIcon: {
|
||||||
|
type: String,
|
||||||
|
default: () => appConfig.ui.input.default.loadingIcon
|
||||||
|
},
|
||||||
|
leadingIcon: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
trailingIcon: {
|
trailingIcon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: () => appConfig.ui.select.default.trailingIcon
|
default: () => appConfig.ui.select.default.trailingIcon
|
||||||
},
|
},
|
||||||
options: {
|
trailing: {
|
||||||
type: Array,
|
type: Boolean,
|
||||||
default: () => []
|
default: false
|
||||||
|
},
|
||||||
|
leading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
},
|
},
|
||||||
padded: {
|
padded: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: () => appConfig.ui.select.default.size,
|
default: () => appConfig.ui.select.default.size,
|
||||||
@@ -125,9 +152,9 @@ export default defineComponent({
|
|||||||
].includes(value)
|
].includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
textAttribute: {
|
optionAttribute: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'text'
|
default: 'label'
|
||||||
},
|
},
|
||||||
valueAttribute: {
|
valueAttribute: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -139,36 +166,36 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'focus', 'blur'],
|
emits: ['update:modelValue', 'focus', 'blur'],
|
||||||
setup (props, { emit }) {
|
setup (props, { emit, slots }) {
|
||||||
// TODO: Remove
|
// TODO: Remove
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
|
|
||||||
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.ui, appConfig.ui.select))
|
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.ui, appConfig.ui.select))
|
||||||
|
|
||||||
const onInput = (event: InputEvent) => {
|
const onInput = (event: InputEvent) => {
|
||||||
emit('update:modelValue', (event.target as any).value)
|
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const guessOptionValue = (option: any) => {
|
const guessOptionValue = (option: any) => {
|
||||||
return get(option, props.valueAttribute, get(option, props.textAttribute))
|
return get(option, props.valueAttribute, get(option, props.optionAttribute))
|
||||||
}
|
}
|
||||||
|
|
||||||
const guessOptionText = (option: any) => {
|
const guessOptionText = (option: any) => {
|
||||||
return get(option, props.textAttribute, get(option, props.valueAttribute))
|
return get(option, props.optionAttribute, get(option, props.valueAttribute))
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeOption = (option: any) => {
|
const normalizeOption = (option: any) => {
|
||||||
if (['string', 'number', 'boolean'].includes(typeof option)) {
|
if (['string', 'number', 'boolean'].includes(typeof option)) {
|
||||||
return {
|
return {
|
||||||
[props.valueAttribute]: option,
|
[props.valueAttribute]: option,
|
||||||
[props.textAttribute]: option
|
[props.optionAttribute]: option
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
[props.valueAttribute]: guessOptionValue(option),
|
[props.valueAttribute]: guessOptionValue(option),
|
||||||
[props.textAttribute]: guessOptionText(option)
|
[props.optionAttribute]: guessOptionText(option)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +211,7 @@ export default defineComponent({
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
[props.valueAttribute]: '',
|
[props.valueAttribute]: '',
|
||||||
[props.textAttribute]: props.placeholder,
|
[props.optionAttribute]: props.placeholder,
|
||||||
disabled: true
|
disabled: true
|
||||||
},
|
},
|
||||||
...normalizedOptions.value
|
...normalizedOptions.value
|
||||||
@@ -207,35 +234,69 @@ export default defineComponent({
|
|||||||
return classNames(
|
return classNames(
|
||||||
ui.value.base,
|
ui.value.base,
|
||||||
ui.value.rounded,
|
ui.value.rounded,
|
||||||
ui.value.placeholder,
|
|
||||||
ui.value.size[props.size],
|
ui.value.size[props.size],
|
||||||
props.padded && ui.value.padding[props.size],
|
props.padded ? ui.value.padding[props.size] : 'p-0',
|
||||||
variant?.replaceAll('{color}', props.color),
|
variant?.replaceAll('{color}', props.color),
|
||||||
!!props.icon && ui.value.leading.padding[props.size],
|
(isLeading.value || slots.leading) && ui.value.leading.padding[props.size],
|
||||||
ui.value.trailing.padding[props.size],
|
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[props.size]
|
||||||
ui.value.custom
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const iconClass = computed(() => {
|
const isLeading = computed(() => {
|
||||||
|
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
|
||||||
|
})
|
||||||
|
|
||||||
|
const isTrailing = computed(() => {
|
||||||
|
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
|
||||||
|
})
|
||||||
|
|
||||||
|
const leadingIconName = computed(() => {
|
||||||
|
if (props.loading) {
|
||||||
|
return props.loadingIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.leadingIcon || props.icon
|
||||||
|
})
|
||||||
|
|
||||||
|
const trailingIconName = computed(() => {
|
||||||
|
if (props.loading && !isLeading.value) {
|
||||||
|
return props.loadingIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.trailingIcon || props.icon
|
||||||
|
})
|
||||||
|
|
||||||
|
const leadingWrapperIconClass = computed(() => {
|
||||||
return classNames(
|
return classNames(
|
||||||
ui.value.icon.base,
|
ui.value.icon.leading.wrapper,
|
||||||
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
ui.value.icon.leading.pointer,
|
||||||
ui.value.icon.size[props.size]
|
ui.value.icon.leading.padding[props.size]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const leadingIconClass = computed(() => {
|
const leadingIconClass = computed(() => {
|
||||||
return classNames(
|
return classNames(
|
||||||
ui.value.icon.leading.wrapper,
|
ui.value.icon.base,
|
||||||
ui.value.icon.leading.padding[props.size]
|
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||||
|
ui.value.icon.size[props.size],
|
||||||
|
props.loading && 'animate-spin'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const trailingWrapperIconClass = computed(() => {
|
||||||
|
return classNames(
|
||||||
|
ui.value.icon.trailing.wrapper,
|
||||||
|
ui.value.icon.trailing.pointer,
|
||||||
|
ui.value.icon.trailing.padding[props.size]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const trailingIconClass = computed(() => {
|
const trailingIconClass = computed(() => {
|
||||||
return classNames(
|
return classNames(
|
||||||
ui.value.icon.trailing.wrapper,
|
ui.value.icon.base,
|
||||||
ui.value.icon.trailing.padding[props.size]
|
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
|
||||||
|
ui.value.icon.size[props.size],
|
||||||
|
props.loading && !isLeading.value && 'animate-spin'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -244,10 +305,15 @@ export default defineComponent({
|
|||||||
ui,
|
ui,
|
||||||
normalizedOptionsWithPlaceholder,
|
normalizedOptionsWithPlaceholder,
|
||||||
normalizedValue,
|
normalizedValue,
|
||||||
|
isLeading,
|
||||||
|
isTrailing,
|
||||||
selectClass,
|
selectClass,
|
||||||
iconClass,
|
leadingIconName,
|
||||||
leadingIconClass,
|
leadingIconClass,
|
||||||
|
leadingWrapperIconClass,
|
||||||
|
trailingIconName,
|
||||||
trailingIconClass,
|
trailingIconClass,
|
||||||
|
trailingWrapperIconClass,
|
||||||
onInput
|
onInput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
:name="name"
|
:name="name"
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
:multiple="multiple"
|
:multiple="multiple"
|
||||||
:disabled="disabled"
|
:disabled="disabled || loading"
|
||||||
as="div"
|
as="div"
|
||||||
:class="ui.wrapper"
|
:class="ui.wrapper"
|
||||||
@update:model-value="onUpdate"
|
@update:model-value="onUpdate"
|
||||||
@@ -27,19 +27,24 @@
|
|||||||
role="button"
|
role="button"
|
||||||
class="inline-flex w-full"
|
class="inline-flex w-full"
|
||||||
>
|
>
|
||||||
<slot :open="open" :disabled="disabled">
|
<slot :open="open" :disabled="disabled" :loading="loading">
|
||||||
<button :class="selectMenuClass" :disabled="disabled" type="button">
|
<button :class="selectMenuClass" :disabled="disabled || loading" type="button" v-bind="$attrs">
|
||||||
<span v-if="icon" :class="leadingIconClass">
|
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
|
||||||
<UIcon :name="icon" :class="iconClass" />
|
<slot name="leading" :disabled="disabled" :loading="loading">
|
||||||
|
<UIcon :name="leadingIconName" :class="leadingIconClass" />
|
||||||
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<slot name="label">
|
<slot name="label">
|
||||||
<span v-if="modelValue" class="block truncate">{{ typeof modelValue === 'string' ? modelValue : modelValue[optionAttribute] }}</span>
|
<span v-if="multiple && Array.isArray(modelValue) && modelValue.length" class="block truncate">{{ modelValue.length }} selected</span>
|
||||||
<span v-else class="block truncate text-gray-400 dark:text-gray-500">{{ placeholder || ' ' }}</span>
|
<span v-else-if="!multiple && modelValue" class="block truncate">{{ typeof modelValue === 'string' ? modelValue : modelValue[optionAttribute] }}</span>
|
||||||
|
<span v-else class="block truncate" :class="ui.placeholder">{{ placeholder || ' ' }}</span>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span v-if="trailingIcon" :class="trailingIconClass">
|
<span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
|
||||||
<UIcon :name="trailingIcon" :class="iconClass" aria-hidden="true" />
|
<slot name="trailing" :disabled="disabled" :loading="loading">
|
||||||
|
<UIcon :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
|
||||||
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</slot>
|
</slot>
|
||||||
@@ -141,6 +146,7 @@ export default defineComponent({
|
|||||||
UIcon,
|
UIcon,
|
||||||
UAvatar
|
UAvatar
|
||||||
},
|
},
|
||||||
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: [String, Number, Object, Array],
|
type: [String, Number, Object, Array],
|
||||||
@@ -166,10 +172,30 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
loadingIcon: {
|
||||||
|
type: String,
|
||||||
|
default: () => appConfig.ui.input.default.loadingIcon
|
||||||
|
},
|
||||||
|
leadingIcon: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
trailingIcon: {
|
trailingIcon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: () => appConfig.ui.select.default.trailingIcon
|
default: () => appConfig.ui.select.default.trailingIcon
|
||||||
},
|
},
|
||||||
|
trailing: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
leading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
selectedIcon: {
|
selectedIcon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: () => appConfig.ui.selectMenu.default.selectedIcon
|
default: () => appConfig.ui.selectMenu.default.selectedIcon
|
||||||
@@ -248,7 +274,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'open', 'close'],
|
emits: ['update:modelValue', 'open', 'close'],
|
||||||
setup (props, { emit }) {
|
setup (props, { emit, slots }) {
|
||||||
// TODO: Remove
|
// TODO: Remove
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
|
|
||||||
@@ -268,38 +294,72 @@ export default defineComponent({
|
|||||||
return classNames(
|
return classNames(
|
||||||
uiSelect.value.base,
|
uiSelect.value.base,
|
||||||
uiSelect.value.rounded,
|
uiSelect.value.rounded,
|
||||||
uiSelect.value.placeholder,
|
|
||||||
'text-left cursor-default',
|
'text-left cursor-default',
|
||||||
uiSelect.value.size[props.size],
|
uiSelect.value.size[props.size],
|
||||||
uiSelect.value.gap[props.size],
|
uiSelect.value.gap[props.size],
|
||||||
props.padded && uiSelect.value.padding[props.size],
|
props.padded ? uiSelect.value.padding[props.size] : 'p-0',
|
||||||
variant?.replaceAll('{color}', props.color),
|
variant?.replaceAll('{color}', props.color),
|
||||||
!!props.icon && uiSelect.value.leading.padding[props.size],
|
(isLeading.value || slots.leading) && uiSelect.value.leading.padding[props.size],
|
||||||
uiSelect.value.trailing.padding[props.size],
|
(isTrailing.value || slots.trailing) && uiSelect.value.trailing.padding[props.size],
|
||||||
uiSelect.value.custom,
|
|
||||||
'inline-flex items-center'
|
'inline-flex items-center'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const iconClass = computed(() => {
|
const isLeading = computed(() => {
|
||||||
|
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
|
||||||
|
})
|
||||||
|
|
||||||
|
const isTrailing = computed(() => {
|
||||||
|
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
|
||||||
|
})
|
||||||
|
|
||||||
|
const leadingIconName = computed(() => {
|
||||||
|
if (props.loading) {
|
||||||
|
return props.loadingIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.leadingIcon || props.icon
|
||||||
|
})
|
||||||
|
|
||||||
|
const trailingIconName = computed(() => {
|
||||||
|
if (props.loading && !isLeading.value) {
|
||||||
|
return props.loadingIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.trailingIcon || props.icon
|
||||||
|
})
|
||||||
|
|
||||||
|
const leadingWrapperIconClass = computed(() => {
|
||||||
return classNames(
|
return classNames(
|
||||||
uiSelect.value.icon.base,
|
uiSelect.value.icon.leading.wrapper,
|
||||||
appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
|
uiSelect.value.icon.leading.pointer,
|
||||||
uiSelect.value.icon.size[props.size]
|
uiSelect.value.icon.leading.padding[props.size]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const leadingIconClass = computed(() => {
|
const leadingIconClass = computed(() => {
|
||||||
return classNames(
|
return classNames(
|
||||||
uiSelect.value.icon.leading.wrapper,
|
uiSelect.value.icon.base,
|
||||||
uiSelect.value.icon.leading.padding[props.size]
|
appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
|
||||||
|
uiSelect.value.icon.size[props.size],
|
||||||
|
props.loading && 'animate-spin'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const trailingWrapperIconClass = computed(() => {
|
||||||
|
return classNames(
|
||||||
|
uiSelect.value.icon.trailing.wrapper,
|
||||||
|
uiSelect.value.icon.trailing.pointer,
|
||||||
|
uiSelect.value.icon.trailing.padding[props.size]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const trailingIconClass = computed(() => {
|
const trailingIconClass = computed(() => {
|
||||||
return classNames(
|
return classNames(
|
||||||
uiSelect.value.icon.trailing.wrapper,
|
uiSelect.value.icon.base,
|
||||||
uiSelect.value.icon.trailing.padding[props.size]
|
appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
|
||||||
|
uiSelect.value.icon.size[props.size],
|
||||||
|
props.loading && !isLeading.value && 'animate-spin'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -339,10 +399,15 @@ export default defineComponent({
|
|||||||
ui,
|
ui,
|
||||||
trigger,
|
trigger,
|
||||||
container,
|
container,
|
||||||
|
isLeading,
|
||||||
|
isTrailing,
|
||||||
selectMenuClass,
|
selectMenuClass,
|
||||||
iconClass,
|
leadingIconName,
|
||||||
leadingIconClass,
|
leadingIconClass,
|
||||||
|
leadingWrapperIconClass,
|
||||||
|
trailingIconName,
|
||||||
trailingIconClass,
|
trailingIconClass,
|
||||||
|
trailingWrapperIconClass,
|
||||||
filteredOptions,
|
filteredOptions,
|
||||||
queryOption,
|
queryOption,
|
||||||
query,
|
query,
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
:required="required"
|
:required="required"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:autocomplete="autocomplete"
|
class="form-textarea"
|
||||||
:class="textareaClass"
|
:class="textareaClass"
|
||||||
|
v-bind="$attrs"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
@focus="$emit('focus', $event)"
|
@focus="$emit('focus', $event)"
|
||||||
@blur="$emit('blur', $event)"
|
@blur="$emit('blur', $event)"
|
||||||
@@ -31,6 +32,7 @@ import appConfig from '#build/app.config'
|
|||||||
// const appConfig = useAppConfig()
|
// const appConfig = useAppConfig()
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
@@ -64,10 +66,6 @@ export default defineComponent({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
autocomplete: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
resize: {
|
resize: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@@ -145,7 +143,7 @@ export default defineComponent({
|
|||||||
const onInput = (event: InputEvent) => {
|
const onInput = (event: InputEvent) => {
|
||||||
autoResize()
|
autoResize()
|
||||||
|
|
||||||
emit('update:modelValue', (event.target as any).value)
|
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.modelValue, () => {
|
watch(() => props.modelValue, () => {
|
||||||
@@ -167,10 +165,9 @@ export default defineComponent({
|
|||||||
ui.value.rounded,
|
ui.value.rounded,
|
||||||
ui.value.placeholder,
|
ui.value.placeholder,
|
||||||
ui.value.size[props.size],
|
ui.value.size[props.size],
|
||||||
props.padded && ui.value.padding[props.size],
|
props.padded ? ui.value.padding[props.size] : 'p-0',
|
||||||
variant?.replaceAll('{color}', props.color),
|
variant?.replaceAll('{color}', props.color),
|
||||||
!props.resize && 'resize-none',
|
!props.resize && 'resize-none'
|
||||||
ui.value.custom
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<Switch
|
<Switch
|
||||||
v-model="active"
|
v-model="active"
|
||||||
|
:name="name"
|
||||||
|
:disabled="disabled"
|
||||||
:class="[active ? ui.active : ui.inactive, ui.base]"
|
:class="[active ? ui.active : ui.inactive, ui.base]"
|
||||||
>
|
>
|
||||||
<span :class="[active ? ui.container.active : ui.container.inactive, ui.container.base]">
|
<span :class="[active ? ui.container.active : ui.container.inactive, ui.container.base]">
|
||||||
<span v-if="iconOn" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
|
<span v-if="onIcon" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
|
||||||
<UIcon :name="iconOn" :class="ui.icon.on" />
|
<UIcon :name="onIcon" :class="ui.icon.on" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="iconOff" :class="[active ? ui.icon.inactive : ui.icon.active, ui.icon.base]" aria-hidden="true">
|
<span v-if="offIcon" :class="[active ? ui.icon.inactive : ui.icon.active, ui.icon.base]" aria-hidden="true">
|
||||||
<UIcon :name="iconOff" :class="ui.icon.off" />
|
<UIcon :name="offIcon" :class="ui.icon.off" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -34,17 +36,25 @@ export default defineComponent({
|
|||||||
UIcon
|
UIcon
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
iconOn: {
|
disabled: {
|
||||||
type: String,
|
type: Boolean,
|
||||||
default: null
|
default: false
|
||||||
},
|
},
|
||||||
iconOff: {
|
onIcon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: () => appConfig.ui.toggle.default.onIcon
|
||||||
|
},
|
||||||
|
offIcon: {
|
||||||
|
type: String,
|
||||||
|
default: () => appConfig.ui.toggle.default.offIcon
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
type: Object as PropType<Partial<typeof appConfig.ui.toggle>>,
|
type: Object as PropType<Partial<typeof appConfig.ui.toggle>>,
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<UButton
|
<UButton
|
||||||
v-if="close"
|
v-if="closeButton"
|
||||||
v-bind="close"
|
v-bind="{ ...ui.default.closeButton, ...closeButton }"
|
||||||
:class="ui.input.close"
|
:class="ui.input.closeButton"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
@click="onClear"
|
@click="onClear"
|
||||||
/>
|
/>
|
||||||
@@ -51,12 +51,16 @@
|
|||||||
</CommandPaletteGroup>
|
</CommandPaletteGroup>
|
||||||
</ComboboxOptions>
|
</ComboboxOptions>
|
||||||
|
|
||||||
<div v-else-if="empty" :class="ui.empty.wrapper">
|
<template v-else-if="emptyState">
|
||||||
<UIcon v-if="empty.icon" :name="empty.icon" :class="ui.empty.icon" aria-hidden="true" />
|
<slot name="empty-state">
|
||||||
<p :class="query ? ui.empty.queryLabel : ui.empty.label">
|
<div :class="ui.emptyState.wrapper">
|
||||||
{{ query ? empty.queryLabel : empty.label }}
|
<UIcon v-if="emptyState.icon" :name="emptyState.icon" :class="ui.emptyState.icon" aria-hidden="true" />
|
||||||
</p>
|
<p :class="query ? ui.emptyState.queryLabel : ui.emptyState.label">
|
||||||
</div>
|
{{ query ? emptyState.queryLabel : emptyState.label }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</template>
|
</template>
|
||||||
@@ -133,13 +137,13 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: () => appConfig.ui.commandPalette.default.selectedIcon
|
default: () => appConfig.ui.commandPalette.default.selectedIcon
|
||||||
},
|
},
|
||||||
close: {
|
closeButton: {
|
||||||
type: Object as PropType<Partial<Button>>,
|
type: Object as PropType<Partial<Button>>,
|
||||||
default: () => appConfig.ui.commandPalette.default.close
|
default: () => appConfig.ui.commandPalette.default.closeButton
|
||||||
},
|
},
|
||||||
empty: {
|
emptyState: {
|
||||||
type: Object as PropType<{ icon: string, label: string, queryLabel: string }>,
|
type: Object as PropType<{ icon: string, label: string, queryLabel: string }>,
|
||||||
default: () => appConfig.ui.commandPalette.default.empty
|
default: () => appConfig.ui.commandPalette.default.emptyState
|
||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -280,6 +284,8 @@ export default defineComponent({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emptyState = computed(() => ({ ...ui.value.default.emptyState, ...props.emptyState }))
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
|
|
||||||
function activateFirstOption () {
|
function activateFirstOption () {
|
||||||
@@ -327,6 +333,8 @@ export default defineComponent({
|
|||||||
query,
|
query,
|
||||||
iconName,
|
iconName,
|
||||||
iconClass,
|
iconClass,
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
emptyState,
|
||||||
onSelect,
|
onSelect,
|
||||||
onClear
|
onClear
|
||||||
}
|
}
|
||||||
|
|||||||
218
src/runtime/components/navigation/Pagination.vue
Normal file
218
src/runtime/components/navigation/Pagination.vue
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="ui.wrapper">
|
||||||
|
<slot name="prev" :on-click="onClickPrev">
|
||||||
|
<UButton
|
||||||
|
v-if="prevButton"
|
||||||
|
:size="size"
|
||||||
|
:disabled="!canGoPrev"
|
||||||
|
:class="[ui.base, ui.rounded]"
|
||||||
|
v-bind="{ ...ui.default.prevButton, ...prevButton }"
|
||||||
|
:ui="{ rounded: '' }"
|
||||||
|
@click="onClickPrev"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
v-for="(page, index) of displayedPages"
|
||||||
|
:key="index"
|
||||||
|
:size="size"
|
||||||
|
:label="`${page}`"
|
||||||
|
v-bind="page === currentPage ? { ...ui.default.activeButton, ...activeButton } : { ...ui.default.inactiveButton, ...inactiveButton }"
|
||||||
|
:class="[{ 'pointer-events-none': typeof page === 'string', 'z-[1]': page === currentPage }, ui.base, ui.rounded]"
|
||||||
|
:ui="{ rounded: '' }"
|
||||||
|
@click="() => onClickPage(page)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<slot name="next" :on-click="onClickNext">
|
||||||
|
<UButton
|
||||||
|
v-if="nextButton"
|
||||||
|
:size="size"
|
||||||
|
:disabled="!canGoNext"
|
||||||
|
:class="[ui.base, ui.rounded]"
|
||||||
|
v-bind="{ ...ui.default.nextButton, ...nextButton }"
|
||||||
|
:ui="{ rounded: '' }"
|
||||||
|
@click="onClickNext"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent } from 'vue'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { defu } from 'defu'
|
||||||
|
import UButton from '../elements/Button.vue'
|
||||||
|
import type { Button } from '../../types/button'
|
||||||
|
import { useAppConfig } from '#imports'
|
||||||
|
// TODO: Remove
|
||||||
|
// @ts-expect-error
|
||||||
|
import appConfig from '#build/app.config'
|
||||||
|
|
||||||
|
// const appConfig = useAppConfig()
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
UButton
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
pageCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 10
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: Number,
|
||||||
|
default: 7,
|
||||||
|
validate (value) {
|
||||||
|
return value >= 7 && value < Number.MAX_VALUE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: () => appConfig.ui.pagination.default.size,
|
||||||
|
validator (value: string) {
|
||||||
|
return Object.keys(appConfig.ui.button.size).includes(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
activeButton: {
|
||||||
|
type: Object as PropType<Partial<Button>>,
|
||||||
|
default: () => appConfig.ui.pagination.default.activeButton
|
||||||
|
},
|
||||||
|
inactiveButton: {
|
||||||
|
type: Object as PropType<Partial<Button>>,
|
||||||
|
default: () => appConfig.ui.pagination.default.inactiveButton
|
||||||
|
},
|
||||||
|
prevButton: {
|
||||||
|
type: Object as PropType<Partial<Button>>,
|
||||||
|
default: () => appConfig.ui.pagination.default.prevButton
|
||||||
|
},
|
||||||
|
nextButton: {
|
||||||
|
type: Object as PropType<Partial<Button>>,
|
||||||
|
default: () => appConfig.ui.pagination.default.nextButton
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
type: String,
|
||||||
|
default: '…'
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
type: Object as PropType<Partial<typeof appConfig.ui.pagination>>,
|
||||||
|
default: () => appConfig.ui.pagination
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup (props, { emit }) {
|
||||||
|
// TODO: Remove
|
||||||
|
const appConfig = useAppConfig()
|
||||||
|
|
||||||
|
const ui = computed<Partial<typeof appConfig.ui.pagination>>(() => defu({}, props.ui, appConfig.ui.pagination))
|
||||||
|
|
||||||
|
const currentPage = computed({
|
||||||
|
get () {
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const pages = computed(() => Array.from({ length: Math.ceil(props.total / props.pageCount) }, (_, i) => i + 1))
|
||||||
|
|
||||||
|
const displayedPages = computed(() => {
|
||||||
|
if (!props.max || pages.value.length <= 5) {
|
||||||
|
return pages.value
|
||||||
|
} else {
|
||||||
|
const current = currentPage.value
|
||||||
|
const max = pages.value.length
|
||||||
|
const r = Math.floor((Math.min(props.max, max) - 5) / 2)
|
||||||
|
const r1 = current - r
|
||||||
|
const r2 = current + r
|
||||||
|
const beforeWrapped = r1 - 1 > 1
|
||||||
|
const afterWrapped = r2 + 1 < max
|
||||||
|
const items: Array<number | string> = [1]
|
||||||
|
|
||||||
|
if (beforeWrapped) items.push(props.divider)
|
||||||
|
|
||||||
|
if (!afterWrapped) {
|
||||||
|
const addedItems = (current + r + 2) - max
|
||||||
|
for (let i = current - r - addedItems; i <= current - r - 1; i++) {
|
||||||
|
items.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = r1 > 2 ? (r1) : 2; i <= Math.min(max, r2); i++) {
|
||||||
|
items.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!beforeWrapped) {
|
||||||
|
const addedItems = 1 - (current - r - 2)
|
||||||
|
for (let i = current + r + 1; i <= current + r + addedItems; i++) {
|
||||||
|
items.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterWrapped) items.push(props.divider)
|
||||||
|
|
||||||
|
if (r2 < max) items.push(max)
|
||||||
|
|
||||||
|
// Replace divider by number on start edge case [1, '…', 3, ...]
|
||||||
|
if (items.length >= 3 && items[1] === props.divider && items[2] === 3) {
|
||||||
|
items[1] = 2
|
||||||
|
}
|
||||||
|
// Replace divider by number on end edge case [..., 48, '…', 50]
|
||||||
|
if (items.length >= 3 && items[items.length - 2] === props.divider && items[items.length - 1] === items.length) {
|
||||||
|
items[items.length - 2] = items.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const canGoPrev = computed(() => currentPage.value > 1)
|
||||||
|
const canGoNext = computed(() => currentPage.value < pages.value.length)
|
||||||
|
|
||||||
|
function onClickPage (page: number | string) {
|
||||||
|
if (typeof page === 'string') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage.value = page
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickPrev () {
|
||||||
|
if (!canGoPrev.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage.value--
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickNext () {
|
||||||
|
if (!canGoNext.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
ui,
|
||||||
|
currentPage,
|
||||||
|
pages,
|
||||||
|
displayedPages,
|
||||||
|
canGoPrev,
|
||||||
|
canGoNext,
|
||||||
|
onClickPrev,
|
||||||
|
onClickNext,
|
||||||
|
onClickPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -42,10 +42,10 @@ import { computed, defineComponent } from 'vue'
|
|||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import type { RouteLocationRaw } from 'vue-router'
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
|
import { omit } from 'lodash-es'
|
||||||
import UIcon from '../elements/Icon.vue'
|
import UIcon from '../elements/Icon.vue'
|
||||||
import UAvatar from '../elements/Avatar.vue'
|
import UAvatar from '../elements/Avatar.vue'
|
||||||
import ULinkCustom from '../elements/LinkCustom.vue'
|
import ULinkCustom from '../elements/LinkCustom.vue'
|
||||||
import { omit } from '../../utils'
|
|
||||||
import type { Avatar } from '../../types/avatar'
|
import type { Avatar } from '../../types/avatar'
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
// TODO: Remove
|
// TODO: Remove
|
||||||
|
|||||||
@@ -16,15 +16,15 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-if="description && actions.length" class="mt-3 flex items-center gap-2">
|
<div v-if="description && actions.length" class="mt-3 flex items-center gap-2">
|
||||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.action, ...action }" @click.stop="onAction(action)" />
|
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0 flex items-center gap-3">
|
<div class="flex-shrink-0 flex items-center gap-3">
|
||||||
<div v-if="!description && actions.length" class="flex items-center gap-2">
|
<div v-if="!description && actions.length" class="flex items-center gap-2">
|
||||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.action, ...action }" @click.stop="onAction(action)" />
|
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UButton v-if="close" v-bind="{ ...ui.default.close, ...close }" @click.stop="onClose" />
|
<UButton v-if="closeButton" v-bind="{ ...ui.default.closeButton, ...closeButton }" @click.stop="onClose" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,9 +80,9 @@ export default defineComponent({
|
|||||||
type: Object as PropType<Partial<Avatar>>,
|
type: Object as PropType<Partial<Avatar>>,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
close: {
|
closeButton: {
|
||||||
type: Object as PropType<Partial<Button>>,
|
type: Object as PropType<Partial<Button>>,
|
||||||
default: () => appConfig.ui.notification.default.close
|
default: () => appConfig.ui.notification.default.closeButton
|
||||||
},
|
},
|
||||||
timeout: {
|
timeout: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -134,7 +134,7 @@ export default defineComponent({
|
|||||||
const iconClass = computed(() => {
|
const iconClass = computed(() => {
|
||||||
return classNames(
|
return classNames(
|
||||||
ui.value.icon.base,
|
ui.value.icon.base,
|
||||||
appConfig.ui.colors.includes(props.color) && ui.value.icon.color?.replaceAll('{color}', props.color)
|
ui.value.icon.color?.replaceAll('{color}', props.color)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ export const defineShortcuts = (config: ShortcutsConfig) => {
|
|||||||
let shortcuts: Shortcut[] = []
|
let shortcuts: Shortcut[] = []
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// Input autocomplete triggers a keydown event
|
||||||
|
if (!e.key) { return }
|
||||||
|
|
||||||
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key)
|
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key)
|
||||||
|
|
||||||
for (const shortcut of shortcuts) {
|
for (const shortcut of shortcuts) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { hexToRgb } from '../utils/colors'
|
import { hexToRgb } from '../utils'
|
||||||
import { defineNuxtPlugin, useHead, useAppConfig, useNuxtApp } from '#imports'
|
import { defineNuxtPlugin, useHead, useAppConfig, useNuxtApp } from '#imports'
|
||||||
import colors from '#tailwind-config/theme/colors'
|
import colors from '#tailwind-config/theme/colors'
|
||||||
|
|
||||||
|
|||||||
2
src/runtime/types/notification.d.ts
vendored
2
src/runtime/types/notification.d.ts
vendored
@@ -12,7 +12,7 @@ export interface Notification {
|
|||||||
description: string
|
description: string
|
||||||
icon?: string
|
icon?: string
|
||||||
avatar?: Partial<Avatar>
|
avatar?: Partial<Avatar>
|
||||||
close?: Partial<Button>
|
closeButton?: Partial<Button>
|
||||||
timeout: number
|
timeout: number
|
||||||
actions?: NotificationAction[]
|
actions?: NotificationAction[]
|
||||||
click?: Function
|
click?: Function
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { omit, kebabCase } from './index'
|
|
||||||
|
|
||||||
export const colorsToExclude = [
|
|
||||||
'inherit',
|
|
||||||
'transparent',
|
|
||||||
'current',
|
|
||||||
'white',
|
|
||||||
'black',
|
|
||||||
'slate',
|
|
||||||
'gray',
|
|
||||||
'zinc',
|
|
||||||
'neutral',
|
|
||||||
'stone',
|
|
||||||
'cool'
|
|
||||||
]
|
|
||||||
|
|
||||||
export const excludeColors = (colors: object) => Object.keys(omit(colors, colorsToExclude)).map(color => kebabCase(color)) as string[]
|
|
||||||
|
|
||||||
export const colorsAsRegex = (colors: string[]): string => colors.join('|')
|
|
||||||
|
|
||||||
export const hexToRgb = (hex) => {
|
|
||||||
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
|
||||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
|
|
||||||
hex = hex.replace(shorthandRegex, function (_, r, g, b) {
|
|
||||||
return r + r + g + g + b + b
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
|
||||||
return result
|
|
||||||
? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}`
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
@@ -2,30 +2,34 @@ export function classNames (...classes: any[string]) {
|
|||||||
return classes.filter(Boolean).join(' ')
|
return classes.filter(Boolean).join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const kebabCase = (str: string) => {
|
export const hexToRgb = (hex) => {
|
||||||
return str
|
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
||||||
?.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
|
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
|
||||||
?.map(x => x.toLowerCase())
|
hex = hex.replace(shorthandRegex, function (_, r, g, b) {
|
||||||
?.join('-')
|
return r + r + g + g + b + b
|
||||||
}
|
})
|
||||||
|
|
||||||
export const omit = (obj: object, keys: string[]) => {
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||||
return Object.fromEntries(
|
return result
|
||||||
Object.entries(obj).filter(([key]) => !keys.includes(key))
|
? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}`
|
||||||
)
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSlotsChildren = (slots: any) => {
|
export const getSlotsChildren = (slots: any) => {
|
||||||
let children = slots.default?.()
|
let children = slots.default?.()
|
||||||
if (children.length) {
|
if (children.length) {
|
||||||
if (typeof children[0].type === 'symbol') {
|
children = children.flatMap(c => {
|
||||||
// @ts-ignore-next
|
if (typeof c.type === 'symbol') {
|
||||||
children = children[0].children
|
if (typeof c.children === 'string') {
|
||||||
// @ts-ignore-next
|
// `v-if="false"` or commented node
|
||||||
} else if (children[0].type.name === 'ContentSlot') {
|
return
|
||||||
// @ts-ignore-next
|
}
|
||||||
children = children[0].ctx.slots.default?.()
|
return c.children
|
||||||
}
|
} else if (c.type.name === 'ContentSlot') {
|
||||||
|
return c.ctx.slots.default?.()
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}).filter(Boolean)
|
||||||
}
|
}
|
||||||
return children
|
return children
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user