mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 20:48:12 +01:00
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6736d1efd | ||
|
|
f6e695ffc8 | ||
|
|
e8898d15a6 | ||
|
|
f65aefb706 | ||
|
|
9b9ccdb59e | ||
|
|
688232215d | ||
|
|
ebfb835033 | ||
|
|
838d6c832f | ||
|
|
e7c2f7856c | ||
|
|
1d5bd89d58 | ||
|
|
6c124bb1ac | ||
|
|
49174b7628 | ||
|
|
50ad14f9df | ||
|
|
6e2678d1d8 | ||
|
|
831c560a96 | ||
|
|
06990beabf | ||
|
|
3ebff4d133 | ||
|
|
d66cfa9d7d | ||
|
|
75c0d9e31f | ||
|
|
6033872ef8 | ||
|
|
838cb7212a | ||
|
|
c8dd71c4f5 | ||
|
|
4f0d00f7a6 | ||
|
|
3b975634e8 | ||
|
|
249bbd49dc | ||
|
|
3c1602af37 | ||
|
|
e1ca6e0cde | ||
|
|
3b3bd16afe | ||
|
|
fab9cbebd8 | ||
|
|
581b470cc7 | ||
|
|
24d30cd1f3 | ||
|
|
cc52bffccf | ||
|
|
eb2601d4da | ||
|
|
f726b5f094 | ||
|
|
37ce62acb9 | ||
|
|
f97b728968 | ||
|
|
7e6ba78681 | ||
|
|
ed5c74dc17 | ||
|
|
bb3ea40218 | ||
|
|
821e15b696 | ||
|
|
bd3fa8658f | ||
|
|
82d619b2a7 | ||
|
|
8d9d9736ba | ||
|
|
3fca66857d | ||
|
|
4853520eb3 | ||
|
|
5481dab53d | ||
|
|
6f60fa9a98 | ||
|
|
cba9ad78db | ||
|
|
bbc8f4e6ac | ||
|
|
ed3a3babdb | ||
|
|
4415d4111e | ||
|
|
c75c0152ce | ||
|
|
993bb89e02 | ||
|
|
9f01145bc6 | ||
|
|
c1d9e0ecd4 | ||
|
|
f610c96a0b | ||
|
|
8b546600db | ||
|
|
f08471ccda | ||
|
|
d19d7077e4 | ||
|
|
07a4d13c0f | ||
|
|
9e90d1768b | ||
|
|
91e5002050 | ||
|
|
eb68d0d453 | ||
|
|
2bdb5d2b42 | ||
|
|
b62cd7905d | ||
|
|
58faa1053b | ||
|
|
e909884d03 | ||
|
|
5e84fd0570 | ||
|
|
98c0f567fc | ||
|
|
379d20fc3c | ||
|
|
c12f94653e | ||
|
|
2392b4aa40 | ||
|
|
36055ba978 | ||
|
|
73541f2d4f | ||
|
|
03030ab1db | ||
|
|
c98d6e31c0 | ||
|
|
49b73aa024 | ||
|
|
bd8b737642 | ||
|
|
dd8a122933 | ||
|
|
0b799e4300 | ||
|
|
8517897c34 | ||
|
|
72889535e7 | ||
|
|
878f7078a2 | ||
|
|
bd8118c124 | ||
|
|
3d5ffe76ef | ||
|
|
c49f8999d3 | ||
|
|
cc62e345eb | ||
|
|
ae58d5c2b9 | ||
|
|
92e736213b | ||
|
|
7d6b5c358f | ||
|
|
f854746bd8 | ||
|
|
e1e05af0ba | ||
|
|
224ec3c1fb | ||
|
|
c3ac4badbf | ||
|
|
398c5d5dcd | ||
|
|
e7697595c8 | ||
|
|
b0ecac563c | ||
|
|
5cb45c52c2 | ||
|
|
0453af65fa | ||
|
|
53cfea40a4 | ||
|
|
386e51d159 | ||
|
|
eb8eec09c5 | ||
|
|
4a4ddbd5cb | ||
|
|
a563d8fed4 | ||
|
|
7658211537 | ||
|
|
e736ecafff | ||
|
|
cee3e126a4 | ||
|
|
f4a48f6016 | ||
|
|
877b22c294 | ||
|
|
8cc8e45b4f | ||
|
|
3f67b9209c | ||
|
|
a2b8b700df | ||
|
|
80cc59375f | ||
|
|
2bb911023c | ||
|
|
ab355a3576 | ||
|
|
6c02d1c704 | ||
|
|
66c3631b3d | ||
|
|
435ef30f26 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"commitMessage": "chore(release): ${version}",
|
||||
"commitMessage": "chore(release): v${version}",
|
||||
"tagName": "v${version}"
|
||||
},
|
||||
"npm": {
|
||||
|
||||
108
CHANGELOG.md
108
CHANGELOG.md
@@ -1,5 +1,113 @@
|
||||
# Changelog
|
||||
|
||||
## [2.17.0](https://github.com/nuxt/ui/compare/v2.16.0...v2.17.0) (2024-06-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Alert:** add `actions` slot ([#1785](https://github.com/nuxt/ui/issues/1785)) ([c8dd71c](https://github.com/nuxt/ui/commit/c8dd71c4f5a5239811b07b50f1dc802101af07d5))
|
||||
* **Form:** update and migrate `valibot` to v0.31.0 ([#1848](https://github.com/nuxt/ui/issues/1848)) ([1d5bd89](https://github.com/nuxt/ui/commit/1d5bd89d5881163fc6dc917e138b9d8304dff6c4))
|
||||
* **Notification:** allow ring customization with `{color}` ([#1830](https://github.com/nuxt/ui/issues/1830)) ([3ebff4d](https://github.com/nuxt/ui/commit/3ebff4d133372e339e2c4c439576e9e192b29cc3))
|
||||
* **Slideover:** handle `top` and `bottom` side ([#1834](https://github.com/nuxt/ui/issues/1834)) ([50ad14f](https://github.com/nuxt/ui/commit/50ad14f9dffe4f76bef888cd10d30b417c75bca5))
|
||||
* **Tabs:** add `content` prop to avoid the render of the HTML markup ([#1831](https://github.com/nuxt/ui/issues/1831)) ([6e2678d](https://github.com/nuxt/ui/commit/6e2678d1d8a498322eb3eff909ccbba55e40a2b7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Alert/Notification:** use `div` for description ([e8898d1](https://github.com/nuxt/ui/commit/e8898d15a667ba66e78828315e3cc4e92845cd3f)), closes [#1551](https://github.com/nuxt/ui/issues/1551)
|
||||
* **Alert:** base style not applied on icon ([#1859](https://github.com/nuxt/ui/issues/1859)) ([f65aefb](https://github.com/nuxt/ui/commit/f65aefb7067c1c64c1355b5d699129e716ef1281))
|
||||
* **Breadcrumb:** allow `aria-current` to be overrideable ([ebfb835](https://github.com/nuxt/ui/commit/ebfb8350339725c0a6f88c73f16bff01d61538c2)), closes [#1856](https://github.com/nuxt/ui/issues/1856)
|
||||
* **Carousel:** prevent mouse click when dragging ([#1781](https://github.com/nuxt/ui/issues/1781)) ([4f0d00f](https://github.com/nuxt/ui/commit/4f0d00f7a6eebf05adceaf1e7c2869ad91949cf3))
|
||||
* **CommandPalette:** hide `empty-state` when `null` ([249bbd4](https://github.com/nuxt/ui/commit/249bbd49dc8420603e8d561543d237abeb400908)), closes [#1787](https://github.com/nuxt/ui/issues/1787)
|
||||
* **Form:** maintain other errors when using `setErrors` with a path ([#1818](https://github.com/nuxt/ui/issues/1818)) ([06990be](https://github.com/nuxt/ui/commit/06990beabf67f668322b4d3fb2ec93cc4f3bdcd4))
|
||||
* **Input:** hide wrapper when type is `hidden` ([#1797](https://github.com/nuxt/ui/issues/1797)) ([e7c2f78](https://github.com/nuxt/ui/commit/e7c2f7856c05ed96f48c83d64d8e1d3f41ab58fe))
|
||||
* **Link:** typo in `exactHash` type ([581b470](https://github.com/nuxt/ui/commit/581b470cc79c2315bb2d56e02a7c134a7861c616)), closes [#1767](https://github.com/nuxt/ui/issues/1767)
|
||||
* **SelectMenu:** wrong placeholder color when `modelValue` is an empty string ([9b9ccdb](https://github.com/nuxt/ui/commit/9b9ccdb59e98fed096dd18809af646b10de46b9f)), closes [#1862](https://github.com/nuxt/ui/issues/1862)
|
||||
* **Select:** remove defaults for `value` and `text` ([6c124bb](https://github.com/nuxt/ui/commit/6c124bb1ac2fef116161da56a3a8e5f92144ce3a)), closes [#1702](https://github.com/nuxt/ui/issues/1702)
|
||||
|
||||
## [2.16.0](https://github.com/nuxt/ui/compare/v2.15.2...v2.16.0) (2024-05-07)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **Input:** redesign `file` type without absolute positioning (#1712)
|
||||
|
||||
### Features
|
||||
|
||||
* **InputMenu/SelectMenu:** allow lazy search ([#1705](https://github.com/nuxt/ui/issues/1705)) ([7e6ba78](https://github.com/nuxt/ui/commit/7e6ba786816516ab5007a2ff15fc974cfdd796ab))
|
||||
* **module:** HMR support with `@nuxtjs/tailwindcss` ([#1665](https://github.com/nuxt/ui/issues/1665)) ([821e15b](https://github.com/nuxt/ui/commit/821e15b696b03d0f5e20e001d39f86a8b3cec426))
|
||||
* **Table:** allow providing a `<caption>` ([#1680](https://github.com/nuxt/ui/issues/1680)) ([3fca668](https://github.com/nuxt/ui/commit/3fca66857d3616bf24a1b0579c90179a7883869d))
|
||||
* **useToast:** allow clearing all notifications ([#1695](https://github.com/nuxt/ui/issues/1695)) ([82d619b](https://github.com/nuxt/ui/commit/82d619b2a75b9d08f3f5b314d37c30d77d8341e9))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Breadcrumb:** pass `click` event to `ULink` ([5481dab](https://github.com/nuxt/ui/commit/5481dab53dbe0b28188b4a16811f3e8816d93edf))
|
||||
* **Input:** redesign `file` type without absolute positioning ([#1712](https://github.com/nuxt/ui/issues/1712)) ([ed5c74d](https://github.com/nuxt/ui/commit/ed5c74dc17df784485eabc39c83e62ada9210a49))
|
||||
* **Notification:** update timer when timeout prop changes ([#1673](https://github.com/nuxt/ui/issues/1673)) ([cba9ad7](https://github.com/nuxt/ui/commit/cba9ad78db58cb9228bb9c96f0469d43bde2bf3e))
|
||||
* **Slideover:** export and clean types ([#1692](https://github.com/nuxt/ui/issues/1692)) ([bd3fa86](https://github.com/nuxt/ui/commit/bd3fa8658f84fb7bd96d322968462c5eaa987b86))
|
||||
* **Table:** provide `aria-sort` for sortable table headings ([#1675](https://github.com/nuxt/ui/issues/1675)) ([6f60fa9](https://github.com/nuxt/ui/commit/6f60fa9a980020f6a5afc2916e699a7f9a47e8ce))
|
||||
|
||||
## [2.15.2](https://github.com/nuxt/ui/compare/v2.15.1...v2.15.2) (2024-04-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Accordion:** add `unmount` prop to allow lazy mounting for heavy components ([#1590](https://github.com/nuxt/ui/issues/1590)) ([91e5002](https://github.com/nuxt/ui/commit/91e50020507ac66992dfb52b3e0ad1a1ae5614b5))
|
||||
* **Table:** add `checkbox` ui config ([#1409](https://github.com/nuxt/ui/issues/1409)) ([8b54660](https://github.com/nuxt/ui/commit/8b546600dbfbff187d9c5be1b35ea1772e94f83f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Breadcrumb:** missing `min-w-0` on wrapper to truncate ([9f01145](https://github.com/nuxt/ui/commit/9f01145bc674378371ff34d7110f3235b57d2459)), closes [#1650](https://github.com/nuxt/ui/issues/1650)
|
||||
* **Carousel:** next and prev buttons disabled ([#1619](https://github.com/nuxt/ui/issues/1619)) ([e909884](https://github.com/nuxt/ui/commit/e909884d0327bfd7b4d5551382123f8998beff6a))
|
||||
* **Popover/Dropdown:** prevent unintended closure on touchstart in mobile devices ([#1609](https://github.com/nuxt/ui/issues/1609)) ([2392b4a](https://github.com/nuxt/ui/commit/2392b4aa405430fc22766f130448a7cc5ced9a3a))
|
||||
* **Slideover:** remove dynamic component when closing ([#1615](https://github.com/nuxt/ui/issues/1615)) ([58faa10](https://github.com/nuxt/ui/commit/58faa1053b9be3f627c3fcff1bcaa14850bb9e7f))
|
||||
* **Slideover:** wait for transition to complete to reset state ([#1624](https://github.com/nuxt/ui/issues/1624)) ([07a4d13](https://github.com/nuxt/ui/commit/07a4d13c0fcb05c87fb42e02a3a2d6c5c52ccf09))
|
||||
|
||||
## [2.15.1](https://github.com/nuxt/ui/compare/v2.15.0...v2.15.1) (2024-04-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Avatar:** add `as` prop to use `NuxtImg` underneath ([49b73aa](https://github.com/nuxt/ui/commit/49b73aa024be14a9aa150a2804f4dcb18542fa49)), closes [#1577](https://github.com/nuxt/ui/issues/1577)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Checkbox:** `[@change](https://github.com/change)` event value ([#1580](https://github.com/nuxt/ui/issues/1580)) ([c98d6e3](https://github.com/nuxt/ui/commit/c98d6e31c0e3f46b97957d5cf3de7f9da1f70c58))
|
||||
* **Divider:** add `w-full` only on horizontal wrapper ([#1565](https://github.com/nuxt/ui/issues/1565)) ([bd8b737](https://github.com/nuxt/ui/commit/bd8b737642280e6a83b67f9a27dd7a823a77e963))
|
||||
* **Dropdown:** missing `mouseenter` event on container ([7288953](https://github.com/nuxt/ui/commit/72889535e7e9763e7ebf59498f22c39bf09d6477))
|
||||
* **Input/SelectMenu:** handle `file` type and `change` events ([#1570](https://github.com/nuxt/ui/issues/1570)) ([878f707](https://github.com/nuxt/ui/commit/878f7078a28c5e70a662682d1293db466d518c7d))
|
||||
* **Popover:** missing `mouseenter` event on container ([8517897](https://github.com/nuxt/ui/commit/8517897c34adaa9e3624f867b43106deb59fcbe8)), closes [#1564](https://github.com/nuxt/ui/issues/1564)
|
||||
|
||||
## [2.15.0](https://github.com/nuxt/ui/compare/v2.14.2...v2.15.0) (2024-03-26)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **forms:** normalize input emits (#1560)
|
||||
|
||||
### Features
|
||||
|
||||
* **Accordion:** emit `open` event with index ([#1559](https://github.com/nuxt/ui/issues/1559)) ([224ec3c](https://github.com/nuxt/ui/commit/224ec3c1fbfb9875398d3af60e5fe49e47ce55b1))
|
||||
* **Alert:** add `icon` & `avatar` slots ([#1401](https://github.com/nuxt/ui/issues/1401)) ([cee3e12](https://github.com/nuxt/ui/commit/cee3e126a472735c0e484de315868bb28287164f))
|
||||
* **Slideover:** open programmatically ([#1465](https://github.com/nuxt/ui/issues/1465)) ([e769759](https://github.com/nuxt/ui/commit/e7697595c8769ceea61690f6c2f294206de50972))
|
||||
* **Toggle:** add `loading` prop ([#1546](https://github.com/nuxt/ui/issues/1546)) ([e1e05af](https://github.com/nuxt/ui/commit/e1e05af0bafd1e5d1b91f374562ed8d389fb0cae))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ButtonGroup:** nested group elements ([#1530](https://github.com/nuxt/ui/issues/1530)) ([7658211](https://github.com/nuxt/ui/commit/765821153753d1a49276421511224336aebcdd2f))
|
||||
* **Carousel:** add tab-based ARIA roles ([#1516](https://github.com/nuxt/ui/issues/1516)) ([e736eca](https://github.com/nuxt/ui/commit/e736ecafff59f9d4eb88b366ef1e9d26449b8ca3))
|
||||
* **Checkbox:** bind `data-n-ids` to root element ([#1495](https://github.com/nuxt/ui/issues/1495)) ([a2b8b70](https://github.com/nuxt/ui/commit/a2b8b700df6ad0907a5d4d622d178d1345b55b83))
|
||||
* **forms:** normalize input emits ([#1560](https://github.com/nuxt/ui/issues/1560)) ([92e7362](https://github.com/nuxt/ui/commit/92e736213b221d5ec8cfb8881fda4fc65ce7dfa0))
|
||||
* **InputMenu:** trigger alignement on safari ([f4a48f6](https://github.com/nuxt/ui/commit/f4a48f6016ede664e4f46741e7811b0dbe0acfbe)), closes [#1505](https://github.com/nuxt/ui/issues/1505)
|
||||
* opt in to `import.meta.*` properties ([#1561](https://github.com/nuxt/ui/issues/1561)) ([cc62e34](https://github.com/nuxt/ui/commit/cc62e345eb96a632730bed796c77afe7ecdadf2a))
|
||||
* **Popover/Dropdown:** use `[@touchstart](https://github.com/touchstart).passive` instead of `[@touchstart](https://github.com/touchstart).prevent` ([#1520](https://github.com/nuxt/ui/issues/1520)) ([a563d8f](https://github.com/nuxt/ui/commit/a563d8fed44535107080fee094995d87ca5dc2b6))
|
||||
* **SelectMenu:** `filteredOptions` might be undefined ([#1541](https://github.com/nuxt/ui/issues/1541)) ([b0ecac5](https://github.com/nuxt/ui/commit/b0ecac563c5702fe40cf42a8861b1d2d1366d423))
|
||||
* **SelectMenu:** handle `Boolean` type as model value ([#1550](https://github.com/nuxt/ui/issues/1550)) ([c49f899](https://github.com/nuxt/ui/commit/c49f8999d319ec487672ebd68e8b3f0031843cd6))
|
||||
|
||||
## [2.14.2](https://github.com/nuxt/ui/compare/v2.14.1...v2.14.2) (2024-03-05)
|
||||
|
||||
|
||||
|
||||
19
README.md
19
README.md
@@ -27,24 +27,7 @@ Read more on [ui.nuxt.com](https://ui.nuxt.com)
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install @nuxt/ui
|
||||
# yarn
|
||||
yarn add @nuxt/ui
|
||||
# pnpm
|
||||
pnpm add @nuxt/ui
|
||||
# bun
|
||||
bun add @nuxt/ui
|
||||
```
|
||||
|
||||
Then, register the module in your `nuxt.config.ts`:
|
||||
|
||||
```js
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@nuxt/ui'
|
||||
]
|
||||
})
|
||||
npx nuxi@latest module add ui
|
||||
```
|
||||
|
||||
If you want latest updates, please use `@nuxt/ui-edge` in your `package.json`:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
imports.autoImport=true
|
||||
@@ -23,6 +23,7 @@
|
||||
</template>
|
||||
</UNotifications>
|
||||
<UModals />
|
||||
<USlideovers />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -71,7 +72,7 @@ const links = computed(() => {
|
||||
active: route.path.startsWith('/pro/getting-started') || route.path.startsWith('/pro/components') || route.path.startsWith('/pro/prose')
|
||||
}, {
|
||||
label: 'Pricing',
|
||||
icon: 'i-heroicons-credit-card',
|
||||
icon: 'i-heroicons-ticket',
|
||||
to: '/pro/pricing'
|
||||
}, {
|
||||
label: 'Templates',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="[&>div>pre]:!rounded-t-none [&>div>pre]:!mt-0">
|
||||
<div
|
||||
v-if="hasPreview"
|
||||
class="flex border border-gray-200 dark:border-gray-700 relative rounded-t-md"
|
||||
:class="[{ 'p-4': padding, 'rounded-b-md': !hasCode, 'border-b-0': hasCode, 'not-prose': !prose }, backgroundClass, extraClass]"
|
||||
>
|
||||
@@ -37,6 +38,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
hiddenPreview: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hiddenCode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -79,6 +84,7 @@ const data = await fetchContentExampleCode(camelName)
|
||||
const highlighter = useShikiHighlighter()
|
||||
|
||||
const hasCode = computed(() => !props.hiddenCode && (data?.code || instance.slots.code))
|
||||
const hasPreview = computed(() => !props.hiddenPreview && (props.component || instance.slots.default))
|
||||
|
||||
const { data: ast } = await useAsyncData(`content-example-${camelName}-ast`, () => transformContent('content:_markdown.md', `\`\`\`vue\n${data?.code ?? ''}\n\`\`\``, {
|
||||
markdown: {
|
||||
|
||||
@@ -61,9 +61,7 @@ const items = [{
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<code>$ npm i @nuxt/ui</code>
|
||||
<code>$ yarn add @nuxt/ui</code>
|
||||
<code>$ pnpm add @nuxt/ui</code>
|
||||
<code>$ npx nuxi@latest module add ui</code>
|
||||
</div>
|
||||
</template>
|
||||
</UAccordion>
|
||||
|
||||
20
docs/components/content/examples/AlertExampleAvatar.vue
Normal file
20
docs/components/content/examples/AlertExampleAvatar.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<UAlert
|
||||
title="Customize Alert Avatar"
|
||||
description="Insert custom content into the avatar slot!"
|
||||
:avatar="{
|
||||
src: 'https://avatars.githubusercontent.com/u/739984?v=4',
|
||||
alt: 'Avatar'
|
||||
}"
|
||||
>
|
||||
<template #avatar="{ avatar }">
|
||||
<UAvatar
|
||||
v-bind="avatar"
|
||||
chip-color="primary"
|
||||
chip-text=""
|
||||
chip-position="top-right"
|
||||
/>
|
||||
</template>
|
||||
</UAlert>
|
||||
</template>
|
||||
|
||||
10
docs/components/content/examples/AlertExampleIcon.vue
Normal file
10
docs/components/content/examples/AlertExampleIcon.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<UAlert title="Customize Alert Icon" description="Insert custom content into the icon slot!" icon="i-heroicons-command-line">
|
||||
<template #icon="{ icon }">
|
||||
<UBadge size="sm">
|
||||
<UIcon :name="icon" />
|
||||
</UBadge>
|
||||
</template>
|
||||
</UAlert>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,7 @@ const links = [{
|
||||
<template>
|
||||
<UBreadcrumb :links="links">
|
||||
<template #default="{ link, isActive, index }">
|
||||
<UBadge :color="isActive ? 'primary' : 'gray'" class="rounded-full">
|
||||
<UBadge :color="isActive ? 'primary' : 'gray'" class="rounded-full truncate">
|
||||
{{ index + 1 }}. {{ link.label }}
|
||||
</UBadge>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useMouse, useWindowScroll } from '@vueuse/core'
|
||||
|
||||
const { x, y } = useMouse()
|
||||
const { y: windowY } = useWindowScroll()
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useMouse, useWindowScroll } from '@vueuse/core'
|
||||
|
||||
const { x, y } = useMouse()
|
||||
const { y: windowY } = useWindowScroll()
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useMouse, useWindowScroll } from '@vueuse/core'
|
||||
|
||||
const { x, y } = useMouse()
|
||||
const { y: windowY } = useWindowScroll()
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useMouse, useWindowScroll } from '@vueuse/core'
|
||||
|
||||
const { x, y } = useMouse()
|
||||
const { y: windowY } = useWindowScroll()
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const date = ref(new Date())
|
||||
<UButton icon="i-heroicons-calendar-days-20-solid" :label="format(date, 'd MMM, yyy')" />
|
||||
|
||||
<template #panel="{ close }">
|
||||
<DatePicker v-model="date" @close="close" />
|
||||
<DatePicker v-model="date" is-required @close="close" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { string, objectAsync, email, minLength, type Input } from 'valibot'
|
||||
import * as v from 'valibot'
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
|
||||
const schema = objectAsync({
|
||||
email: string([email('Invalid email')]),
|
||||
password: string([minLength(8, 'Must be at least 8 characters')])
|
||||
const schema = v.object({
|
||||
email: v.pipe(v.string(), v.email('Invalid email')),
|
||||
password: v.pipe(v.string(), v.minLength(8, 'Must be at least 8 characters'))
|
||||
})
|
||||
|
||||
type Schema = Input<typeof schema>
|
||||
type Schema = v.InferOutput<typeof schema>
|
||||
|
||||
const state = reactive({
|
||||
email: '',
|
||||
@@ -21,7 +21,7 @@ async function onSubmit (event: FormSubmitEvent<Schema>) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UForm :schema="v.safeParser(schema)" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UFormGroup label="Email" name="email">
|
||||
<UInput v-model="state.email" />
|
||||
</UFormGroup>
|
||||
|
||||
@@ -31,8 +31,8 @@ const selected = ref(people[0])
|
||||
<template>
|
||||
<UInputMenu v-model="selected" :options="people">
|
||||
<template #leading>
|
||||
<UIcon v-if="selected.icon" :name="(selected.icon as string)" class="w-4 h-4 mx-0.5" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="(selected.avatar as Avatar)" size="3xs" class="mx-0.5" />
|
||||
<UIcon v-if="selected.icon" :name="(selected.icon as string)" class="w-5 h-5" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="(selected.avatar as Avatar)" size="2xs" />
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
|
||||
@@ -5,13 +5,24 @@ defineProps({
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
function onSuccess () {
|
||||
emit('success')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal>
|
||||
<UCard>
|
||||
<p>This modal was opened programmatically !</p>
|
||||
<p>Count: {{ count }}</p>
|
||||
<div class="space-y-2">
|
||||
<p>This modal was opened programmatically !</p>
|
||||
<p>Count: {{ count }}</p>
|
||||
<UButton @click="onSuccess">
|
||||
Click to emit a success event
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ModalExampleComponent } from '#components'
|
||||
|
||||
const toast = useToast()
|
||||
const modal = useModal()
|
||||
const count = ref(0)
|
||||
|
||||
function openModal () {
|
||||
count.value += 1
|
||||
modal.open(ModalExampleComponent, {
|
||||
count: count.value
|
||||
count: count.value,
|
||||
onSuccess () {
|
||||
toast.add({
|
||||
title: 'Success !',
|
||||
id: 'modal-success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
17
docs/components/content/examples/PaginationExampleTo.vue
Normal file
17
docs/components/content/examples/PaginationExampleTo.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
const page = ref(1)
|
||||
const items = ref(Array(50))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPagination
|
||||
v-model="page"
|
||||
:page-count="5"
|
||||
:total="items.length"
|
||||
:to="(page: number) => ({
|
||||
query: { page },
|
||||
// Hash is specified here to prevent the page from scrolling to the top
|
||||
hash: '#links',
|
||||
})"
|
||||
/>
|
||||
</template>
|
||||
@@ -31,8 +31,8 @@ const selected = ref(people[0])
|
||||
<template>
|
||||
<USelectMenu v-model="selected" :options="people">
|
||||
<template #leading>
|
||||
<UIcon v-if="selected.icon" :name="(selected.icon as string)" class="w-4 h-4 mx-0.5" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="(selected.avatar as Avatar)" size="3xs" class="mx-0.5" />
|
||||
<UIcon v-if="selected.icon" :name="(selected.icon as string)" class="w-5 h-5" />
|
||||
<UAvatar v-else-if="selected.avatar" v-bind="(selected.avatar as Avatar)" size="2xs" />
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,16 @@ const isOpen = ref(false)
|
||||
|
||||
<USlideover v-model="isOpen">
|
||||
<div class="p-4 flex-1">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
class="flex sm:hidden absolute end-5 top-5 z-10"
|
||||
square
|
||||
padded
|
||||
@click="isOpen = false"
|
||||
/>
|
||||
<Placeholder class="h-full" />
|
||||
</div>
|
||||
</USlideover>
|
||||
|
||||
@@ -7,8 +7,22 @@ const isOpen = ref(false)
|
||||
<UButton label="Open" @click="isOpen = true" />
|
||||
|
||||
<USlideover v-model="isOpen">
|
||||
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<UCard
|
||||
class="flex flex-col flex-1"
|
||||
:ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
|
||||
>
|
||||
<template #header>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
class="flex sm:hidden absolute end-5 top-5 z-10"
|
||||
square
|
||||
padded
|
||||
@click="isOpen = false"
|
||||
/>
|
||||
|
||||
<Placeholder class="h-8" />
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const props = defineProps({
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
close: [];
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USlideover>
|
||||
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Opened programmatically: {{ props.count }} times
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="emits('close')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Placeholder class="h-full" />
|
||||
</UCard>
|
||||
</USlideover>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { SlideoverExampleComponent } from '#components'
|
||||
const slideover = useSlideover()
|
||||
const count = ref(0)
|
||||
function openSlideover () {
|
||||
count.value += 1
|
||||
slideover.open(SlideoverExampleComponent, {
|
||||
count: count.value,
|
||||
onClose: slideover.close
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Reveal slideover" @click="openSlideover" />
|
||||
</template>
|
||||
@@ -8,6 +8,17 @@ const isOpen = ref(false)
|
||||
|
||||
<USlideover v-model="isOpen" :overlay="false">
|
||||
<div class="p-4 flex-1">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
class="flex sm:hidden absolute end-5 top-5 z-10"
|
||||
square
|
||||
padded
|
||||
@click="isOpen = false"
|
||||
/>
|
||||
|
||||
<Placeholder class="h-full" />
|
||||
</div>
|
||||
</USlideover>
|
||||
|
||||
@@ -8,6 +8,17 @@ const isOpen = ref(false)
|
||||
|
||||
<USlideover v-model="isOpen" :transition="false">
|
||||
<div class="p-4 flex-1">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
class="flex sm:hidden absolute end-5 top-5 z-10"
|
||||
square
|
||||
padded
|
||||
@click="isOpen = false"
|
||||
/>
|
||||
|
||||
<Placeholder class="h-full" />
|
||||
</div>
|
||||
</USlideover>
|
||||
|
||||
@@ -186,7 +186,7 @@ const { data: todos, pending } = await useLazyAsyncData<{
|
||||
sort-desc-icon="i-heroicons-arrow-down"
|
||||
sort-mode="manual"
|
||||
class="w-full"
|
||||
:ui="{ td: { base: 'max-w-[0] truncate' } }"
|
||||
:ui="{ td: { base: 'max-w-[0] truncate' }, default: { checkbox: { color: 'gray' } } }"
|
||||
@select="select"
|
||||
>
|
||||
<template #completed-data="{ row }">
|
||||
|
||||
63
docs/components/content/examples/TableExampleCaptionSlot.vue
Normal file
63
docs/components/content/examples/TableExampleCaptionSlot.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :rows="people" :columns="columns">
|
||||
<template #caption>
|
||||
<caption>Employees of ACME</caption>
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
@@ -32,13 +32,7 @@ const links = [{
|
||||
<template>
|
||||
<UVerticalNavigation :links="links">
|
||||
<template #avatar="{ link }">
|
||||
<UAvatar
|
||||
v-if="link.avatar"
|
||||
v-bind="link.avatar"
|
||||
size="3xs"
|
||||
loading="lazy"
|
||||
/>
|
||||
<UIcon v-else name="i-heroicons-user-circle-20-solid" class="text-lg" />
|
||||
<UAvatar v-bind="link.avatar" size="2xs" loading="lazy" />
|
||||
</template>
|
||||
</UVerticalNavigation>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function fetchComponentMeta (name: string) {
|
||||
// Store promise to avoid multiple calls
|
||||
|
||||
// add to nitro prerender
|
||||
if (process.server) {
|
||||
if (import.meta.server) {
|
||||
const event = useRequestEvent()
|
||||
event.node.res.setHeader(
|
||||
'x-nitro-prerender',
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function fetchContentExampleCode (name?: string) {
|
||||
if (state.value[name]) { return state.value[name] }
|
||||
|
||||
// add to nitro prerender
|
||||
if (process.server) {
|
||||
if (import.meta.server) {
|
||||
const event = useRequestEvent()
|
||||
event.node.res.setHeader(
|
||||
'x-nitro-prerender',
|
||||
|
||||
@@ -5,7 +5,7 @@ description: 'Fully styled and customizable components for Nuxt.'
|
||||
|
||||
Nuxt UI is a module that provides a set of Vue components and composables built with [Tailwind CSS](https://tailwindcss.com/) and [Headless UI](https://headlessui.dev/) to help you build beautiful and accessible user interfaces.
|
||||
|
||||
Its goal is to provide everything related to UI when building a Nuxt app. This includes components, icons, colors, dark mode but also keyboard shortcuts.
|
||||
Its goal is to provide everything related to UI when building a Nuxt app. This includes components, icons, colors, dark mode as well as keyboard shortcuts.
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -5,29 +5,15 @@ description: 'Learn how to install and configure Nuxt UI in your application.'
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install `@nuxt/ui` dependency to your project:
|
||||
### Add to a Nuxt project
|
||||
|
||||
::code-group
|
||||
1. Add `@nuxt/ui` module to your project:
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add @nuxt/ui
|
||||
```bash
|
||||
npx nuxi@latest module add ui
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add @nuxt/ui
|
||||
```
|
||||
|
||||
```bash [npm]
|
||||
npm install @nuxt/ui
|
||||
```
|
||||
|
||||
```bash [bun]
|
||||
bun add @nuxt/ui
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
2. Add it to your `modules` section in your `nuxt.config`:
|
||||
2. Add it to the `modules` section in your `nuxt.config.ts`:
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
@@ -37,6 +23,19 @@ export default defineNuxtConfig({
|
||||
|
||||
That's it! You can now use all the components and composables in your Nuxt app ✨
|
||||
|
||||
### Use Nuxt starter
|
||||
|
||||
[Nuxt Starter](https://nuxt.new/) template makes it easy to get started with Nuxt UI.
|
||||
The Nuxt Starter template is available from the `nuxi init` command.
|
||||
|
||||
```bash
|
||||
npx nuxi@latest init -t ui
|
||||
```
|
||||
|
||||
|
||||
|
||||
Please check [nuxt/starter](https://github.com/nuxt/starter/tree/ui) for details.
|
||||
|
||||
## Modules
|
||||
|
||||
Nuxt UI will automatically install the [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/), [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) and [nuxt-icon](https://github.com/nuxt-modules/icon) modules for you.
|
||||
@@ -194,7 +193,7 @@ To enable these two features, you can add the following to your `.vscode/setting
|
||||
{
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["ui:\\s*{([^)]*)\\s*}", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||
["/\\*ui\\*/\\s*{([^;]*)}", ":\\s*[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
["/\\*\\s?ui\\s?\\*/\\s*{([^;]*)}", ":\\s*[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -207,7 +206,7 @@ An example SFC using IntelliSense (note the `/*ui*/` prefix, also works with `re
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const ui = /*ui*/ {
|
||||
const ui = /* ui */ {
|
||||
background: 'bg-white dark:bg-slate-900'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -163,6 +163,22 @@ This can be handy when you want to display HTML content. To achieve this, you ca
|
||||
|
||||
:component-example{component="alert-example-html"}
|
||||
|
||||
### `icon` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `#icon` slot to customize the displayed icon.
|
||||
|
||||
:component-example{component="alert-example-icon"}
|
||||
|
||||
### `avatar` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `#avatar` slot to customize the displayable avatar.
|
||||
|
||||
:component-example{component="alert-example-avatar"}
|
||||
|
||||
### `actions` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `#actions` slot to add custom user interaction elements.
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
@@ -336,7 +336,7 @@ Use the `#leading` slot to set the content of the leading icon.
|
||||
::component-card
|
||||
---
|
||||
slots:
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="2xs" />
|
||||
baseProps:
|
||||
color: 'gray'
|
||||
props:
|
||||
@@ -347,7 +347,7 @@ excludedProps:
|
||||
---
|
||||
|
||||
#leading
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="2xs"}
|
||||
::
|
||||
|
||||
### `trailing`
|
||||
@@ -357,7 +357,7 @@ Use the `#trailing` slot to set the content of the trailing icon.
|
||||
::component-card
|
||||
---
|
||||
slots:
|
||||
trailing: <UIcon name="i-heroicons-arrow-right-20-solid" />
|
||||
trailing: <UIcon name="i-heroicons-arrow-right-20-solid" class="w-5 h-5" />
|
||||
props:
|
||||
label: Button
|
||||
color: 'gray'
|
||||
@@ -366,7 +366,7 @@ excludedProps:
|
||||
---
|
||||
|
||||
#trailing
|
||||
:u-icon{name="i-heroicons-arrow-right-20-solid"}
|
||||
:u-icon{name="i-heroicons-arrow-right-20-solid" class="w-5 h-5"}
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
@@ -196,7 +196,7 @@ Use the `#description` slot to set the custom content for description.
|
||||
::component-card
|
||||
---
|
||||
slots:
|
||||
description: Write only valid email address <UIcon name="i-heroicons-arrow-right-20-solid" />
|
||||
description: Write only valid email address <UIcon name="i-heroicons-information-circle" />
|
||||
default: <UInput model-value="benjamincanac" placeholder="you@example.com" />
|
||||
props:
|
||||
label: 'Email'
|
||||
|
||||
@@ -10,7 +10,7 @@ links:
|
||||
|
||||
Use the Form component to validate form data using schema libraries such as [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi), [Valibot](https://valibot.dev/), or your own validation logic.
|
||||
|
||||
It works with the [FormGroup](/components/input) component to display error messages around form elements automatically.
|
||||
It works with the [FormGroup](/components/form-group) component to display error messages around form elements automatically.
|
||||
|
||||
The form component requires two props:
|
||||
- `state` - a reactive object holding the form's state.
|
||||
@@ -63,7 +63,7 @@ The validation function must return a list of errors with the following attribut
|
||||
- `path` - Path to the form element corresponding to the `name` attribute.
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
Note: this can be used alongside the `schema` prop to handle complex use cases.
|
||||
Note that it can be used alongside the `schema` prop to handle complex use cases.
|
||||
::
|
||||
|
||||
::component-example
|
||||
@@ -184,13 +184,13 @@ Take a look at the component!
|
||||
|
||||
## Error event
|
||||
|
||||
You can listen to the `@error` event to handle errors. This event is triggered when the form is validated and contains an array of `FormError` objects with the following fields:
|
||||
You can listen to the `@error` event to handle errors. This event is triggered when the form is submitted and contains an array of `FormError` objects with the following fields:
|
||||
|
||||
- `id` - the identifier of the form element.
|
||||
- `path` - the path to the form element matching the `name`.
|
||||
- `message` - the error message to display.
|
||||
|
||||
Here is an example of how to focus the first form element with an error:
|
||||
Here's an example that focuses the first input element with an error after the form is submitted:
|
||||
|
||||
::component-example
|
||||
---
|
||||
|
||||
@@ -18,7 +18,11 @@ props:
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-exclamation-triangle"}
|
||||
You won't be able to use any icon in the `name` prop here as icons are bundled using [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons), read more about this in [Theming](/getting-started/theming#icons).
|
||||
You won't be able to use all icons in the `name` prop here as icons are bundled using [egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons).
|
||||
::
|
||||
|
||||
::callout{icon="i-heroicons-light-bulb"}
|
||||
Don't forget to install and specify the icon collections you need in your `nuxt.config.ts`, read more about this in [Theming](/getting-started/theming#icons).
|
||||
::
|
||||
|
||||
### Dynamic
|
||||
|
||||
@@ -130,6 +130,8 @@ Pass a function to the `search` prop to customize the search behavior and filter
|
||||
|
||||
Use the `debounce` prop to adjust the delay of the function.
|
||||
|
||||
Use the `searchLazy` prop to control the immediacy of data requests. :u-badge{label="New" class="!rounded-full" variant="subtle"}
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'input-menu-example-search-async'
|
||||
|
||||
@@ -75,6 +75,8 @@ Some types have been implemented in their own components, such as [Checkbox](/co
|
||||
|
||||
::component-card
|
||||
---
|
||||
baseProps:
|
||||
icon: 'i-heroicons-folder'
|
||||
props:
|
||||
type: 'file'
|
||||
size: sm
|
||||
@@ -180,13 +182,13 @@ Use the `#leading` slot to set the content of the leading icon.
|
||||
::component-card
|
||||
---
|
||||
slots:
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5" />
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="2xs" />
|
||||
baseProps:
|
||||
placeholder: 'Search...'
|
||||
---
|
||||
|
||||
#leading
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5"}
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="2xs"}
|
||||
::
|
||||
|
||||
### `trailing`
|
||||
|
||||
@@ -77,7 +77,13 @@ First of all, add the `Modals` component to your app, preferably inside `app.vue
|
||||
|
||||
Then, you can use the `useModal` composable to control your modals within your app.
|
||||
|
||||
:component-example{component="modal-example-composable"}
|
||||
<!-- For prerendering -->
|
||||
:component-example{component="modal-example-component" hiddenCode hiddenPreview }
|
||||
|
||||
::code-group{class="[&>div:last-child>div:first-child]:!rounded-t-none"}
|
||||
:component-example{component="modal-example-composable" label="app.vue" }
|
||||
:component-example{component="modal-example-component" hiddenPreview label="modal.vue" }
|
||||
::
|
||||
|
||||
Additionally, you can close the modal within the modal component by calling `modal.close()`.
|
||||
|
||||
|
||||
@@ -46,6 +46,12 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Links
|
||||
|
||||
Use the `to` property to transform buttons into links. Note that it must be a function that receives the page number and returns a route destination.
|
||||
|
||||
:component-example{component="pagination-example-to"}
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` prop to disable all the buttons.
|
||||
|
||||
@@ -50,7 +50,7 @@ componentProps:
|
||||
---
|
||||
::
|
||||
|
||||
If you only want to select a single object property rather than the whole object as value, you can set the `value-attribute` property. This prop defaults to `null`.
|
||||
If you only want to select a single object property rather than the whole object as value, you can set the `value-attribute` property. This prop defaults to `null`.The value of the `value-attribute` field in options must be unique.
|
||||
|
||||
::component-example
|
||||
---
|
||||
@@ -150,6 +150,8 @@ Pass a function to the `searchable` prop to customize the search behavior and fi
|
||||
|
||||
Use the `debounce` prop to adjust the delay of the function.
|
||||
|
||||
Use the `searchableLazy` prop to control the immediacy of data requests. :u-badge{label="New" class="!rounded-full" variant="subtle"}
|
||||
|
||||
::component-example
|
||||
---
|
||||
component: 'select-menu-example-search-async'
|
||||
|
||||
@@ -203,7 +203,7 @@ Use the `#leading` slot to set the content of the leading icon.
|
||||
::component-card
|
||||
---
|
||||
slots:
|
||||
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5" />
|
||||
leading: <UIcon name="i-heroicons-flag" class="w-5 h-5" />
|
||||
baseProps:
|
||||
options:
|
||||
- 'United States'
|
||||
@@ -213,7 +213,7 @@ baseProps:
|
||||
---
|
||||
|
||||
#leading
|
||||
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mx-0.5"}
|
||||
:u-icon{name="i-heroicons-flag" class="w-5 h-5"}
|
||||
::
|
||||
|
||||
### `trailing`
|
||||
@@ -223,13 +223,13 @@ 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" />
|
||||
trailing: <UIcon name="i-heroicons-arrows-up-down-20-solid" class="w-5 h-5" />
|
||||
baseProps:
|
||||
placeholder: 'Search...'
|
||||
---
|
||||
|
||||
#trailing
|
||||
:u-icon{name="i-heroicons-arrows-up-down-20-solid"}
|
||||
:u-icon{name="i-heroicons-arrows-up-down-20-solid" class="w-5 h-5"}
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
@@ -33,7 +33,7 @@ Set the `transition` prop to `false` to disable it.
|
||||
|
||||
### Prevent close
|
||||
|
||||
Use the `prevent-close` prop to disable the outside click alongside the `esc` keyboard shortcut. A `close-prevented` event will be emitted when the user tries to close the modal.
|
||||
Use the `prevent-close` prop to disable the outside click alongside the `esc` keyboard shortcut. A `close-prevented` event will be emitted when the user tries to close the slideover.
|
||||
|
||||
:component-example{component="slideover-example-prevent-close"}
|
||||
|
||||
@@ -53,6 +53,24 @@ defineShortcuts({
|
||||
</script>
|
||||
```
|
||||
|
||||
### Control programmatically :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
First of all, add the `USlideovers` component to your app, preferably inside `app.vue`.
|
||||
|
||||
```vue [app.vue]
|
||||
<template>
|
||||
<div>
|
||||
<UContainer>
|
||||
<NuxtPage />
|
||||
</UContainer>
|
||||
<USlideovers />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Then, you can use the `useSlideover` composable to control your slideovers within your app.
|
||||
|
||||
:component-example{component="slideover-example-composable"}
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
@@ -49,7 +49,7 @@ extraClass: 'overflow-hidden'
|
||||
padding: false
|
||||
component: 'table-example-columns-selectable'
|
||||
componentProps:
|
||||
class: 'flex-1'
|
||||
class: 'flex-1 flex-col overflow-hidden'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -282,7 +282,7 @@ extraClass: 'overflow-hidden'
|
||||
padding: false
|
||||
component: 'table-example-searchable'
|
||||
componentProps:
|
||||
class: 'flex-1'
|
||||
class: 'flex-1 flex-col overflow-hidden'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -296,7 +296,7 @@ extraClass: 'overflow-hidden'
|
||||
padding: false
|
||||
component: 'table-example-paginable'
|
||||
componentProps:
|
||||
class: 'flex-1'
|
||||
class: 'flex-1 flex-col overflow-hidden'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -450,6 +450,19 @@ componentProps:
|
||||
---
|
||||
::
|
||||
|
||||
### `caption`
|
||||
|
||||
Use the `#caption` slot to customize the table's caption.
|
||||
|
||||
::component-example
|
||||
---
|
||||
padding: false
|
||||
component: 'table-example-caption-slot'
|
||||
componentProps:
|
||||
class: 'flex-1'
|
||||
---
|
||||
::
|
||||
|
||||
## Props
|
||||
|
||||
:component-props
|
||||
|
||||
@@ -63,6 +63,8 @@ componentProps:
|
||||
---
|
||||
::
|
||||
|
||||
You can use the `content` prop and set it to `false` to avoid the rendering of the HTML content if you don't need it.
|
||||
|
||||
### Control the selected index
|
||||
|
||||
Use a `v-model` to control the selected index.
|
||||
|
||||
@@ -52,6 +52,19 @@ excludedProps:
|
||||
---
|
||||
::
|
||||
|
||||
### Loading :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||
|
||||
Use the `loading` prop to show a loading icon and disable the Toggle.
|
||||
|
||||
Use the `loading-icon` prop to set a different icon or change it globally in `ui.toggle.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
|
||||
|
||||
::component-card
|
||||
---
|
||||
props:
|
||||
loading: true
|
||||
---
|
||||
::
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` prop to disable the Toggle.
|
||||
|
||||
@@ -79,7 +79,7 @@ const links = computed(() => {
|
||||
active: route.path.startsWith('/pro/getting-started') || route.path.startsWith('/pro/components') || route.path.startsWith('/pro/prose')
|
||||
}, {
|
||||
label: 'Pricing',
|
||||
icon: 'i-heroicons-credit-card',
|
||||
icon: 'i-heroicons-ticket',
|
||||
to: '/pro/pricing'
|
||||
}, {
|
||||
label: 'Templates',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createResolver } from '@nuxt/kit'
|
||||
import colors from 'tailwindcss/colors'
|
||||
import module from '../src/module'
|
||||
import { excludeColors } from '../src/colors'
|
||||
import { excludeColors } from '../src/runtime/utils/colors'
|
||||
import pkg from '../package.json'
|
||||
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
@@ -74,16 +74,6 @@ export default defineNuxtConfig({
|
||||
image: {
|
||||
provider: 'ipx'
|
||||
},
|
||||
fontMetrics: {
|
||||
fonts: ['DM Sans']
|
||||
},
|
||||
googleFonts: {
|
||||
display: 'swap',
|
||||
download: true,
|
||||
families: {
|
||||
'DM+Sans': [400, 500, 600, 700]
|
||||
}
|
||||
},
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: [
|
||||
|
||||
@@ -3,33 +3,30 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@nuxt/ui": "workspace:latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/heroicons": "^1.1.20",
|
||||
"@iconify-json/simple-icons": "^1.1.94",
|
||||
"@nuxt/content": "^2.12.0",
|
||||
"@nuxt/devtools": "^1.0.8",
|
||||
"@nuxt/eslint-config": "^0.2.0",
|
||||
"@nuxt/fonts": "^0.0.1",
|
||||
"@nuxt/image": "^1.3.0",
|
||||
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@1.0.1-28492961.4d49b9c",
|
||||
"@nuxtjs/plausible": "^0.2.4",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@vueuse/nuxt": "^10.9.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"@iconify-json/heroicons": "^1.1.21",
|
||||
"@iconify-json/simple-icons": "^1.1.105",
|
||||
"@nuxt/content": "^2.12.1",
|
||||
"@nuxt/eslint-config": "^0.3.13",
|
||||
"@nuxt/fonts": "^0.7.0",
|
||||
"@nuxt/image": "^1.7.0",
|
||||
"@nuxt/ui": "latest",
|
||||
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@1.2.0-28637819.42c6d9b",
|
||||
"@nuxtjs/plausible": "^1.0.0",
|
||||
"@octokit/rest": "^20.1.1",
|
||||
"@vueuse/nuxt": "^10.11.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"eslint": "^8.57.0",
|
||||
"joi": "^17.12.2",
|
||||
"nuxt": "^3.10.3",
|
||||
"joi": "^17.13.1",
|
||||
"nuxt": "^3.12.1",
|
||||
"nuxt-cloudflare-analytics": "^1.0.8",
|
||||
"nuxt-component-meta": "^0.6.3",
|
||||
"nuxt-component-meta": "^0.6.4",
|
||||
"nuxt-og-image": "^2.2.4",
|
||||
"prettier": "^3.2.5",
|
||||
"typescript": "^5.3.3",
|
||||
"ufo": "^1.4.0",
|
||||
"prettier": "^3.3.2",
|
||||
"typescript": "^5.4.5",
|
||||
"ufo": "^1.5.3",
|
||||
"v-calendar": "^3.1.2",
|
||||
"valibot": "^0.29.0",
|
||||
"yup": "^1.3.3",
|
||||
"zod": "^3.22.4"
|
||||
"valibot": "^0.31.1",
|
||||
"yup": "^1.4.0",
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,11 @@
|
||||
readonly
|
||||
autocomplete="off"
|
||||
icon="i-heroicons-command-line"
|
||||
input-class="select-none"
|
||||
class="w-72"
|
||||
input-class="focus:ring-1 focus:ring-gray-300 dark:focus:ring-gray-700"
|
||||
aria-label="Install @nuxt/ui"
|
||||
size="lg"
|
||||
:ui="{ base: 'disabled:cursor-default', icon: { trailing: { pointer: '' } } }"
|
||||
:ui="{ icon: { trailing: { pointer: '' } } }"
|
||||
>
|
||||
<template #trailing>
|
||||
<UButton
|
||||
@@ -435,7 +436,7 @@ useSeoMeta({
|
||||
twitterImage: 'https://ui.nuxt.com/social-card.png'
|
||||
})
|
||||
|
||||
const source = ref('npm i @nuxt/ui')
|
||||
const source = ref('npx nuxi@latest module add ui')
|
||||
const sectionRef = ref()
|
||||
const demoRef = ref()
|
||||
const start = ref(0)
|
||||
|
||||
@@ -44,7 +44,7 @@ function createPrettierWorkerApi (worker: Worker): SimplePrettier {
|
||||
export default defineNuxtPlugin({
|
||||
async setup () {
|
||||
let prettier: SimplePrettier
|
||||
if (process.server) {
|
||||
if (import.meta.server) {
|
||||
const prettierModule = await import('prettier')
|
||||
prettier = {
|
||||
format (source, options = {
|
||||
|
||||
@@ -23,7 +23,7 @@ export default defineNuxtPlugin({
|
||||
`
|
||||
})
|
||||
|
||||
if (process.client) {
|
||||
if (import.meta.client) {
|
||||
watch(root, () => {
|
||||
window.localStorage.setItem('nuxt-ui-root', root.value)
|
||||
})
|
||||
@@ -31,7 +31,7 @@ export default defineNuxtPlugin({
|
||||
appConfig.ui.primary = window.localStorage.getItem('nuxt-ui-primary') || appConfig.ui.primary
|
||||
appConfig.ui.gray = window.localStorage.getItem('nuxt-ui-gray') || appConfig.ui.gray
|
||||
}
|
||||
if (process.server) {
|
||||
if (import.meta.server) {
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
|
||||
66
package.json
66
package.json
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@nuxt/ui",
|
||||
"version": "2.14.2",
|
||||
"version": "2.17.0",
|
||||
"packageManager": "pnpm@9.1.1",
|
||||
"repository": "nuxt/ui",
|
||||
"homepage": "https://ui.nuxt.com",
|
||||
"type": "module",
|
||||
@@ -33,55 +34,54 @@
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@egoist/tailwindcss-icons": "^1.7.4",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@headlessui/vue": "^1.7.19",
|
||||
"@iconify-json/heroicons": "^1.1.20",
|
||||
"@nuxt/kit": "^3.10.3",
|
||||
"@nuxtjs/color-mode": "^3.3.2",
|
||||
"@nuxtjs/tailwindcss": "^6.11.4",
|
||||
"@egoist/tailwindcss-icons": "^1.8.1",
|
||||
"@headlessui/tailwindcss": "^0.2.1",
|
||||
"@headlessui/vue": "^1.7.22",
|
||||
"@iconify-json/heroicons": "^1.1.21",
|
||||
"@nuxt/kit": "^3.12.1",
|
||||
"@nuxtjs/color-mode": "^3.4.1",
|
||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@vueuse/integrations": "^10.9.0",
|
||||
"@vueuse/math": "^10.9.0",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"@vueuse/integrations": "^10.11.0",
|
||||
"@vueuse/math": "^10.11.0",
|
||||
"defu": "^6.1.4",
|
||||
"fuse.js": "^6.6.2",
|
||||
"nuxt-icon": "^0.6.8",
|
||||
"nuxt-icon": "^0.6.10",
|
||||
"ohash": "^1.1.3",
|
||||
"pathe": "^1.1.2",
|
||||
"scule": "^1.3.0",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss": "^3.4.1"
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "^3.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint-config": "^0.2.0",
|
||||
"@nuxt/eslint-config": "^0.3.13",
|
||||
"@nuxt/module-builder": "^0.5.5",
|
||||
"@nuxt/test-utils": "^3.11.0",
|
||||
"@nuxt/test-utils": "^3.13.1",
|
||||
"@release-it/conventional-changelog": "^8.0.1",
|
||||
"@vue/test-utils": "^2.4.4",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"eslint": "^8.57.0",
|
||||
"happy-dom": "^13.6.2",
|
||||
"joi": "^17.12.2",
|
||||
"nuxt": "^3.10.3",
|
||||
"release-it": "^17.1.1",
|
||||
"typescript": "^5.3.3",
|
||||
"happy-dom": "^14.10.1",
|
||||
"joi": "^17.13.1",
|
||||
"nuxt": "^3.12.1",
|
||||
"release-it": "^17.3.0",
|
||||
"typescript": "^5.4.5",
|
||||
"unbuild": "^2.0.0",
|
||||
"valibot": "^0.29.0",
|
||||
"vitest": "^1.3.1",
|
||||
"valibot30": "npm:valibot@0.30.0",
|
||||
"valibot": "^0.31.1",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-environment-nuxt": "^1.0.0",
|
||||
"vue-tsc": "^2.0.4",
|
||||
"yup": "^1.3.3",
|
||||
"zod": "^3.22.4"
|
||||
"vue-tsc": "^2.0.16",
|
||||
"yup": "^1.4.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"resolutions": {
|
||||
"@nuxt/kit": "3.10.3",
|
||||
"@nuxt/schema": "3.10.3",
|
||||
"tailwindcss": "3.4.1",
|
||||
"@headlessui/vue": "1.7.19",
|
||||
"vue": "3.4.21"
|
||||
"@nuxt/ui": "workspace:*",
|
||||
"@nuxt/module-builder": "0.5.5",
|
||||
"vue-tsc": "2.0.16"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
imports.autoImport=true
|
||||
14
playground/package.json
Normal file
14
playground/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@nuxt/ui-playground",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "nuxi dev",
|
||||
"build": "nuxi build",
|
||||
"generate": "nuxi generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/ui": "latest",
|
||||
"nuxt": "^3.12.1"
|
||||
}
|
||||
}
|
||||
21857
pnpm-lock.yaml
generated
21857
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
packages:
|
||||
- "docs"
|
||||
- "./"
|
||||
- "docs"
|
||||
- "playground"
|
||||
|
||||
106
src/module.ts
106
src/module.ts
@@ -1,14 +1,12 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, createResolver, addPlugin } from '@nuxt/kit'
|
||||
import { defaultExtractor as createDefaultExtractor } from 'tailwindcss/lib/lib/defaultExtractor.js'
|
||||
import { iconsPlugin, getIconCollections, type CollectionNames, type IconsPluginOptions } from '@egoist/tailwindcss-icons'
|
||||
import type { CollectionNames, IconsPluginOptions } from '@egoist/tailwindcss-icons'
|
||||
import { name, version } from '../package.json'
|
||||
import createTemplates from './templates'
|
||||
import { generateSafelist, excludeColors, customSafelistExtractor } from './colors'
|
||||
import * as config from './runtime/ui.config'
|
||||
import type { DeepPartial, Strategy } from './runtime/types/utils'
|
||||
import installTailwind from './tailwind'
|
||||
|
||||
const defaultExtractor = createDefaultExtractor({ tailwindConfig: { separator: ':' } })
|
||||
const _require = createRequire(import.meta.url)
|
||||
const defaultColors = _require('tailwindcss/colors.js')
|
||||
|
||||
@@ -88,107 +86,13 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
nuxt.options.css.push(resolve(runtimeDir, 'ui.css'))
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
nuxt.hook('tailwindcss:config', function (tailwindConfig) {
|
||||
tailwindConfig.theme = tailwindConfig.theme || {}
|
||||
tailwindConfig.theme.extend = tailwindConfig.theme.extend || {}
|
||||
tailwindConfig.theme.extend.colors = tailwindConfig.theme.extend.colors || {}
|
||||
|
||||
const globalColors: any = {
|
||||
...(tailwindConfig.theme.colors || defaultColors),
|
||||
...tailwindConfig.theme.extend?.colors
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
globalColors.primary = tailwindConfig.theme.extend.colors.primary = {
|
||||
50: 'rgb(var(--color-primary-50) / <alpha-value>)',
|
||||
100: 'rgb(var(--color-primary-100) / <alpha-value>)',
|
||||
200: 'rgb(var(--color-primary-200) / <alpha-value>)',
|
||||
300: 'rgb(var(--color-primary-300) / <alpha-value>)',
|
||||
400: 'rgb(var(--color-primary-400) / <alpha-value>)',
|
||||
500: 'rgb(var(--color-primary-500) / <alpha-value>)',
|
||||
600: 'rgb(var(--color-primary-600) / <alpha-value>)',
|
||||
700: 'rgb(var(--color-primary-700) / <alpha-value>)',
|
||||
800: 'rgb(var(--color-primary-800) / <alpha-value>)',
|
||||
900: 'rgb(var(--color-primary-900) / <alpha-value>)',
|
||||
950: 'rgb(var(--color-primary-950) / <alpha-value>)',
|
||||
DEFAULT: 'rgb(var(--color-primary-DEFAULT) / <alpha-value>)'
|
||||
}
|
||||
|
||||
if (globalColors.gray) {
|
||||
// @ts-ignore
|
||||
globalColors.cool = tailwindConfig.theme.extend.colors.cool = defaultColors.gray
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
globalColors.gray = tailwindConfig.theme.extend.colors.gray = {
|
||||
50: 'rgb(var(--color-gray-50) / <alpha-value>)',
|
||||
100: 'rgb(var(--color-gray-100) / <alpha-value>)',
|
||||
200: 'rgb(var(--color-gray-200) / <alpha-value>)',
|
||||
300: 'rgb(var(--color-gray-300) / <alpha-value>)',
|
||||
400: 'rgb(var(--color-gray-400) / <alpha-value>)',
|
||||
500: 'rgb(var(--color-gray-500) / <alpha-value>)',
|
||||
600: 'rgb(var(--color-gray-600) / <alpha-value>)',
|
||||
700: 'rgb(var(--color-gray-700) / <alpha-value>)',
|
||||
800: 'rgb(var(--color-gray-800) / <alpha-value>)',
|
||||
900: 'rgb(var(--color-gray-900) / <alpha-value>)',
|
||||
950: 'rgb(var(--color-gray-950) / <alpha-value>)'
|
||||
}
|
||||
|
||||
const colors = excludeColors(globalColors)
|
||||
|
||||
// @ts-ignore
|
||||
nuxt.options.appConfig.ui = {
|
||||
primary: 'green',
|
||||
gray: 'cool',
|
||||
colors,
|
||||
strategy: 'merge'
|
||||
}
|
||||
|
||||
tailwindConfig.safelist = tailwindConfig.safelist || []
|
||||
tailwindConfig.safelist.push(...generateSafelist(options.safelistColors || [], colors))
|
||||
})
|
||||
|
||||
createTemplates(nuxt)
|
||||
|
||||
// Modules
|
||||
|
||||
await installModule('nuxt-icon')
|
||||
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
|
||||
await installModule('@nuxtjs/tailwindcss', {
|
||||
exposeConfig: true,
|
||||
config: {
|
||||
darkMode: 'class',
|
||||
plugins: [
|
||||
require('@tailwindcss/forms')({ strategy: 'class' }),
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/container-queries'),
|
||||
require('@headlessui/tailwindcss'),
|
||||
iconsPlugin(Array.isArray(options.icons) || options.icons === 'all' ? { collections: getIconCollections(options.icons) } : typeof options.icons === 'object' ? options.icons as IconsPluginOptions : {})
|
||||
],
|
||||
content: {
|
||||
files: [
|
||||
resolve(runtimeDir, 'components/**/*.{vue,mjs,ts}'),
|
||||
resolve(runtimeDir, 'ui.config/**/*.{mjs,js,ts}')
|
||||
],
|
||||
transform: {
|
||||
vue: (content) => {
|
||||
return content.replaceAll(/(?:\r\n|\r|\n)/g, ' ')
|
||||
}
|
||||
},
|
||||
extract: {
|
||||
vue: (content) => {
|
||||
return [
|
||||
...defaultExtractor(content),
|
||||
// @ts-ignore
|
||||
...customSafelistExtractor(options.prefix, content, nuxt.options.appConfig.ui.colors, options.safelistColors)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
await installTailwind(options, nuxt, resolve)
|
||||
|
||||
// Plugins
|
||||
|
||||
@@ -200,6 +104,10 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
src: resolve(runtimeDir, 'plugins', 'modals')
|
||||
})
|
||||
|
||||
addPlugin({
|
||||
src: resolve(runtimeDir, 'plugins', 'slideovers')
|
||||
})
|
||||
|
||||
// Components
|
||||
|
||||
addComponentsDir({
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper" v-bind="attrs">
|
||||
<table :class="[ui.base, ui.divide]">
|
||||
<slot v-if="$slots.caption || caption" name="caption">
|
||||
<caption :class="ui.caption">
|
||||
{{ caption }}
|
||||
</caption>
|
||||
</slot>
|
||||
<thead :class="ui.thead">
|
||||
<tr :class="ui.tr.base">
|
||||
<th v-if="modelValue" scope="col" :class="ui.checkbox.padding">
|
||||
<UCheckbox :model-value="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" aria-label="Select all" @change="onChange" />
|
||||
<UCheckbox :model-value="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" v-bind="ui.default.checkbox" aria-label="Select all" @change="onChange" />
|
||||
</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, column.class]">
|
||||
<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, column.class]"
|
||||
:aria-sort="getAriaSort(column)"
|
||||
>
|
||||
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
|
||||
<UButton
|
||||
v-if="column.sortable"
|
||||
@@ -57,7 +68,7 @@
|
||||
<template v-else>
|
||||
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected, $attrs.onSelect && ui.tr.active, row?.class]" @click="() => onSelect(row)">
|
||||
<td v-if="modelValue" :class="ui.checkbox.padding">
|
||||
<UCheckbox v-model="selected" :value="row" aria-label="Select row" @click.stop />
|
||||
<UCheckbox v-model="selected" :value="row" v-bind="ui.default.checkbox" aria-label="Select row" @click.stop />
|
||||
</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, row[column.key]?.class]">
|
||||
@@ -74,7 +85,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, toRaw, toRef } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { PropType, AriaAttributes } from 'vue'
|
||||
import { upperFirst } from 'scule'
|
||||
import { defu } from 'defu'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
@@ -107,6 +118,15 @@ function defaultSort (a: any, b: any, direction: 'asc' | 'desc') {
|
||||
}
|
||||
}
|
||||
|
||||
interface Column {
|
||||
key: string
|
||||
sortable?: boolean
|
||||
sort?: (a: any, b: any, direction: 'asc' | 'desc') => number
|
||||
direction?: 'asc' | 'desc'
|
||||
class?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UIcon,
|
||||
@@ -129,7 +149,7 @@ export default defineComponent({
|
||||
default: () => []
|
||||
},
|
||||
columns: {
|
||||
type: Array as PropType<{ key: string, sortable?: boolean, sort?: (a: any, b: any, direction: 'asc' | 'desc') => number, direction?: 'asc' | 'desc', class?: string, [key: string]: any }[]>,
|
||||
type: Array as PropType<Column[]>,
|
||||
default: null
|
||||
},
|
||||
columnAttribute: {
|
||||
@@ -168,6 +188,10 @@ export default defineComponent({
|
||||
type: Object as PropType<{ icon: string, label: string }>,
|
||||
default: () => config.default.emptyState
|
||||
},
|
||||
caption: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
progress: {
|
||||
type: Object as PropType<{ color: ProgressColor, animation: ProgressAnimation }>,
|
||||
default: () => config.default.progress
|
||||
@@ -280,8 +304,8 @@ export default defineComponent({
|
||||
})
|
||||
}
|
||||
|
||||
function onChange (event: any) {
|
||||
if (event.target.checked) {
|
||||
function onChange (checked: boolean) {
|
||||
if (checked) {
|
||||
selectAllRows()
|
||||
} else {
|
||||
selected.value = []
|
||||
@@ -292,6 +316,26 @@ export default defineComponent({
|
||||
return get(row, rowKey, defaultValue)
|
||||
}
|
||||
|
||||
function getAriaSort (column: Column): AriaAttributes['aria-sort'] {
|
||||
if (!column.sortable) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (sort.value.column !== column.key) {
|
||||
return 'none'
|
||||
}
|
||||
|
||||
if (sort.value.direction === 'asc') {
|
||||
return 'ascending'
|
||||
}
|
||||
|
||||
if (sort.value.direction === 'desc') {
|
||||
return 'descending'
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
@@ -312,7 +356,8 @@ export default defineComponent({
|
||||
onSort,
|
||||
onSelect,
|
||||
onChange,
|
||||
getRowData
|
||||
getRowData,
|
||||
getAriaSort
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -39,20 +39,34 @@
|
||||
@before-leave="onBeforeLeave"
|
||||
@leave="onLeave"
|
||||
>
|
||||
<div v-show="open">
|
||||
<HDisclosurePanel :class="[ui.item.base, ui.item.size, ui.item.color, ui.item.padding]" static>
|
||||
<slot :name="item.slot || 'item'" :item="item" :index="index" :open="open" :close="close">
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
</HDisclosurePanel>
|
||||
</div>
|
||||
<HDisclosurePanel
|
||||
v-if="unmount"
|
||||
:class="[ui.item.base, ui.item.size, ui.item.color, ui.item.padding]"
|
||||
unmount
|
||||
>
|
||||
<slot :name="item.slot || 'item'" :item="item" :index="index" :open="open" :close="close">
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
</HDisclosurePanel>
|
||||
<template v-else>
|
||||
<div v-show="open">
|
||||
<HDisclosurePanel
|
||||
:class="[ui.item.base, ui.item.size, ui.item.color, ui.item.padding]"
|
||||
static
|
||||
>
|
||||
<slot :name="item.slot || 'item'" :item="item" :index="index" :open="open" :close="close">
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
</HDisclosurePanel>
|
||||
</div>
|
||||
</template>
|
||||
</Transition>
|
||||
</HDisclosure>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, toRef, defineComponent } from 'vue'
|
||||
import { ref, computed, toRef, defineComponent, watch } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { Disclosure as HDisclosure, DisclosureButton as HDisclosureButton, DisclosurePanel as HDisclosurePanel, provideUseId } from '@headlessui/vue'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
@@ -91,6 +105,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: () => config.default.openIcon
|
||||
},
|
||||
unmount: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
closeIcon: {
|
||||
type: String,
|
||||
default: () => config.default.closeIcon
|
||||
@@ -108,12 +126,25 @@ export default defineComponent({
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
emits: ['open'],
|
||||
setup (props, { emit }) {
|
||||
const { ui, attrs } = useUI('accordion', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
const uiButton = computed<typeof configButton>(() => configButton)
|
||||
|
||||
const buttonRefs = ref<{ open: boolean, close: (e: EventTarget) => {} }[]>([])
|
||||
|
||||
const openedStates = computed(() => buttonRefs.value.map(({ open }) => open))
|
||||
watch(openedStates, (newValue, oldValue) => {
|
||||
for (const index in newValue) {
|
||||
const isOpenBefore = oldValue[index]
|
||||
const isOpenAfter = newValue[index]
|
||||
|
||||
if (!isOpenBefore && isOpenAfter) {
|
||||
emit('open', index)
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function closeOthers (currentIndex: number, e: Event) {
|
||||
if (!props.items[currentIndex].closeOthers && props.multiple) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<template>
|
||||
<div :class="alertClass" v-bind="attrs">
|
||||
<div class="flex" :class="[ui.gap, { 'items-start': (description || $slots.description), 'items-center': !description && !$slots.description }]">
|
||||
<UIcon v-if="icon" :name="icon" :class="ui.icon.base" />
|
||||
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
|
||||
<slot name="icon" :icon="icon">
|
||||
<UIcon v-if="icon" :name="icon" :class="ui.icon.base" />
|
||||
</slot>
|
||||
<slot name="avatar" :avatar="avatar">
|
||||
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
|
||||
</slot>
|
||||
|
||||
<div :class="ui.inner">
|
||||
<p v-if="(title || $slots.title)" :class="ui.title">
|
||||
@@ -10,19 +14,23 @@
|
||||
{{ title }}
|
||||
</slot>
|
||||
</p>
|
||||
<p v-if="description || $slots.description" :class="twMerge(ui.description, !(title && $slots.title) && 'mt-0 leading-5')">
|
||||
<div v-if="description || $slots.description" :class="twMerge(ui.description, !(title && $slots.title) && 'mt-0 leading-5')">
|
||||
<slot name="description" :description="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="(description || $slots.description) && actions.length" :class="ui.actions">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
|
||||
<div v-if="(description || $slots.description) && (actions.length || $slots.actions)" :class="ui.actions">
|
||||
<slot name="actions">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="closeButton || (!description && !$slots.description && actions.length)" :class="twMerge(ui.actions, 'mt-0')">
|
||||
<template v-if="!description && !$slots.description && actions.length">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
|
||||
<template v-if="!description && !$slots.description && (actions.length || $slots.actions)">
|
||||
<slot name="actions">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<UButton v-if="closeButton" aria-label="Close" v-bind="{ ...(ui.default.closeButton || {}), ...closeButton }" @click.stop="$emit('close')" />
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<span :class="wrapperClass">
|
||||
<img
|
||||
<component
|
||||
:is="as"
|
||||
v-if="url && !error"
|
||||
:class="imgClass"
|
||||
:alt="alt"
|
||||
:src="url"
|
||||
v-bind="attrs"
|
||||
@error="onError"
|
||||
>
|
||||
/>
|
||||
<span v-else-if="text" :class="ui.text">{{ text }}</span>
|
||||
<UIcon v-else-if="icon" :name="icon" :class="iconClass" />
|
||||
<span v-else-if="placeholder" :class="ui.placeholder">{{ placeholder }}</span>
|
||||
@@ -39,6 +40,10 @@ export default defineComponent({
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
as: {
|
||||
type: [String, Object],
|
||||
default: 'img'
|
||||
},
|
||||
src: {
|
||||
type: [String, Boolean],
|
||||
default: null
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
:class="ui.item"
|
||||
:role="indicators ? 'tabpanel' : null"
|
||||
>
|
||||
<slot :item="item" :index="index" />
|
||||
</div>
|
||||
@@ -34,11 +35,13 @@
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-if="indicators" :class="ui.indicators.wrapper">
|
||||
<div v-if="indicators" role="tablist" :class="ui.indicators.wrapper">
|
||||
<template v-for="page in pages" :key="page">
|
||||
<slot name="indicator" :on-click="onClick" :active="page === currentPage" :page="page">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="page === currentPage"
|
||||
:class="[
|
||||
ui.indicators.base,
|
||||
page === currentPage ? ui.indicators.active : ui.indicators.inactive
|
||||
@@ -53,7 +56,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, toRef, toRefs, computed, defineComponent } from 'vue'
|
||||
import { ref, toRef, computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { mergeConfig } from '../../utils'
|
||||
@@ -109,10 +112,9 @@ export default defineComponent({
|
||||
const carouselRef = ref<HTMLElement>()
|
||||
const itemWidth = ref(0)
|
||||
|
||||
const { x, arrivedState } = useScroll(carouselRef, { behavior: 'smooth' })
|
||||
const { width: carouselWidth } = useElementSize(carouselRef)
|
||||
const { x } = useScroll(carouselRef, { behavior: 'smooth' })
|
||||
|
||||
const { left: isFirst, right: isLast } = toRefs(arrivedState)
|
||||
const { width: carouselWidth } = useElementSize(carouselRef)
|
||||
|
||||
useCarouselScroll(carouselRef)
|
||||
|
||||
@@ -122,7 +124,13 @@ export default defineComponent({
|
||||
itemWidth.value = entry?.target?.firstElementChild?.clientWidth || 0
|
||||
})
|
||||
|
||||
const currentPage = computed(() => Math.round(x.value / itemWidth.value) + 1)
|
||||
const currentPage = computed(() => {
|
||||
if (!itemWidth.value) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return Math.round(x.value / itemWidth.value) + 1
|
||||
})
|
||||
|
||||
const pages = computed(() => {
|
||||
if (!itemWidth.value) {
|
||||
@@ -132,6 +140,9 @@ export default defineComponent({
|
||||
return props.items.length - Math.round(carouselWidth.value / itemWidth.value) + 1
|
||||
})
|
||||
|
||||
const isFirst = computed(() => currentPage.value <= 1)
|
||||
const isLast = computed(() => currentPage.value === pages.value)
|
||||
|
||||
function onClickNext () {
|
||||
x.value += itemWidth.value
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:class="ui.trigger"
|
||||
role="button"
|
||||
@mouseenter="onMouseEnter"
|
||||
@touchstart.prevent="onTouchStart"
|
||||
@touchstart.passive="onTouchStart"
|
||||
>
|
||||
<slot :open="open" :disabled="disabled">
|
||||
<button :disabled="disabled">
|
||||
@@ -17,7 +17,7 @@
|
||||
</slot>
|
||||
</HMenuButton>
|
||||
|
||||
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" :style="containerStyle">
|
||||
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseenter="onMouseEnter">
|
||||
<Transition appear v-bind="ui.transition">
|
||||
<div>
|
||||
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(ui.arrow)" />
|
||||
@@ -182,8 +182,8 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
function onTouchStart () {
|
||||
if (!menuApi.value) {
|
||||
function onTouchStart (event: TouchEvent) {
|
||||
if (!event.cancelable || !menuApi.value) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper">
|
||||
<div :class="ui.wrapper" :data-n-ids="attrs['data-n-ids']">
|
||||
<div :class="ui.container">
|
||||
<input
|
||||
:id="inputId"
|
||||
@@ -119,7 +119,7 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
emit('change', event)
|
||||
emit('change', (event.target as HTMLInputElement).checked)
|
||||
emitFormChange()
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import { useEventBus } from '@vueuse/core'
|
||||
import type { ZodSchema } from 'zod'
|
||||
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
|
||||
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
|
||||
import type { ObjectSchemaAsync as ValibotObjectSchema } from 'valibot'
|
||||
import type { BaseSchema as ValibotSchema30, BaseSchemaAsync as ValibotSchemaAsync30 } from 'valibot30'
|
||||
import type { GenericSchema as ValibotSchema, GenericSchemaAsync as ValibotSchemaAsync, SafeParser as ValibotSafeParser, SafeParserAsync as ValibotSafeParserAsync } from 'valibot'
|
||||
import type { FormError, FormEvent, FormEventType, FormSubmitEvent, FormErrorEvent, Form } from '../../types/form'
|
||||
import { useId } from '#imports'
|
||||
|
||||
@@ -25,11 +26,13 @@ class FormException extends Error {
|
||||
export default defineComponent({
|
||||
props: {
|
||||
schema: {
|
||||
type: Object as
|
||||
type: [Object, Function] as
|
||||
| PropType<ZodSchema>
|
||||
| PropType<YupObjectSchema<any>>
|
||||
| PropType<JoiSchema>
|
||||
| PropType<ValibotObjectSchema<any>>,
|
||||
| PropType<ValibotSchema30 | ValibotSchemaAsync30>
|
||||
| PropType<ValibotSchema | ValibotSchemaAsync>
|
||||
| PropType<ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any>>,
|
||||
default: undefined
|
||||
},
|
||||
state: {
|
||||
@@ -151,7 +154,6 @@ export default defineComponent({
|
||||
validate,
|
||||
errors,
|
||||
setErrors (errs: FormError[], path?: string) {
|
||||
errors.value = errs
|
||||
if (path) {
|
||||
errors.value = errors.value.filter(
|
||||
(error) => error.path !== path
|
||||
@@ -256,21 +258,19 @@ async function getJoiErrors (
|
||||
}
|
||||
}
|
||||
|
||||
function isValibotSchema (schema: any): schema is ValibotObjectSchema<any> {
|
||||
return schema._parse !== undefined
|
||||
function isValibotSchema (schema: any): schema is ValibotSchema30 | ValibotSchemaAsync30 | ValibotSchema | ValibotSchemaAsync | ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any> {
|
||||
return '_parse' in schema || '_run' in schema || (typeof schema === 'function' && 'schema' in schema)
|
||||
}
|
||||
|
||||
async function getValibotError (
|
||||
state: any,
|
||||
schema: ValibotObjectSchema<any>
|
||||
schema: ValibotSchema30 | ValibotSchemaAsync30 | ValibotSchema | ValibotSchemaAsync | ValibotSafeParser<any, any> | ValibotSafeParserAsync<any, any>
|
||||
): Promise<FormError[]> {
|
||||
const result = await schema._parse(state)
|
||||
if (result.issues) {
|
||||
return result.issues.map((issue) => ({
|
||||
path: issue.path?.map(p => p.key).join('.') || '',
|
||||
message: issue.message
|
||||
}))
|
||||
}
|
||||
return []
|
||||
const result = await ('_parse' in schema ? schema._parse(state) : '_run' in schema ? schema._run({ typed: false, value: state }, {}) : schema(state))
|
||||
return result.issues?.map((issue) => ({
|
||||
// We know that the key for a form schema is always a string or a number
|
||||
path: issue.path?.map((item) => item.key).join('.') || '',
|
||||
message: issue.message
|
||||
})) || []
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper">
|
||||
<div :class="(type === 'hidden') ? 'hidden' : ui.wrapper">
|
||||
<input
|
||||
:id="inputId"
|
||||
ref="input"
|
||||
@@ -163,7 +163,7 @@ export default defineComponent({
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'blur'],
|
||||
emits: ['update:modelValue', 'blur', 'change'],
|
||||
setup (props, { emit, slots }) {
|
||||
const { ui, attrs } = useUI('input', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
@@ -205,15 +205,19 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
|
||||
if (modelModifiers.value.lazy) {
|
||||
updateInput(value)
|
||||
}
|
||||
|
||||
// Update trimmed input so that it has same behavior as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
|
||||
if (modelModifiers.value.trim) {
|
||||
(event.target as HTMLInputElement).value = value.trim()
|
||||
if (props.type === 'file') {
|
||||
const value = (event.target as HTMLInputElement).files
|
||||
emit('change', value)
|
||||
} else {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
emit('change', value)
|
||||
if (modelModifiers.value.lazy) {
|
||||
updateInput(value)
|
||||
}
|
||||
// Update trimmed input so that it has same behavior as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
|
||||
if (modelModifiers.value.trim) {
|
||||
(event.target as HTMLInputElement).value = value.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +240,7 @@ export default defineComponent({
|
||||
ui.value.form,
|
||||
rounded.value,
|
||||
ui.value.placeholder,
|
||||
props.type === 'file' && [ui.value.file.base, ui.value.file.padding[size.value]],
|
||||
props.type === 'file' && ui.value.file.base,
|
||||
ui.value.size[size.value],
|
||||
props.padded ? ui.value.padding[size.value] : 'p-0',
|
||||
variant?.replaceAll('{color}', color.value),
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
autocomplete="off"
|
||||
v-bind="attrs"
|
||||
:display-value="() => query ? query : label"
|
||||
@change="onChange"
|
||||
@change="onQueryChange"
|
||||
/>
|
||||
|
||||
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
|
||||
@@ -249,6 +249,10 @@ export default defineComponent({
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
searchLazy: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
debounce: {
|
||||
type: Number,
|
||||
default: 200
|
||||
@@ -407,6 +411,8 @@ export default defineComponent({
|
||||
return child !== null && child !== undefined && String(child).search(new RegExp(query.value, 'i')) !== -1
|
||||
})
|
||||
})
|
||||
}, [], {
|
||||
lazy: props.searchLazy
|
||||
})
|
||||
|
||||
watch(container, (value) => {
|
||||
@@ -418,14 +424,15 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
function onUpdate (event: any) {
|
||||
function onUpdate (value: any) {
|
||||
query.value = ''
|
||||
emit('update:modelValue', event)
|
||||
emit('change', event)
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
|
||||
emitFormChange()
|
||||
}
|
||||
|
||||
function onChange (event: any) {
|
||||
function onQueryChange (event: any) {
|
||||
query.value = event.target.value
|
||||
}
|
||||
|
||||
@@ -459,7 +466,7 @@ export default defineComponent({
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
query,
|
||||
onUpdate,
|
||||
onChange
|
||||
onQueryChange
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper">
|
||||
<div :class="ui.wrapper" :data-n-ids="attrs['data-n-ids']">
|
||||
<div :class="ui.container">
|
||||
<input
|
||||
:id="inputId"
|
||||
@@ -11,6 +11,7 @@
|
||||
type="radio"
|
||||
:class="inputClass"
|
||||
v-bind="attrs"
|
||||
@change="onChange"
|
||||
>
|
||||
</div>
|
||||
<div v-if="label || $slots.label" :class="ui.inner">
|
||||
@@ -42,6 +43,7 @@ import { useId } from '#imports'
|
||||
const config = mergeConfig<typeof radio>(appConfig.ui.strategy, appConfig.ui.radio, radio)
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
@@ -110,14 +112,16 @@ export default defineComponent({
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
|
||||
if (!radioGroup) {
|
||||
emitFormChange()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function onChange (event: Event) {
|
||||
emit('change', (event.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return twMerge(twJoin(
|
||||
ui.value.base,
|
||||
@@ -138,7 +142,8 @@ export default defineComponent({
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
name,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
inputClass
|
||||
inputClass,
|
||||
onChange
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -107,7 +107,7 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
emit('change', event)
|
||||
emit('change', (event.target as HTMLInputElement).value)
|
||||
emitFormChange()
|
||||
}
|
||||
|
||||
|
||||
@@ -194,16 +194,16 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
emit('change', (event.target as HTMLInputElement).value)
|
||||
emitFormChange()
|
||||
emit('change', event)
|
||||
}
|
||||
|
||||
const guessOptionValue = (option: any) => {
|
||||
return get(option, props.valueAttribute, get(option, props.optionAttribute))
|
||||
return get(option, props.valueAttribute, '')
|
||||
}
|
||||
|
||||
const guessOptionText = (option: any) => {
|
||||
return get(option, props.optionAttribute, get(option, props.valueAttribute))
|
||||
return get(option, props.optionAttribute, '')
|
||||
}
|
||||
|
||||
const normalizeOption = (option: any) => {
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
:class="uiMenu.input"
|
||||
@change="onChange"
|
||||
@change="onQueryChange"
|
||||
/>
|
||||
<component
|
||||
:is="searchable ? 'HComboboxOption' : 'HListboxOption'"
|
||||
@@ -105,12 +105,12 @@
|
||||
</div>
|
||||
</li>
|
||||
</component>
|
||||
<p v-else-if="searchable && query && !filteredOptions.length" :class="uiMenu.option.empty">
|
||||
<p v-else-if="searchable && query && !filteredOptions?.length" :class="uiMenu.option.empty">
|
||||
<slot name="option-empty" :query="query">
|
||||
No results for "{{ query }}".
|
||||
</slot>
|
||||
</p>
|
||||
<p v-else-if="!filteredOptions.length" :class="uiMenu.empty">
|
||||
<p v-else-if="!filteredOptions?.length" :class="uiMenu.empty">
|
||||
<slot name="empty" :query="query">
|
||||
No options.
|
||||
</slot>
|
||||
@@ -174,7 +174,7 @@ export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
type: [String, Number, Object, Array, Boolean],
|
||||
default: ''
|
||||
},
|
||||
query: {
|
||||
@@ -249,6 +249,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: 'Search...'
|
||||
},
|
||||
searchableLazy: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
clearSearchOnClose: {
|
||||
type: Boolean,
|
||||
default: () => configMenu.default.clearSearchOnClose
|
||||
@@ -362,7 +366,7 @@ export default defineComponent({
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} else if (props.modelValue) {
|
||||
} else if (props.modelValue !== undefined && props.modelValue !== null) {
|
||||
if (props.valueAttribute) {
|
||||
const option = props.options.find(option => option[props.valueAttribute] === props.modelValue)
|
||||
return option ? option[props.optionAttribute] : null
|
||||
@@ -470,6 +474,8 @@ export default defineComponent({
|
||||
return child !== null && child !== undefined && String(child).search(new RegExp(query.value, 'i')) !== -1
|
||||
})
|
||||
})
|
||||
}, [], {
|
||||
lazy: props.searchableLazy
|
||||
})
|
||||
|
||||
const createOption = computed(() => {
|
||||
@@ -505,13 +511,13 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
function onUpdate (event: any) {
|
||||
emit('update:modelValue', event)
|
||||
emit('change', event)
|
||||
function onUpdate (value: any) {
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
emitFormChange()
|
||||
}
|
||||
|
||||
function onChange (event: any) {
|
||||
function onQueryChange (event: any) {
|
||||
query.value = event.target.value
|
||||
}
|
||||
|
||||
@@ -546,7 +552,7 @@ export default defineComponent({
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
query,
|
||||
onUpdate,
|
||||
onChange
|
||||
onQueryChange
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -131,7 +131,7 @@ export default defineComponent({
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'blur'],
|
||||
emits: ['update:modelValue', 'blur', 'change'],
|
||||
setup (props, { emit }) {
|
||||
const { ui, attrs } = useUI('textarea', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
@@ -192,6 +192,7 @@ export default defineComponent({
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
emit('change', value)
|
||||
|
||||
if (modelModifiers.value.lazy) {
|
||||
updateInput(value)
|
||||
|
||||
@@ -3,15 +3,26 @@
|
||||
:id="inputId"
|
||||
v-model="active"
|
||||
:name="name"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || loading"
|
||||
:class="switchClass"
|
||||
v-bind="attrs"
|
||||
>
|
||||
<span :class="containerClass">
|
||||
<span v-if="onIcon" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
|
||||
<span v-if="loading" :class="[ui.icon.active, ui.icon.base]" aria-hidden="true">
|
||||
<UIcon :name="loadingIcon" :class="loadingIconClass" />
|
||||
</span>
|
||||
<span
|
||||
v-if="!loading && onIcon"
|
||||
:class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<UIcon :name="onIcon" :class="onIconClass" />
|
||||
</span>
|
||||
<span v-if="offIcon" :class="[active ? ui.icon.inactive : ui.icon.active, ui.icon.base]" aria-hidden="true">
|
||||
<span
|
||||
v-if="!loading && offIcon"
|
||||
:class="[active ? ui.icon.inactive : ui.icon.active, ui.icon.base]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<UIcon :name="offIcon" :class="offIconClass" />
|
||||
</span>
|
||||
</span>
|
||||
@@ -58,6 +69,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
onIcon: {
|
||||
type: String,
|
||||
default: () => config.default.onIcon
|
||||
@@ -66,6 +81,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: () => config.default.offIcon
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => config.default.loadingIcon
|
||||
},
|
||||
color: {
|
||||
type: String as PropType<ToggleColor>,
|
||||
default: () => config.default.color,
|
||||
@@ -101,6 +120,8 @@ export default defineComponent({
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
|
||||
emitFormChange()
|
||||
}
|
||||
})
|
||||
@@ -137,6 +158,13 @@ export default defineComponent({
|
||||
)
|
||||
})
|
||||
|
||||
const loadingIconClass = computed(() => {
|
||||
return twJoin(
|
||||
ui.value.icon.size[props.size],
|
||||
color.value && ui.value.icon.loading.replaceAll('{color}', color.value)
|
||||
)
|
||||
})
|
||||
|
||||
provideUseId(() => useId())
|
||||
|
||||
return {
|
||||
@@ -150,7 +178,8 @@ export default defineComponent({
|
||||
switchClass,
|
||||
containerClass,
|
||||
onIconClass,
|
||||
offIconClass
|
||||
offIconClass,
|
||||
loadingIconClass
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
<ULink
|
||||
as="span"
|
||||
:class="[ui.base, index === links.length - 1 ? ui.active : !!link.to ? ui.inactive : '']"
|
||||
v-bind="getULinkProps(link)"
|
||||
:aria-current="index === links.length - 1 ? 'page' : undefined"
|
||||
v-bind="getULinkProps(link)"
|
||||
@click="link.click"
|
||||
>
|
||||
<slot name="icon" :link="link" :index="index" :is-active="index === links.length - 1">
|
||||
<UIcon
|
||||
|
||||
@@ -321,7 +321,10 @@ export default defineComponent({
|
||||
)
|
||||
})
|
||||
|
||||
const emptyState = computed(() => ({ ...ui.value.default.emptyState, ...props.emptyState }))
|
||||
const emptyState = computed(() => {
|
||||
if (props.emptyState === null) return null
|
||||
return { ...ui.value.default.emptyState, ...props.emptyState }
|
||||
})
|
||||
|
||||
// Methods
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<UButton
|
||||
v-for="(page, index) of displayedPages"
|
||||
:key="`${page}-${index}`"
|
||||
:to="typeof page === 'number' ? to?.(page) : null"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
:label="`${page}`"
|
||||
@@ -69,6 +70,7 @@
|
||||
<script lang="ts">
|
||||
import { computed, toRef, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationRaw } from '#vue-router'
|
||||
import UButton from '../elements/Button.vue'
|
||||
import { useUI } from '../../composables/useUI'
|
||||
import { mergeConfig } from '../../utils'
|
||||
@@ -117,6 +119,10 @@ export default defineComponent({
|
||||
return Object.keys(buttonConfig.size).includes(value)
|
||||
}
|
||||
},
|
||||
to: {
|
||||
type: Function as PropType<(page: number) => RouteLocationRaw>,
|
||||
default: null
|
||||
},
|
||||
activeButton: {
|
||||
type: Object as PropType<Button>,
|
||||
default: () => config.default.activeButton as Button
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</HTab>
|
||||
</HTabList>
|
||||
|
||||
<HTabPanels :class="ui.container">
|
||||
<HTabPanels v-if="content" :class="ui.container">
|
||||
<HTabPanel v-for="(item, index) of items" :key="index" v-slot="{ selected }" :class="ui.base" :unmount="unmount">
|
||||
<slot :name="item.slot || 'item'" :item="item" :index="index" :selected="selected">
|
||||
{{ item.content }}
|
||||
@@ -88,6 +88,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
content: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array] as PropType<any>,
|
||||
default: () => ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TransitionRoot :appear="appear" :show="isOpen" as="template">
|
||||
<TransitionRoot :appear="appear" :show="isOpen" as="template" @after-leave="onAfterLeave">
|
||||
<HDialog :class="ui.wrapper" v-bind="attrs" @close="close">
|
||||
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
|
||||
<div :class="[ui.overlay.base, ui.overlay.background]" />
|
||||
@@ -82,7 +82,7 @@ export default defineComponent({
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close', 'close-prevented'],
|
||||
emits: ['update:modelValue', 'close', 'close-prevented', 'after-leave'],
|
||||
setup (props, { emit }) {
|
||||
const { ui, attrs } = useUI('modal', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
@@ -117,6 +117,10 @@ export default defineComponent({
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const onAfterLeave = () => {
|
||||
emit('after-leave')
|
||||
}
|
||||
|
||||
provideUseId(() => useId())
|
||||
|
||||
return {
|
||||
@@ -125,6 +129,7 @@ export default defineComponent({
|
||||
attrs,
|
||||
isOpen,
|
||||
transitionClass,
|
||||
onAfterLeave,
|
||||
close
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<component :is="modalState.component" v-if="modalState" v-bind="modalState.props" v-model="isOpen" />
|
||||
<component
|
||||
:is="modalState.component"
|
||||
v-if="modalState"
|
||||
v-bind="modalState.props"
|
||||
v-model="isOpen"
|
||||
@after-leave="reset"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -8,5 +14,5 @@ import { useModal, modalInjectionKey } from '../../composables/useModal'
|
||||
|
||||
const modalState = inject(modalInjectionKey)
|
||||
|
||||
const { isOpen } = useModal()
|
||||
const { isOpen, reset } = useModal()
|
||||
</script>
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
{{ title }}
|
||||
</slot>
|
||||
</p>
|
||||
<p v-if="(description || $slots.description)" :class="twMerge(ui.description, !(title && $slots.title) && 'mt-0 leading-5')">
|
||||
<div v-if="(description || $slots.description)" :class="twMerge(ui.description, !(title && $slots.title) && 'mt-0 leading-5')">
|
||||
<slot name="description" :description="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="(description || $slots.description) && actions.length" :class="ui.actions">
|
||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...(ui.default.actionButton || {}), ...action }" @click.stop="onAction(action)" />
|
||||
@@ -43,7 +43,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, toRef, onMounted, onUnmounted, watchEffect, defineComponent } from 'vue'
|
||||
import { ref, computed, toRef, onMounted, onUnmounted, watch, watchEffect, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { twMerge, twJoin } from 'tailwind-merge'
|
||||
import UIcon from '../elements/Icon.vue'
|
||||
@@ -123,7 +123,7 @@ export default defineComponent({
|
||||
setup (props, { emit }) {
|
||||
const { ui, attrs } = useUI('notification', toRef(props, 'ui'), config)
|
||||
|
||||
let timer: any = null
|
||||
let timer: null | ReturnType<typeof useTimer> = null
|
||||
const remaining = ref(props.timeout)
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
@@ -131,7 +131,8 @@ export default defineComponent({
|
||||
ui.value.wrapper,
|
||||
ui.value.background?.replaceAll('{color}', props.color),
|
||||
ui.value.rounded,
|
||||
ui.value.shadow
|
||||
ui.value.shadow,
|
||||
ui.value.ring?.replaceAll('{color}', props.color)
|
||||
), props.class)
|
||||
})
|
||||
|
||||
@@ -191,7 +192,11 @@ export default defineComponent({
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
function initTimer () {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
}
|
||||
|
||||
if (!props.timeout) {
|
||||
return
|
||||
}
|
||||
@@ -203,7 +208,11 @@ export default defineComponent({
|
||||
watchEffect(() => {
|
||||
remaining.value = timer.remaining.value
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.timeout, initTimer)
|
||||
|
||||
onMounted(initTimer)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:class="ui.trigger"
|
||||
role="button"
|
||||
@mouseenter="onMouseEnter"
|
||||
@touchstart.prevent="onTouchStart"
|
||||
@touchstart.passive="onTouchStart"
|
||||
>
|
||||
<slot :open="open" :close="close">
|
||||
<button :disabled="disabled">
|
||||
@@ -21,7 +21,7 @@
|
||||
<div v-if="open" :class="[ui.overlay.base, ui.overlay.background]" />
|
||||
</Transition>
|
||||
|
||||
<div v-if="open" ref="container" :class="[ui.container, ui.width]" :style="containerStyle">
|
||||
<div v-if="open" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseenter="onMouseEnter">
|
||||
<Transition appear v-bind="ui.transition">
|
||||
<div>
|
||||
<div v-if="popper.arrow" data-popper-arrow :class="Object.values(ui.arrow)" />
|
||||
@@ -154,8 +154,8 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
function onTouchStart () {
|
||||
if (!popoverApi.value) {
|
||||
function onTouchStart (event: TouchEvent) {
|
||||
if (!event.cancelable || !popoverApi.value) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<TransitionRoot as="template" :appear="appear" :show="isOpen">
|
||||
<HDialog :class="[ui.wrapper, { 'justify-end': side === 'right' }]" v-bind="attrs" @close="close">
|
||||
<TransitionRoot as="template" :appear="appear" :show="isOpen" @after-leave="onAfterLeave">
|
||||
<HDialog :class="[ui.wrapper, { 'justify-end': side === 'right' }, { 'items-end': side === 'bottom' }]" v-bind="attrs" @close="close">
|
||||
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
|
||||
<div :class="[ui.overlay.base, ui.overlay.background]" />
|
||||
</TransitionChild>
|
||||
|
||||
<TransitionChild as="template" :appear="appear" v-bind="transitionClass">
|
||||
<HDialogPanel :class="[ui.base, ui.width, ui.background, ui.ring, ui.padding]">
|
||||
<HDialogPanel :class="[ui.base, sideType === 'horizontal' ? [ui.width, 'h-full'] : [ui.height, 'w-full'], ui.background, ui.ring, ui.padding]">
|
||||
<slot />
|
||||
</HDialogPanel>
|
||||
</TransitionChild>
|
||||
@@ -46,9 +46,9 @@ export default defineComponent({
|
||||
default: false
|
||||
},
|
||||
side: {
|
||||
type: String as PropType<'left' | 'right'>,
|
||||
type: String as PropType<'left' | 'right' | 'top' | 'bottom'>,
|
||||
default: 'right',
|
||||
validator: (value: string) => ['left', 'right'].includes(value)
|
||||
validator: (value: string) => ['left', 'right', 'top', 'bottom'].includes(value)
|
||||
},
|
||||
overlay: {
|
||||
type: Boolean,
|
||||
@@ -71,7 +71,7 @@ export default defineComponent({
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close', 'close-prevented'],
|
||||
emits: ['update:modelValue', 'close', 'close-prevented', 'after-leave'],
|
||||
setup (props, { emit }) {
|
||||
const { ui, attrs } = useUI('slideover', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||
|
||||
@@ -89,14 +89,52 @@ export default defineComponent({
|
||||
return {}
|
||||
}
|
||||
|
||||
let enterFrom, leaveTo
|
||||
switch (props.side) {
|
||||
case 'left':
|
||||
enterFrom = ui.value.translate.left
|
||||
leaveTo = ui.value.translate.left
|
||||
break
|
||||
case 'right':
|
||||
enterFrom = ui.value.translate.right
|
||||
leaveTo = ui.value.translate.right
|
||||
break
|
||||
case 'top':
|
||||
enterFrom = ui.value.translate.top
|
||||
leaveTo = ui.value.translate.top
|
||||
break
|
||||
case 'bottom':
|
||||
enterFrom = ui.value.translate.bottom
|
||||
leaveTo = ui.value.translate.bottom
|
||||
break
|
||||
default:
|
||||
enterFrom = ui.value.translate.right
|
||||
leaveTo = ui.value.translate.right
|
||||
}
|
||||
|
||||
return {
|
||||
...ui.value.transition,
|
||||
enterFrom: props.side === 'left' ? ui.value.translate.left : ui.value.translate.right,
|
||||
enterFrom,
|
||||
enterTo: ui.value.translate.base,
|
||||
leaveFrom: ui.value.translate.base,
|
||||
leaveTo: props.side === 'left' ? ui.value.translate.left : ui.value.translate.right
|
||||
leaveTo
|
||||
}
|
||||
})
|
||||
const sideType = computed(() => {
|
||||
switch (props.side) {
|
||||
case 'left':
|
||||
return 'horizontal'
|
||||
case 'right':
|
||||
return 'horizontal'
|
||||
case 'top':
|
||||
return 'vertical'
|
||||
case 'bottom':
|
||||
return 'vertical'
|
||||
default:
|
||||
return 'right'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function close (value: boolean) {
|
||||
if (props.preventClose) {
|
||||
@@ -109,6 +147,10 @@ export default defineComponent({
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const onAfterLeave = () => {
|
||||
emit('after-leave')
|
||||
}
|
||||
|
||||
provideUseId(() => useId())
|
||||
|
||||
return {
|
||||
@@ -117,8 +159,10 @@ export default defineComponent({
|
||||
attrs,
|
||||
isOpen,
|
||||
transitionClass,
|
||||
sideType,
|
||||
onAfterLeave,
|
||||
close
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
18
src/runtime/components/overlays/Slideovers.client.vue
Normal file
18
src/runtime/components/overlays/Slideovers.client.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<component
|
||||
:is="slideoverState.component"
|
||||
v-if="slideoverState"
|
||||
v-bind="slideoverState.props"
|
||||
v-model="isOpen"
|
||||
@after-leave="reset"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue'
|
||||
import { useSlideover, slidOverInjectionKey } from '../../composables/useSlideover'
|
||||
|
||||
const slideoverState = inject(slidOverInjectionKey)
|
||||
|
||||
const { isOpen, reset } = useSlideover()
|
||||
</script>
|
||||
@@ -42,6 +42,17 @@ export function useProvideButtonGroup (buttonGroupProps: ButtonGroupProps) {
|
||||
export function useInjectButtonGroup ({ ui, props }: { ui: any, props: any }) {
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
provide('ButtonGroupContextConsumer', true)
|
||||
const isParentPartOfGroup = inject('ButtonGroupContextConsumer', false)
|
||||
|
||||
// early return if a parent is already part of the group
|
||||
if (isParentPartOfGroup) {
|
||||
return {
|
||||
size: computed(() => props.size),
|
||||
rounded: computed(() => ui.value.rounded)
|
||||
}
|
||||
}
|
||||
|
||||
let parent = instance.parent
|
||||
let groupContext: Ref<ButtonGroupContext> | undefined
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export const useCarouselScroll = (el: Ref<HTMLElement>) => {
|
||||
function onMouseUp () {
|
||||
el.value.style.removeProperty('scroll-behavior')
|
||||
el.value.style.removeProperty('scroll-snap-type')
|
||||
el.value.style.removeProperty('pointer-events')
|
||||
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('mouseup', onMouseUp)
|
||||
@@ -24,6 +25,8 @@ export const useCarouselScroll = (el: Ref<HTMLElement>) => {
|
||||
function onMouseMove (e) {
|
||||
e.preventDefault()
|
||||
|
||||
el.value.style.pointerEvents = 'none'
|
||||
|
||||
const delta = e.pageX - x.value
|
||||
|
||||
x.value = e.pageX
|
||||
|
||||
@@ -8,19 +8,29 @@ export const modalInjectionKey: InjectionKey<ShallowRef<ModalState>> = Symbol('n
|
||||
|
||||
function _useModal () {
|
||||
const modalState = inject(modalInjectionKey)
|
||||
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
function open<T extends Component> (component: T, props?: Modal & ComponentProps<T>) {
|
||||
if (!modalState) {
|
||||
throw new Error('useModal() is called without provider')
|
||||
}
|
||||
|
||||
modalState.value = {
|
||||
component,
|
||||
props: props ?? {}
|
||||
}
|
||||
|
||||
isOpen.value = true
|
||||
}
|
||||
|
||||
function close () {
|
||||
async function close () {
|
||||
if (!modalState) return
|
||||
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
function reset () {
|
||||
modalState.value = {
|
||||
component: 'div',
|
||||
props: {}
|
||||
@@ -31,6 +41,8 @@ function _useModal () {
|
||||
* Allows updating the modal props
|
||||
*/
|
||||
function patch <T extends Component = {}> (props: Partial<Modal & ComponentProps<T>>) {
|
||||
if (!modalState) return
|
||||
|
||||
modalState.value = {
|
||||
...modalState.value,
|
||||
props: {
|
||||
@@ -41,11 +53,12 @@ function _useModal () {
|
||||
}
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
patch
|
||||
reset,
|
||||
patch,
|
||||
isOpen
|
||||
}
|
||||
}
|
||||
|
||||
export const useModal = createSharedComposable(_useModal)
|
||||
export const useModal = createSharedComposable(_useModal)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import type {} from '@vueuse/shared'
|
||||
|
||||
export const _useShortcuts = () => {
|
||||
const macOS = computed(() => process.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/))
|
||||
const macOS = computed(() => import.meta.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/))
|
||||
|
||||
const metaSymbol = ref(' ')
|
||||
|
||||
|
||||
64
src/runtime/composables/useSlideover.ts
Normal file
64
src/runtime/composables/useSlideover.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ref, inject } from 'vue'
|
||||
import type { ShallowRef, Component, InjectionKey } from 'vue'
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
import type { ComponentProps } from '../types/component'
|
||||
import type { Slideover, SlideoverState } from '../types/slideover'
|
||||
|
||||
export const slidOverInjectionKey: InjectionKey<ShallowRef<SlideoverState>> = Symbol('nuxt-ui.slideover')
|
||||
|
||||
function _useSlideover () {
|
||||
const slideoverState = inject(slidOverInjectionKey)
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
function open<T extends Component> (component: T, props?: Slideover & ComponentProps<T>) {
|
||||
if (!slideoverState) {
|
||||
throw new Error('useSlideover() is called without provider')
|
||||
}
|
||||
|
||||
slideoverState.value = {
|
||||
component,
|
||||
props: props ?? {}
|
||||
}
|
||||
|
||||
isOpen.value = true
|
||||
}
|
||||
|
||||
async function close () {
|
||||
if (!slideoverState) return
|
||||
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
function reset () {
|
||||
slideoverState.value = {
|
||||
component: 'div',
|
||||
props: {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows updating the slideover props
|
||||
*/
|
||||
function patch<T extends Component = {}> (props: Partial<Slideover & ComponentProps<T>>) {
|
||||
if (!slideoverState) return
|
||||
|
||||
slideoverState.value = {
|
||||
...slideoverState.value,
|
||||
props: {
|
||||
...slideoverState.value.props,
|
||||
...props
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
reset,
|
||||
patch,
|
||||
isOpen
|
||||
}
|
||||
}
|
||||
|
||||
export const useSlideover = createSharedComposable(_useSlideover)
|
||||
@@ -22,8 +22,22 @@ export function useToast () {
|
||||
notifications.value = notifications.value.filter((n: Notification) => n.id !== id)
|
||||
}
|
||||
|
||||
function update (id: string, notification: Partial<Notification>) {
|
||||
const index = notifications.value.findIndex((n: Notification) => n.id === id)
|
||||
if (index !== -1) {
|
||||
const previous = notifications.value[index]
|
||||
notifications.value.splice(index, 1, { ...previous, ...notification })
|
||||
}
|
||||
}
|
||||
|
||||
function clear () {
|
||||
notifications.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
add,
|
||||
remove
|
||||
remove,
|
||||
update,
|
||||
clear
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ ${Object.entries(gray || colors.cool).map(([key, value]) => `--color-gray-${key}
|
||||
}
|
||||
|
||||
// SPA mode
|
||||
if (process.client && nuxtApp.isHydrating && !nuxtApp.payload.serverRendered) {
|
||||
if (import.meta.client && nuxtApp.isHydrating && !nuxtApp.payload.serverRendered) {
|
||||
const style = document.createElement('style')
|
||||
|
||||
style.innerHTML = root.value
|
||||
|
||||
13
src/runtime/plugins/slideovers.ts
Normal file
13
src/runtime/plugins/slideovers.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineNuxtPlugin } from '#imports'
|
||||
import { shallowRef } from 'vue'
|
||||
import { slidOverInjectionKey } from '../composables/useSlideover'
|
||||
import type { SlideoverState } from '../types/slideover'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const slideoverState = shallowRef<SlideoverState>({
|
||||
component: 'div',
|
||||
props: {}
|
||||
})
|
||||
|
||||
nuxtApp.vueApp.provide(slidOverInjectionKey, slideoverState)
|
||||
})
|
||||
1
src/runtime/types/index.d.ts
vendored
1
src/runtime/types/index.d.ts
vendored
@@ -17,6 +17,7 @@ export * from './kbd'
|
||||
export * from './link'
|
||||
export * from './meter'
|
||||
export * from './modal'
|
||||
export * from './slideover'
|
||||
export * from './notification'
|
||||
export * from './popper'
|
||||
export * from './progress'
|
||||
|
||||
2
src/runtime/types/link.d.ts
vendored
2
src/runtime/types/link.d.ts
vendored
@@ -7,6 +7,6 @@ export interface Link extends NuxtLinkProps {
|
||||
active?: boolean
|
||||
exact?: boolean
|
||||
exactQuery?: boolean
|
||||
exactMatch?: boolean
|
||||
exactHash?: boolean
|
||||
inactiveClass?: string
|
||||
}
|
||||
|
||||
16
src/runtime/types/slideover.d.ts
vendored
Normal file
16
src/runtime/types/slideover.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Component } from 'vue'
|
||||
|
||||
export interface Slideover {
|
||||
ui?: any
|
||||
side?: 'right' | 'left'
|
||||
transition?: boolean
|
||||
appear?: boolean
|
||||
overlay?: boolean
|
||||
preventClose?: boolean
|
||||
modelValue?: boolean
|
||||
}
|
||||
|
||||
export interface SlideoverState {
|
||||
component: Component | string
|
||||
props: Slideover
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export default {
|
||||
divide: 'divide-y divide-gray-300 dark:divide-gray-700',
|
||||
thead: 'relative',
|
||||
tbody: 'divide-y divide-gray-200 dark:divide-gray-800',
|
||||
caption: 'sr-only',
|
||||
tr: {
|
||||
base: '',
|
||||
selected: 'bg-gray-50 dark:bg-gray-800/50',
|
||||
@@ -50,6 +51,9 @@ export default {
|
||||
variant: 'ghost' as const,
|
||||
class: '-m-1.5'
|
||||
},
|
||||
checkbox: {
|
||||
color: 'primary' as const
|
||||
},
|
||||
progress: {
|
||||
color: 'primary' as const,
|
||||
animation: 'carousel' as const
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user