Compare commits

..

1 Commits

Author SHA1 Message Date
Romain Hamel
75a0ac84c9 fix(Form): extend Form<...> type with HTMLFormElement 2024-04-07 13:09:55 +02:00
178 changed files with 11874 additions and 13501 deletions

View File

@@ -15,7 +15,6 @@ module.exports = {
'space-before-blocks': ['error', 'always'],
'space-infix-ops': ['error', { int32Hint: false }],
'no-multi-spaces': ['error', { ignoreEOLComments: true }],
'no-trailing-spaces': ['error'],
// Typescript
'@typescript-eslint/type-annotation-spacing': 'error',

View File

@@ -1,6 +1,6 @@
name: "🐛 Bug report"
description: Report a bug to help us improve the module.
labels: ["triage", "bug"]
labels: ["bug"]
body:
- type: markdown
attributes:

View File

@@ -4,5 +4,5 @@ contact_links:
url: https://ui.nuxt.com
about: Check the documentation for guides and examples.
- name: 📚 Discord
url: https://go.nuxt.com/discord
about: Consider asking questions in the help channel.
url: https://discord.com/channels/473401852243869706/1153996761426300948
about: Consider asking questions in the `#ui` channel.

View File

@@ -1,6 +1,6 @@
name: "🚀 Feature request"
description: Suggest an idea or enhancement for the module.
labels: ["triage", "enhancement"]
labels: ["enhancement"]
body:
- type: markdown
attributes:

View File

@@ -19,26 +19,41 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest] # macos-latest, windows-latest
node: [20]
node: [18]
env:
NUXT_GITHUB_TOKEN: ${{ secrets.NUXT_GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install node
uses: actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Filter changes
uses: dorny/paths-filter@v3
- name: checkout
uses: actions/checkout@master
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |

View File

@@ -12,23 +12,39 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest] # macos-latest, windows-latest
node: [20]
node: [18]
env:
NUXT_GITHUB_TOKEN: ${{ secrets.NUXT_GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install node
uses: actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: checkout
uses: actions/checkout@master
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install

View File

@@ -1,23 +0,0 @@
name: stale
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
exempt-issue-labels: triage,v3
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity.'
stale-issue-label: stale
stale-pr-label: stale
days-before-stale: 30
days-before-close: -1

View File

@@ -1,166 +1,5 @@
# Changelog
## [2.18.5](https://github.com/nuxt/ui/compare/v2.18.4...v2.18.5) (2024-09-18)
### Features
* **Form:** add errors slot prop ([#2188](https://github.com/nuxt/ui/issues/2188)) ([67c6a74](https://github.com/nuxt/ui/commit/67c6a74ed15db1ee8a40e9c74ecfef0d3c3e374a))
### Bug Fixes
* **Button:** button link not showing disabled classes ([#2185](https://github.com/nuxt/ui/issues/2185)) ([e8ea84a](https://github.com/nuxt/ui/commit/e8ea84a5736759d953664f8f397a2339c212b294))
* **Carousel:** remove trailing space in next button icon ([#2088](https://github.com/nuxt/ui/issues/2088)) ([1282a5f](https://github.com/nuxt/ui/commit/1282a5f6c001aa05597d458800bafcf6b6419634))
* **FormGroup:** remove id when used with `RadioGroup` ([#2152](https://github.com/nuxt/ui/issues/2152)) ([7aec42c](https://github.com/nuxt/ui/commit/7aec42ca15aaa0ccc63c520b484cba203fd3232b))
* **Input:** avoid binding value when type is `file` ([#2047](https://github.com/nuxt/ui/issues/2047)) ([82313e8](https://github.com/nuxt/ui/commit/82313e862cbf21ae631156af4cd057f1383db634))
* **module:** allow CSS variables in tailwind colors ([#2014](https://github.com/nuxt/ui/issues/2014)) ([7f50c70](https://github.com/nuxt/ui/commit/7f50c7031fecb5ab26a6d0f58b576b2fd0860487))
* **module:** augment `@nuxt/schema` rather than `nuxt/schema` ([#2171](https://github.com/nuxt/ui/issues/2171)) ([ead904f](https://github.com/nuxt/ui/commit/ead904fd2f2bbb29fd60ccde063bf02daa2cbdbb))
* **module:** consider user tailwind `configPath` for module as string ([#2074](https://github.com/nuxt/ui/issues/2074)) ([e4ba4f7](https://github.com/nuxt/ui/commit/e4ba4f7c729f99dde51891636605793864812d30))
* **Pagination:** use links on prev and next button ([#2179](https://github.com/nuxt/ui/issues/2179)) ([c850f85](https://github.com/nuxt/ui/commit/c850f85aaa40c7abbe8cc4dc1bd4705bf7677390))
* **README:** update license link ([#2154](https://github.com/nuxt/ui/issues/2154)) ([8d79eea](https://github.com/nuxt/ui/commit/8d79eea19b3276b1f1e069d33b98b311e9b91cfd))
* **Slideover:** bind `rounded` class to panel ([#2187](https://github.com/nuxt/ui/issues/2187)) ([bf32baa](https://github.com/nuxt/ui/commit/bf32baaab01dc4150622f67b3b4a8d02d21b922c))
* **Slideover:** bind `shadow` class to panel ([#2201](https://github.com/nuxt/ui/issues/2201)) ([d22526c](https://github.com/nuxt/ui/commit/d22526c0c10735a92e63b7d086e7b8534a08d768))
* **Table:** checkbox can emit the `[@select](https://github.com/select)` event ([#2072](https://github.com/nuxt/ui/issues/2072)) ([b1f691f](https://github.com/nuxt/ui/commit/b1f691f28ca8c94f6b658dcb61eeff06951bd1d0))
* **Table:** select all rows reactivity issue ([#2200](https://github.com/nuxt/ui/issues/2200)) ([68124de](https://github.com/nuxt/ui/commit/68124de5106e55cb2987a6ba4ec1120d79b51788))
* **Tabs:** recalculate marker if items change ([#2101](https://github.com/nuxt/ui/issues/2101)) ([82c4926](https://github.com/nuxt/ui/commit/82c4926c090ce7fac48022a93b1b05b877bb48dd))
* **Textarea:** resolve row count calculation errors caused by scrollbar ([#2040](https://github.com/nuxt/ui/issues/2040)) ([8210936](https://github.com/nuxt/ui/commit/8210936f22fcf6b7eb5b9711e2c29be38956b8d6))
## [2.18.4](https://github.com/nuxt/ui/compare/v2.18.3...v2.18.4) (2024-08-05)
### Bug Fixes
* **Form:** submit event data ([#2012](https://github.com/nuxt/ui/issues/2012)) ([4d61936](https://github.com/nuxt/ui/commit/4d61936e7e90b664846a8f265825612c509511d7))
* **module:** handle nested colors from ui config ([#2008](https://github.com/nuxt/ui/issues/2008)) ([1cc7e2a](https://github.com/nuxt/ui/commit/1cc7e2a306e0f3f666b9a588f6ed02e7eabc0272))
* **module:** reduce css bundle size by fixing safelist regex ([#2005](https://github.com/nuxt/ui/issues/2005)) ([8ac9ca4](https://github.com/nuxt/ui/commit/8ac9ca49789a9a7281f7a40926e7e9a8068cc395))
* **module:** suffix types imports with `/index` ([7e37668](https://github.com/nuxt/ui/commit/7e37668940d06c5aa20b60d9bfd600d50a171014)), closes [#2018](https://github.com/nuxt/ui/issues/2018)
* **Tabs:** use `nextTick` before marker calc ([#2020](https://github.com/nuxt/ui/issues/2020)) ([9c04969](https://github.com/nuxt/ui/commit/9c049690227af8aba61a1f7c002b00c5dfeb63ff))
* **useFormGroup:** app config default input size ([#2011](https://github.com/nuxt/ui/issues/2011)) ([3485092](https://github.com/nuxt/ui/commit/3485092edb55f9ef2ca038a8c137431866d6c28a))
## [2.18.3](https://github.com/nuxt/ui/compare/v2.18.2...v2.18.3) (2024-07-30)
### Bug Fixes
* **Link:** define `rel` as any ([69f605f](https://github.com/nuxt/ui/commit/69f605fa724454e4be9e4cee9666a5d57f43a129))
* **types:** only use `.ts` for index ([93ddf1d](https://github.com/nuxt/ui/commit/93ddf1d60b0ea5f99f564f3d3969c397ad91cc72))
## [2.18.2](https://github.com/nuxt/ui/compare/v2.18.1...v2.18.2) (2024-07-25)
### Bug Fixes
* **Tabs:** add missing `UIcon` import ([4fd1be2](https://github.com/nuxt/ui/commit/4fd1be28922bf39584005c14982e5cd9a7d0c624))
## [2.18.1](https://github.com/nuxt/ui/compare/v2.18.0...v2.18.1) (2024-07-25)
### Bug Fixes
* **components:** use relative imports ([ea721a3](https://github.com/nuxt/ui/commit/ea721a3705cfbcef3075f8c9c1f4acf359974597))
## [2.18.0](https://github.com/nuxt/ui/compare/v2.17.0...v2.18.0) (2024-07-25)
### ⚠ BREAKING CHANGES
* **Icon:** migrate from `@egoist/tailwindcss-icons` to new `@nuxt/icon` (#1789)
### Features
* **Checkbox/Radio/RadioGroup:** add `help` slot ([c3122f7](https://github.com/nuxt/ui/commit/c3122f776daa6d68f201f22c37e0084aac37ed06)), closes [#1957](https://github.com/nuxt/ui/issues/1957)
* **CommandPalette:** handle `static` groups ([#1458](https://github.com/nuxt/ui/issues/1458)) ([b264ad2](https://github.com/nuxt/ui/commit/b264ad2ebdc8d4ee4aab5c994df968025207021f))
* **Icon:** migrate from `@egoist/tailwindcss-icons` to new `@nuxt/icon` ([#1789](https://github.com/nuxt/ui/issues/1789)) ([c904604](https://github.com/nuxt/ui/commit/c904604c23987c2535e0e91e9c4fec50477f6b34))
* **module:** improve app config types autocomplete ([#1870](https://github.com/nuxt/ui/issues/1870)) ([3f8ea5d](https://github.com/nuxt/ui/commit/3f8ea5dbded7b6836495103739688905ff26fe22))
* **RadioGroup:** add `selected` to label slot props ([#1587](https://github.com/nuxt/ui/issues/1587)) ([d18477d](https://github.com/nuxt/ui/commit/d18477def58171d51bdb7d00e31e2807b2e7015b))
* **SelectMenu:** add selected to `label` / `leading` / `trailing` slots props ([#1349](https://github.com/nuxt/ui/issues/1349)) ([6b216ca](https://github.com/nuxt/ui/commit/6b216cab1ba3bb69cb317254dfd562ab020c5e92))
* **SelectMenu:** handle function in `showCreateOptionWhen` prop ([#1853](https://github.com/nuxt/ui/issues/1853)) ([7e974b5](https://github.com/nuxt/ui/commit/7e974b55d72b8ac0ab42ef722a2d1904c3e4e091))
* **Skeleton:** add `as` prop ([#1955](https://github.com/nuxt/ui/issues/1955)) ([bce94db](https://github.com/nuxt/ui/commit/bce94db9fdb2c29a4f2e5981e5dce49a44a4ac8a))
* **Table:** expand row ([#1036](https://github.com/nuxt/ui/issues/1036)) ([7155318](https://github.com/nuxt/ui/commit/71553180294c53024c28de9bbebf4ea69f616da7))
* **Table:** handle `rowClass` property in `columns` ([#1632](https://github.com/nuxt/ui/issues/1632)) ([748e491](https://github.com/nuxt/ui/commit/748e49175da37b85bd18d62a8455875990866d5b))
* **Tabs:** handle `icon` in items ([#1798](https://github.com/nuxt/ui/issues/1798)) ([e8eb394](https://github.com/nuxt/ui/commit/e8eb3941ad4c1c306ccbe9e11d979d5f6c808330))
### Bug Fixes
* **Accordion:** truncate buttons ([5db18c0](https://github.com/nuxt/ui/commit/5db18c00565f9d2bb9f2768c2de2ab291a55bcae)), closes [#1909](https://github.com/nuxt/ui/issues/1909)
* **Alert/Notification:** missing margin on description ([2c55fb6](https://github.com/nuxt/ui/commit/2c55fb63365ee7cc1e993ebd5aa5f83ddadcd26a)), closes [#1959](https://github.com/nuxt/ui/issues/1959)
* **Breadcrumb:** use `rotate` on rtl icon ([53003fc](https://github.com/nuxt/ui/commit/53003fcd07d67d13ada0759ff6c5cd3635fba0e3))
* **ButtonGroup/FormGroup:** pass default sizes to children ([#1875](https://github.com/nuxt/ui/issues/1875)) ([6b6b03d](https://github.com/nuxt/ui/commit/6b6b03d59f5ab3096b731c59d18a1085d25b5e8e))
* **Carousel:** remove `mix-blend-overlay` on indicators ([#1714](https://github.com/nuxt/ui/issues/1714)) ([f74f1df](https://github.com/nuxt/ui/commit/f74f1df6ca5f93e11e542245b611c1aa7c4b8308))
* **FormGroup:** don't check for `error` slot so `help` slot can render ([#1888](https://github.com/nuxt/ui/issues/1888)) ([99c52e5](https://github.com/nuxt/ui/commit/99c52e50082d5e99440894c7a077a17510f0de50))
* **InputMenu/SelectMenu:** invalid `label` with `value-attribute` and async search ([4d5f250](https://github.com/nuxt/ui/commit/4d5f2509022e4fb74fc268d5479f7cc8f0415040)), closes [#1780](https://github.com/nuxt/ui/issues/1780)
* **InputMenu/SelectMenu:** prevent double filter with async search ([e2881d3](https://github.com/nuxt/ui/commit/e2881d3801c54c49d66d41d4f0ba312a7b3ebce7)), closes [#1966](https://github.com/nuxt/ui/issues/1966)
* **Link:** allow `ariaLabel` to be picked ([720c44d](https://github.com/nuxt/ui/commit/720c44dd5ee90bb3b30aef32f01ff6eae1397aa4)), closes [#1934](https://github.com/nuxt/ui/issues/1934)
* **Progress:** pass down attrs to `<progress>` to improve accessibility ([#1881](https://github.com/nuxt/ui/issues/1881)) ([abd13f1](https://github.com/nuxt/ui/commit/abd13f1f8fd4c8b10069174534c5fdec6c83576e))
* **RadioGroup:** allow boolean in `modelValue` prop ([#1913](https://github.com/nuxt/ui/issues/1913)) ([8eca5a0](https://github.com/nuxt/ui/commit/8eca5a0d627e22f42350a060f09c4e44b6de422f))
## [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)

View File

@@ -27,7 +27,24 @@ Read more on [ui.nuxt.com](https://ui.nuxt.com)
## Installation
```bash
npx nuxi@latest module add ui
# 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'
]
})
```
If you want latest updates, please use `@nuxt/ui-edge` in your `package.json`:
@@ -77,7 +94,7 @@ Licensed under the [MIT license](https://github.com/nuxt/ui/blob/dev/LICENSE.md)
[npm-downloads-href]: https://npmjs.com/package/@nuxt/ui
[license-src]: https://img.shields.io/github/license/nuxt/ui.svg?style=flat&colorA=18181B&colorB=28CF8D
[license-href]: https://github.com/nuxt/ui/blob/main/LICENSE.md
[license-href]: https://github.com/nuxt/ui/blob/main/LICENSE
[nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
[nuxt-href]: https://nuxt.com

View File

@@ -30,7 +30,7 @@
<script setup lang="ts">
import { withoutTrailingSlash } from 'ufo'
import { debounce } from 'perfect-debounce'
import type { ParsedContent } from '@nuxt/content'
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
const searchRef = ref()

View File

@@ -25,8 +25,6 @@
</template>
<script setup lang="ts">
const { $ui } = useNuxtApp()
const links = [{
icon: 'i-simple-icons-figma',
label: 'Figma Kit',
@@ -45,8 +43,5 @@ const links = [{
label: 'Releases',
icon: 'i-heroicons-rocket-launch',
to: '/releases'
}, {
label: 'Terms',
to: '/pro/terms'
}]
</script>

View File

@@ -1,34 +0,0 @@
<template>
<svg viewBox="0 0 1440 181" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1_1086_7239" fill="white">
<path d="M0 0H1440V181H0V0Z" />
</mask>
<path d="M0 0H1440V181H0V0Z" fill="url(#paint0_linear_1086_7239)" fill-opacity="0.22" />
<path d="M0 2H1440V-2H0V2Z" fill="url(#paint1_linear_1086_7239)" mask="url(#path-1-inside-1_1086_7239)" />
<defs>
<linearGradient
id="paint0_linear_1086_7239"
x1="720"
y1="0"
x2="720"
y2="181"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="rgb(var(--color-primary-DEFAULT))" />
<stop offset="1" stop-color="rgb(var(--color-primary-DEFAULT))" stop-opacity="0" />
</linearGradient>
<linearGradient
id="paint1_linear_1086_7239"
x1="0"
y1="90.5"
x2="1440"
y2="90.5"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="rgb(var(--color-primary-DEFAULT))" stop-opacity="0" />
<stop offset="0.395" stop-color="rgb(var(--color-primary-DEFAULT))" />
<stop offset="1" stop-color="rgb(var(--color-primary-DEFAULT))" stop-opacity="0" />
</linearGradient>
</defs>
</svg>
</template>

View File

@@ -46,7 +46,7 @@
</template>
<script setup lang="ts">
import type { NavItem } from '@nuxt/content'
import type { NavItem } from '@nuxt/content/dist/runtime/types'
import type { HeaderLink } from '#ui-pro/types'
defineProps<{
@@ -54,7 +54,6 @@ defineProps<{
}>()
const route = useRoute()
const { $ui } = useNuxtApp()
const { metaSymbol } = useShortcuts()
const nav = inject<Ref<NavItem[]>>('navigation')

View File

@@ -1,4 +1,8 @@
<script lang="ts" setup>
defineOptions({
inheritAttrs: false
})
defineProps({
title: {
type: String,

View File

@@ -156,11 +156,9 @@ const generateOptions = (key: string, schema: { kind: string, schema: [], type:
options = [...appConfig.ui.colors]
}
const schemaOptions = Object.values(schema?.schema || {})
if (key.toLowerCase() === 'size' && schemaOptions?.length > 0) {
if (key.toLowerCase() === 'size' && schema?.schema?.length > 0) {
const baseSizeOrder = { 'xs': 1, 'sm': 2, 'md': 3, 'lg': 4, 'xl': 5 }
schemaOptions.sort((a: string, b: string) => {
schema.schema.sort((a: string, b: string) => {
const aBase = a.match(/[a-zA-Z]+/)[0].toLowerCase()
const bBase = b.match(/[a-zA-Z]+/)[0].toLowerCase()
@@ -175,8 +173,8 @@ const generateOptions = (key: string, schema: { kind: string, schema: [], type:
})
}
if (schemaOptions?.length > 0 && schema?.kind === 'enum' && !hasIgnoredTypes && optionItem?.restriction !== 'only') {
options = schemaOptions.filter(option => typeof option === 'string').map((option: string) => option.replaceAll('"', ''))
if (schema?.schema?.length > 0 && schema?.kind === 'enum' && !hasIgnoredTypes && optionItem?.restriction !== 'only') {
options = schema.schema.filter(option => typeof option === 'string').map((option: string) => option.replaceAll('"', ''))
}
if (optionItem?.restriction === 'only') {

View File

@@ -1,7 +1,6 @@
<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]"
>
@@ -38,10 +37,6 @@ const props = defineProps({
type: Object,
default: () => ({})
},
hiddenPreview: {
type: Boolean,
default: false
},
hiddenCode: {
type: Boolean,
default: false
@@ -84,7 +79,6 @@ 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: {

View File

@@ -5,15 +5,15 @@
{{ prop.description }}
</p>
<Collapsible v-if="prop.schema?.kind === 'array' && Object.values(prop.schema?.schema)?.filter((schema: any) => schema.kind === 'object').length">
<FieldGroup v-for="schema in (Object.values(prop.schema.schema) as any[])" :key="schema.name">
<Collapsible v-if="prop.schema?.kind === 'array' && prop.schema?.schema?.filter(schema => schema.kind === 'object').length">
<FieldGroup v-for="schema in prop.schema.schema" :key="schema.name">
<ComponentPropsField v-for="subProp in Object.values(schema.schema)" :key="(subProp as any).name" :prop="subProp" />
</FieldGroup>
</Collapsible>
<Collapsible v-else-if="(prop.schema?.kind === 'enum' || prop.schema?.kind === 'array') && Object.values(prop.schema?.schema)?.filter((schema: any) => schema.kind === 'array' && typeof schema.schema === 'object')?.length > 1">
<FieldGroup v-for="schema in (Object.values(prop.schema.schema) as any[])" :key="schema.name">
<Collapsible v-else-if="prop.schema?.kind === 'array' && prop.schema?.schema?.filter(schema => schema.kind === 'array').length">
<FieldGroup v-for="schema in prop.schema.schema" :key="schema.name">
<template v-for="subSchema in schema.schema" :key="subSchema.name">
<ComponentPropsField v-for="subProp in subSchema.schema" :key="(subProp as any).name" :prop="subProp" />
<ComponentPropsField v-for="subProp in Object.values(subSchema.schema)" :key="(subProp as any).name" :prop="subProp" />
</template>
</FieldGroup>
</Collapsible>
@@ -23,7 +23,7 @@
</FieldGroup>
</Collapsible>
<div v-else-if="prop.schema?.kind === 'enum' && prop.schema.type !== 'boolean' && startsWithCapital(prop.schema.type) && !prop.schema.type.startsWith(prop.schema.schema[0])" class="flex items-center flex-wrap gap-1 -my-1">
<code v-for="value in Object.values(prop.schema.schema).filter(value => typeof value === 'string')" :key="value" class="whitespace-pre-wrap break-words leading-6">{{ value }}</code>
<code v-for="value in prop.schema.schema.filter(value => typeof value === 'string')" :key="value" class="whitespace-pre-wrap break-words leading-6">{{ value }}</code>
</div>
</Field>
</template>

View File

@@ -61,7 +61,9 @@ const items = [{
</div>
<div class="flex flex-col items-center">
<code>$ npx nuxi@latest module add ui</code>
<code>$ npm i @nuxt/ui</code>
<code>$ yarn add @nuxt/ui</code>
<code>$ pnpm add @nuxt/ui</code>
</div>
</template>
</UAccordion>

View File

@@ -1,15 +1,15 @@
<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'
<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"
<UAvatar
v-bind="avatar"
chip-color="primary"
chip-text=""
chip-position="top-right"
@@ -17,3 +17,4 @@
</template>
</UAlert>
</template>

View File

@@ -7,3 +7,4 @@
</template>
</UAlert>
</template>

View File

@@ -12,7 +12,7 @@ const links = [{
<template>
<UBreadcrumb :links="links">
<template #default="{ link, isActive, index }">
<UBadge :color="isActive ? 'primary' : 'gray'" class="rounded-full truncate">
<UBadge :color="isActive ? 'primary' : 'gray'" class="rounded-full">
{{ index + 1 }}. {{ link.label }}
</UBadge>
</template>

View File

@@ -7,8 +7,7 @@ const groups = [{
return []
}
// @ts-ignore
const users: any[] = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
const users = await $fetch<any[]>('https://jsonplaceholder.typicode.com/users', { params: { q } })
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email }))
}

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { NavItem, ParsedContent } from '@nuxt/content'
import type { NavItem, ParsedContent } from '@nuxt/content/dist/runtime/types'
import type { Button } from '#ui/types'
const commandPaletteRef = ref()

View File

@@ -1,6 +1,4 @@
<script setup lang="ts">
import { useMouse, useWindowScroll } from '@vueuse/core'
const { x, y } = useMouse()
const { y: windowY } = useWindowScroll()

View File

@@ -1,6 +1,4 @@
<script setup lang="ts">
import { useMouse, useWindowScroll } from '@vueuse/core'
const { x, y } = useMouse()
const { y: windowY } = useWindowScroll()

View File

@@ -1,6 +1,4 @@
<script setup lang="ts">
import { useMouse, useWindowScroll } from '@vueuse/core'
const { x, y } = useMouse()
const { y: windowY } = useWindowScroll()

View File

@@ -1,6 +1,4 @@
<script setup lang="ts">
import { useMouse, useWindowScroll } from '@vueuse/core'
const { x, y } = useMouse()
const { y: windowY } = useWindowScroll()

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import * as v from 'valibot'
import { string, objectAsync, email, minLength, type Input } from 'valibot'
import type { FormSubmitEvent } from '#ui/types'
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'))
const schema = objectAsync({
email: string([email('Invalid email')]),
password: string([minLength(8, 'Must be at least 8 characters')])
})
type Schema = v.InferOutput<typeof schema>
type Schema = Input<typeof schema>
const state = reactive({
email: '',
@@ -21,7 +21,7 @@ async function onSubmit (event: FormSubmitEvent<Schema>) {
</script>
<template>
<UForm :schema="v.safeParser(schema)" :state="state" class="space-y-4" @submit="onSubmit">
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>

View File

@@ -5,7 +5,7 @@ const selected = ref()
async function search (q: string) {
loading.value = true
const users: any[] = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
const users = await $fetch<any[]>('https://jsonplaceholder.typicode.com/users', { params: { q } })
loading.value = false

View File

@@ -5,24 +5,13 @@ defineProps({
default: 0
}
})
const emit = defineEmits(['success'])
function onSuccess () {
emit('success')
}
</script>
<template>
<UModal>
<UCard>
<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>
<p>This modal was opened programmatically !</p>
<p>Count: {{ count }}</p>
</UCard>
</UModal>
</template>

View File

@@ -1,20 +1,13 @@
<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,
onSuccess () {
toast.add({
title: 'Success !',
id: 'modal-success'
})
}
count: count.value
})
}
</script>

View File

@@ -1,17 +0,0 @@
<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>

View File

@@ -1,60 +0,0 @@
<script setup lang="ts">
const options = ref([
{ id: 1, name: 'bug' },
{ id: 2, name: 'documentation' },
{ id: 3, name: 'duplicate' },
{ id: 4, name: 'enhancement' },
{ id: 5, name: 'good first issue' },
{ id: 6, name: 'help wanted' },
{ id: 7, name: 'invalid' },
{ id: 8, name: 'question' },
{ id: 9, name: 'wontfix' }
])
const selected = ref([])
const labels = computed({
get: () => selected.value,
set: async (labels) => {
const promises = labels.map(async (label) => {
if (label.id) {
return label
}
// In a real app, you would make an API call to create the label
const response = {
id: options.value.length + 1,
name: label.name
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
const showCreateOption = (query, results) => {
const lowercaseQuery = String.prototype.toLowerCase.apply(query || '')
return lowercaseQuery.length >= 3 && !results.find(option => {
return String.prototype.toLowerCase.apply(option['name'] || '') === lowercaseQuery
})
}
</script>
<template>
<USelectMenu
v-model="labels"
by="id"
name="labels"
:options="options"
option-attribute="name"
multiple
searchable
creatable
:show-create-option-when="showCreateOption"
placeholder="Select labels"
/>
</template>

View File

@@ -5,7 +5,7 @@ const selected = ref([])
async function search (q: string) {
loading.value = true
const users: any[] = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
const users = await $fetch<any[]>('https://jsonplaceholder.typicode.com/users', { params: { q } })
loading.value = false

View File

@@ -8,16 +8,6 @@ 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>

View File

@@ -7,22 +7,8 @@ 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>

View File

@@ -23,7 +23,7 @@ const emits = defineEmits<{
<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>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import { SlideoverExampleComponent } from '#components'
const slideover = useSlideover()
const count = ref(0)
function openSlideover () {
@@ -14,4 +13,4 @@ function openSlideover () {
<template>
<UButton label="Reveal slideover" @click="openSlideover" />
</template>
</template>

View File

@@ -8,17 +8,6 @@ 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>

View File

@@ -8,17 +8,6 @@ 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>

View File

@@ -85,7 +85,7 @@ const pageFrom = computed(() => (page.value - 1) * pageCount.value + 1)
const pageTo = computed(() => Math.min(page.value * pageCount.value, pageTotal.value))
// Data
const { data: todos, status } = await useLazyAsyncData<{
const { data: todos, pending } = await useLazyAsyncData<{
id: number
title: string
completed: string
@@ -181,12 +181,12 @@ const { data: todos, status } = await useLazyAsyncData<{
v-model:sort="sort"
:rows="todos"
:columns="columnsTable"
:loading="status === 'pending'"
:loading="pending"
sort-asc-icon="i-heroicons-arrow-up"
sort-desc-icon="i-heroicons-arrow-down"
sort-mode="manual"
class="w-full"
:ui="{ td: { base: 'max-w-[0] truncate' }, default: { checkbox: { color: 'gray' } } }"
:ui="{ td: { base: 'max-w-[0] truncate' } }"
@select="select"
>
<template #completed-data="{ row }">

View File

@@ -1,63 +0,0 @@
<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>

View File

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

View File

@@ -1,16 +1,13 @@
<script setup lang="ts">
const items = [{
label: 'Tab1',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
icon: 'i-heroicons-arrow-down-tray',
disabled: true,
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]
</script>

View File

@@ -1,15 +1,12 @@
<script setup lang="ts">
const items = [{
label: 'Tab1',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
icon: 'i-heroicons-arrow-down-tray',
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]

View File

@@ -17,7 +17,13 @@ const items = [{
<template>
<UTabs :items="items" class="w-full">
<template #default="{ item, index, selected }">
<span class="truncate" :class="[selected && 'text-primary-500 dark:text-primary-400']">{{ index + 1 }}. {{ item.label }}</span>
<div class="flex items-center gap-2 relative truncate">
<UIcon :name="item.icon" class="w-4 h-4 flex-shrink-0" />
<span class="truncate">{{ index + 1 }}. {{ item.label }}</span>
<span v-if="selected" class="absolute -right-4 w-2 h-2 rounded-full bg-primary-500 dark:bg-primary-400" />
</div>
</template>
</UTabs>
</template>

View File

@@ -1,23 +0,0 @@
<script setup lang="ts">
const items = [{
label: 'Getting Started',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Installation',
icon: 'i-heroicons-arrow-down-tray',
content: 'And, this is the content for Tab2'
}, {
label: 'Theming',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]
</script>
<template>
<UTabs :items="items" class="w-full">
<template #icon="{ item, selected }">
<UIcon :name="item.icon" class="w-4 h-4 flex-shrink-0 mr-2" :class="[selected && 'text-primary-500 dark:text-primary-400']" />
</template>
</UTabs>
</template>

View File

@@ -1,15 +1,12 @@
<script setup lang="ts">
const items = [{
label: 'Tab1',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
icon: 'i-heroicons-arrow-down-tray',
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]
</script>

View File

@@ -1,15 +1,12 @@
<script setup lang="ts">
const items = [{
label: 'Tab1',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
icon: 'i-heroicons-arrow-down-tray',
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]

View File

@@ -1,15 +1,12 @@
<script setup lang="ts">
const items = [{
label: 'Tab1',
icon: 'i-heroicons-information-circle',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
icon: 'i-heroicons-arrow-down-tray',
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
icon: 'i-heroicons-eye-dropper',
content: 'Finally, this is the content for Tab3'
}]
</script>

View File

@@ -1,135 +0,0 @@
<script setup lang="ts">
import { breakpointsTailwind } from '@vueuse/core'
const props = withDefaults(defineProps<{
contributors: {
username: string
}[]
level?: number
max?: number
}>(), {
level: 0,
max: 4
})
const contributors = computed(() => props.contributors.slice(0, 5))
const el = ref(null)
const { width } = useElementSize(el)
const breakpoints = useBreakpoints(breakpointsTailwind)
const smallerThanSm = breakpoints.smaller('sm')
</script>
<template>
<div
class="isolate rounded-full relative circle w-full aspect-[1/1] p-8 sm:p-12 md:p-14 lg:p-10 xl:p-16 before:absolute before:inset-px before:bg-white dark:before:bg-gradient-to-b dark:before:from-gray-950 dark:before:to-gray-900 before:rounded-full z-[--level]"
:style="{
'--duration': `${((level + 1) * 5)}s`,
'--level': level + 1
}"
>
<HomeContributors
v-if="(level + 1) < max"
:max="max"
:level="level + 1"
:contributors="props.contributors.slice(5)"
/>
<div
ref="el"
class="avatars absolute inset-0 grid place-items-center"
:style="{
'--total': contributors.length,
'--offset': `${width / 2}px`
}"
>
<UTooltip
v-for="(contributor, index) in contributors"
:key="contributor.username"
:text="contributor.username"
:style="{
'--index': index + 1
}"
class="avatar flex absolute"
>
<UAvatar
:alt="contributor.username"
:src="`https://ipx.nuxt.com/s_40x40/gh_avatar/${contributor.username}`"
:srcset="`https://ipx.nuxt.com/s_80x80/gh_avatar/${contributor.username} 2x`"
class="ring-2 ring-gray-200 dark:ring-gray-700 lg:hover:ring-gray-900 dark:lg:hover:ring-white transition"
width="40"
height="40"
:size="smallerThanSm ? 'xs' : 'sm'"
loading="lazy"
>
<NuxtLink :to="`https://github.com/${contributor.username}`" :aria-label="contributor.username" target="_blank" class="focus:outline-none" tabindex="-1">
<span class="absolute inset-0" aria-hidden="true" />
</NuxtLink>
</UAvatar>
</UTooltip>
</div>
</div>
</template>
<style scoped>
.circle:after {
--start: 0deg;
--end: 360deg;
--border-color: rgb(var(--color-gray-200));
--highlight-color: rgb(var(--color-gray-700));
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: -1px;
opacity: 1;
border-radius: 9999px;
z-index: -1;
background: var(--border-color);
@supports (background: paint(houdini)) {
background: linear-gradient(var(--angle), var(--border-color), var(--border-color), var(--border-color), var(--border-color), var(--highlight-color));
animation: var(--duration) rotate linear infinite;
}
}
.dark .circle:after {
--border-color: rgb(var(--color-gray-700));
--highlight-color: white;
}
.avatars {
--start: calc(var(--level) * 36deg);
--end: calc(360deg + (var(--level) * 36deg));
transform: rotate(var(--angle));
animation: calc(var(--duration) + 60s) rotate linear infinite;
}
.avatar {
--deg: calc(var(--index) * (360deg / var(--total)));
--transformX: calc(cos(var(--deg)) * var(--offset));
--transformY: calc(sin(var(--deg)) * var(--offset));
transform: translate(var(--transformX), var(--transformY)) rotate(calc(360deg - var(--angle)));
}
@keyframes rotate {
from {
--angle: var(--start);
}
to {
--angle: var(--end);
}
}
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: true;
}
</style>

View File

@@ -31,7 +31,7 @@ onMounted(() => {
</script>
<template>
<ULandingGrid ref="section" class="lg:grid-cols-10 lg:gap-8 overflow-hidden p-px">
<ULandingGrid ref="section" class="lg:grid-cols-10 lg:gap-8">
<div :ref="(el) => (refs[1] = el)" class="col-span-8 flex items-center animate-top">
<RangeExample />
</div>

View File

@@ -5,15 +5,29 @@ description: 'Learn how to install and configure Nuxt UI in your application.'
## Setup
### Add to a Nuxt project
1. Install `@nuxt/ui` dependency to your project:
1. Add `@nuxt/ui` module to your project:
::code-group
```bash
npx nuxi@latest module add ui
```bash [pnpm]
pnpm add @nuxt/ui
```
2. Add it to the `modules` section in your `nuxt.config.ts`:
```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`:
```ts [nuxt.config.ts]
export default defineNuxtConfig({
@@ -23,33 +37,14 @@ 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 [@nuxt/icon](https://github.com/nuxt/icon), [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/) and [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) modules for you.
Nuxt UI will automatically install the [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/), [@nuxtjs/color-mode](https://color-mode.nuxtjs.org/) and [nuxt-icon](https://github.com/nuxt-modules/icon) modules for you.
::callout{icon="i-heroicons-exclamation-triangle"}
You should remove them from your `modules` and `dependencies` if you've previously installed them.
::
### `@nuxt/icon`
This module is installed to provide an easy way to use icons in your application. You can use the `UIcon` component to display any icon from Iconify.
::callout{icon="i-heroicons-light-bulb"}
You can read more about this in the [Theming](/getting-started/theming#icons) section.
::
### `@nuxtjs/tailwindcss`
This module is pre-configured and will automatically load the following plugins:
@@ -92,6 +87,14 @@ This module is installed to provide dark mode support out of the box thanks to t
You can read more about this in the [Theming](/getting-started/theming#dark-mode) section.
::
### `nuxt-icon`
This module is installed when using the `dynamic` prop on the `Icon` component or globally through the `ui.icons.dynamic` option in your `app.config.ts`.
::callout{icon="i-heroicons-light-bulb"}
You can read more about this in the [Theming](/getting-started/theming#dynamic-icons) section and on the [Icon](/components/icon) component page.
::
## TypeScript
This module is written in TypeScript and provides typings for all the components and composables. You can look at the [source code](https://github.com/nuxt/ui/tree/dev/src/runtime/types) to see all the available types.
@@ -229,6 +232,7 @@ You can also add the following to your `.vscode/settings.json` to enable Intelli
|-----------------------|-----------------|-------------------------------------------------------------------------------------------------------------|
| `prefix` | `u` | Define the prefix of the imported components. |
| `global` | `false` | Expose components globally. |
| `icons` | `['heroicons']` | Icon collections to load. |
| `safelistColors` | `['primary']` | Force safelisting of colors to need be purged. |
| `disableGlobalStyles` | `false` | Disable [global CSS styles](https://github.com/nuxt/ui/blob/dev/src/runtime/ui.css) injected by the module. |
@@ -238,7 +242,8 @@ Configure options in your `nuxt.config.ts` as such:
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
ui: {
global: true
global: true,
icons: ['mdi', 'simple-icons']
}
})
```

View File

@@ -23,7 +23,7 @@ export default defineAppConfig({
Try to change the `primary` and `gray` colors by clicking on the :u-icon{name="i-heroicons-swatch-20-solid" class="w-4 h-4 align-middle text-primary-500 dark:text-primary-400"} button in the header.
::
As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors or groups, such as `brand.primary`. By default, the `primary` color is `green` and the `gray` color is `cool`.
As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors. By default, the `primary` color is `green` and the `gray` color is `cool`.
When [using custom colors](https://tailwindcss.com/docs/customizing-colors#using-custom-colors) or [adding additional colors](https://tailwindcss.com/docs/customizing-colors#adding-additional-colors) through the `extend` key in your `tailwind.config.ts`, you'll need to make sure to define all the shades from `50` to `950` as most of them are used in the components config defined in [`ui.config/`](https://github.com/nuxt/ui/tree/dev/src/runtime/ui.config) directory. You can [generate your colors](https://tailwindcss.com/docs/customizing-colors#generating-colors) using tools such as https://uicolors.app/ for example.
@@ -106,7 +106,7 @@ This can also happen when you bind a dynamic color to a component: `<UBadge :col
### `app.config.ts`
You can override component config in your own `app.config.ts`:
You can find the config of each component in the [`ui.config/`](https://github.com/nuxt/ui/tree/dev/src/runtime/ui.config) directory. You can override those classes in your own `app.config.ts`.
```ts [app.config.ts]
export default defineAppConfig({
@@ -118,8 +118,6 @@ export default defineAppConfig({
})
```
The available options for each component should auto-complete, and you can review the defaults for each component using your IDE's function such as `Cmd`+`Click` (these files can be found in [`src/runtime/ui.config/`](https://github.com/nuxt/ui/tree/dev/src/runtime/ui.config)).
Thanks to [tailwind-merge](https://github.com/dcastil/tailwind-merge), the `app.config.ts` is smartly merged with the default config. This means you don't have to rewrite everything.
You can change this behavior by setting `strategy` to `override` in your `app.config.ts`:
@@ -260,7 +258,6 @@ const isDark = computed({
}
})
</script>
<template>
<ClientOnly>
<UButton
@@ -280,11 +277,9 @@ const isDark = computed({
## Icons
Thanks to [`@nuxt/icon`](https://github.com/nuxt/icon), add 200,000+ ready to use icons to your Nuxt application based on [Iconify](https://iconify.design).
You can use any icon (100,000+) from [Iconify](https://iconify.design/).
You can use any name from the https://icones.js.org collection such as the `i-` prefix (for example, `i-heroicons-cog`) with:
- any `icon` prop available across the components:
Some components have an `icon` prop that allows you to add an icon to the component.
```vue
<template>
@@ -292,17 +287,31 @@ You can use any name from the https://icones.js.org collection such as the `i-`
</template>
```
- the `UIcon` component to use icons anywhere:
You can also use the [Icon](/components/icon) component to add an icon anywhere in your app by following this pattern: `i-{collection_name}-{icon_name}`.
```vue
<template>
<UIcon name="i-heroicons-moon" class="w-5 h-5 text-primary-500" />
<UIcon name="i-heroicons-moon" />
</template>
```
### Collections
It's highly recommended to install the icons collections locally with:
By default, the module uses [Heroicons](https://heroicons.com/) but you can change it from the module options in your `nuxt.config.ts`.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
ui: {
icons: ['mdi', 'simple-icons']
}
})
```
::callout{icon="i-heroicons-light-bulb"}
Search the icon you want to use on https://icones.js.org built by [@antfu](https://github.com/antfu).
::
Thanks to [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons) plugin, only the icons you use in your app will be bundled in your CSS. However, you need to install the icon collections you specified in the `ui.icons` key:
::code-group
@@ -320,11 +329,55 @@ npm install @iconify-json/{collection_name}
::
For example, to use the `i-uil-github` icon, install it's collection with `@iconify-json/uil`. This way the icons can be served locally or from your serverless functions, which is faster and more reliable on both SSR and client-side.
If you choose to use the full `@iconify/json` icon collection (50MB), you can specifiy `icons: 'all'` or `icons: {}` in your `nuxt.config.ts` to use any icon in your app.
::callout{icon="i-heroicons-light-bulb" to="https://github.com/nuxt/icon?tab=readme-ov-file#custom-local-collections" target="_blank"}
Read more about custom collections in the `@nuxt/icon` documentation.
::
```ts [nuxt.config.ts]
export default defineNuxtConfig({
ui: {
icons: {}
}
})
```
### Custom config
If you have specific needs, like using a custom icon collection, you can use the `icons` option in your `nuxt.config.ts` as an object to override the config of the [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
```ts [nuxt.config.ts]
import { getIconCollections } from '@egoist/tailwindcss-icons'
export default defineNuxtConfig({
ui: {
icons: {
// might solve stretch bug on generate, see https://github.com/egoist/tailwindcss-icons/issues/23
extraProperties: {
'mask-size': 'contain',
'mask-position': 'center'
},
collections: {
foo: {
icons: {
'arrow-left': {
// svg body
body: '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />',
// svg width and height, optional
width: 24,
height: 24
}
}
},
...getIconCollections(['heroicons', 'simple-icons'])
}
}
}
})
```
### Dynamic icons
The `Icon` component also has a `dynamic` prop to use the [nuxt-icon](https://github.com/nuxt-modules/icon/) module instead of the [@egoist/tailwindcss-icons](https://github.com/egoist/tailwindcss-icons#plugin-options) plugin.
Read more about this in the [Icon](/components/icon#dynamic) component page.
### Defaults

View File

@@ -3,7 +3,7 @@ description: Display togglable accordion panels.
links:
- label: Disclosure
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/v1/vue/disclosure'
to: 'https://headlessui.com/vue/disclosure'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Accordion.vue

View File

@@ -163,22 +163,18 @@ This can be handy when you want to display HTML content. To achieve this, you ca
:component-example{component="alert-example-html"}
### `icon`
### `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`
### `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`
Use the `#actions` slot to add custom user interaction elements.
## Props
:component-props

View File

@@ -98,7 +98,7 @@ The number of indicators will be automatically generated based on the number of
:component-example{component="carousel-example-indicators-size"}
## Autoplay
## Autoplay :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
You can easily implement an autoplay behavior using the exposed [API](#api) through a template ref.

View File

@@ -87,10 +87,6 @@ slots:
[Label]{.italic}
::
### `help` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
Like the `#label` slot, use the `#help` slot to override the content of the help text.
## Props
:component-props

View File

@@ -4,7 +4,7 @@ description: Add a customizable command palette to your app.
links:
- label: 'Combobox'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/v1/vue/combobox'
to: 'https://headlessui.com/vue/combobox'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/navigation/CommandPalette.vue
@@ -246,7 +246,7 @@ When accessing the component via a template ref, you can use the following:
::field{name="updateQuery (query)"}
Updates the current query.
::
::field{name="results" type="ComputedRef<FuseResult<Command>[]>"}
::field{name="results" type="ComputedRef<Fuse.FuseResult<Command>[]>"}
The results exposed by [useFuse](https://vueuse.org/integrations/useFuse/#usefuse).
::
::field{name="comboboxApi"}

View File

@@ -66,7 +66,7 @@ excludedProps:
---
::
### Size
### Size :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
Use the `size` prop to change the size of the divider.

View File

@@ -3,7 +3,7 @@ description: Display a list of actions in a dropdown menu.
links:
- label: Menu
icon: i-simple-icons-headlessui
to: https://headlessui.com/v1/vue/menu
to: https://headlessui.com/vue/menu
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Dropdown.vue

View File

@@ -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-information-circle" />
description: Write only valid email address <UIcon name="i-heroicons-arrow-right-20-solid" />
default: <UInput model-value="benjamincanac" placeholder="you@example.com" />
props:
label: 'Email'

View File

@@ -8,13 +8,13 @@ links:
## Usage
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://github.com/fabian-hiller/valibot), or your own validation logic.
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/form-group) component to display error messages around form elements automatically.
It works with the [FormGroup](/components/input) component to display error messages around form elements automatically.
The form component requires two props:
- `state` - a reactive object holding the form's state.
- `schema` - a schema object from a validation library like [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi) or [Valibot](https://github.com/fabian-hiller/valibot).
- `schema` - a schema object from [Yup](#yup), [Zod](#zod), [Joi](#joi), or [Valibot](#valibot).
::callout{icon="i-heroicons-light-bulb"}
Note that **no validation library is included** by default, so ensure you **install the one you need**.
@@ -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 that it can be used alongside the `schema` prop to handle complex use cases.
Note: this can be used alongside the `schema` prop to handle complex use cases.
::
::component-example
@@ -105,7 +105,7 @@ defineExpose({
</script>
<template>
<UForm ref="form" :state="model" :validate="validateWithVuelidate">
<UForm ref="form" :model="model" :validate="validateWithVuelidate">
<slot />
</UForm>
</template>
@@ -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 submitted 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 validated 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's an example that focuses the first input element with an error after the form is submitted:
Here is an example of how to focus the first form element with an error:
::component-example
---

View File

@@ -1,27 +1,56 @@
---
description: Add 200,000+ ready to use icons to your Nuxt application, based on Iconify.
description: Display any icon (100,000+) from Iconify.
links:
- label: Iconify
icon: i-simple-icons-iconify
to: https://icones.js.org
- label: Nuxt Icon
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/icon
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/elements/Icon.vue
---
## Usage
You can use any name from the https://icones.js.org collection such as the `i-` prefix:
Use the `name` prop by following this pattern: `i-{collection_name}-{icon_name}`. You can search any icon from [Iconify](https://iconify.design/) using https://icones.js.org.
::component-card
---
baseProps:
class: 'w-5 h-5'
props:
name: 'i-heroicons-light-bulb'
---
::
::callout{icon="i-heroicons-light-bulb"}
It's highly recommended to install the icons collections you need, read more about this in [Theming](/getting-started/theming#icons).
::callout{icon="i-heroicons-exclamation-triangle"}
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
You can use the `dynamic` prop to enable dynamic icon loading. This will use [`nuxt-icon`](https://github.com/nuxt-modules/icon) instead and allow you to use any icon from [Iconify](https://iconify.design/) as well as the `{collection_name}:{icon_name}` pattern.
This can be quite useful when using [dynamic class names](https://tailwindcss.com/docs/content-configuration#dynamic-class-names) or for icons that are not bundled by default (fetched from a database for example).
::component-card
---
props:
name: 'i-ph-rocket-launch'
dynamic: true
---
::
You can also change the default behavior of the `dynamic` prop by setting the `ui.icons.dynamic` option in your `app.config.ts`.
```ts [app.config.ts]
export default defineAppConfig({
ui: {
icons: {
dynamic: true
}
}
})
```
## Props
:component-props

View File

@@ -4,7 +4,7 @@ description: Display an autocomplete input with real-time suggestions.
links:
- label: 'Combobox'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/v1/vue/combobox'
to: 'https://headlessui.com/vue/combobox'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/InputMenu.vue
@@ -130,8 +130,6 @@ 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.
::component-example
---
component: 'input-menu-example-search-async'

View File

@@ -71,12 +71,10 @@ props:
Use the `type` prop to change the input type, the default `type` is set to `text`, you can check all the available types at [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types).
Some types have been implemented in their own components, such as [Checkbox](/components/checkbox), [Radio](/components/radio-group), etc. and others have been styled like `file` for example.
Some types have been implemented in their own components, such as [Checkbox](/components/checkbox), [Radio](/components/radio-group), etc. and others have been styled like `file` for example. :u-badge{label="New" class="!rounded-full" variant="subtle"}
::component-card
---
baseProps:
icon: 'i-heroicons-folder'
props:
type: 'file'
size: sm

View File

@@ -3,7 +3,7 @@ description: Display a modal within your application.
links:
- label: 'Dialog'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/v1/vue/dialog'
to: 'https://headlessui.com/vue/dialog'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/Modal.vue
@@ -59,9 +59,9 @@ Set the `fullscreen` prop to `true` to enable it.
:component-example{component="modal-example-fullscreen"}
### Control programmatically
### Control programmatically :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
First of all, add the `Modals` component to your app, preferably inside `app.vue`. This component provides your application a place to render programmatically created modals.
First of all, add the `Modals` component to your app, preferably inside `app.vue`.
```vue [app.vue]
<template>
@@ -77,15 +77,9 @@ 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.
<!-- For prerendering -->
:component-example{component="modal-example-component" hiddenCode hiddenPreview }
:component-example{component="modal-example-composable"}
::code-group{class="[&>div:last-child>div:first-child]:!rounded-t-none"}
:component-example{component="modal-example-composable" label="index.vue" }
:component-example{component="modal-example-component" hiddenPreview label="components/ModalExampleComponent.vue" }
::
Additionally, you can close the modal within the modal component by calling `modal.close()`.
Additionally, you can close the modal within the modal component by calling `modal.close()`.
## Props

View File

@@ -29,7 +29,7 @@ export default defineAppConfig({
ui: {
notifications: {
// Show toasts at the top right of the screen
position: 'top-0 right-0'
position: 'top-0 bottom-auto'
}
}
})

View File

@@ -46,12 +46,6 @@ 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.

View File

@@ -3,7 +3,7 @@ description: Display a non-modal dialog that floats around a trigger element.
links:
- label: 'Popover'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/v1/vue/popover'
to: 'https://headlessui.com/vue/popover'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/Popover.vue

View File

@@ -126,10 +126,6 @@ slots:
[Label]{.italic}
::
### `help` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
Like the `#label` slot, use the `#help` slot to override the content of the help text.
### `legend`
Use the `#legend` slot to override the content of the legend.
@@ -156,11 +152,7 @@ slots:
## Config
::callout{icon="i-heroicons-light-bulb"}
Use the `ui` prop to override the radio group config and the `uiRadio` prop to override the radio config.
::
::tabs
:component-preset{label="Radio (uiRadio)" slug="Radio"}
:component-preset{label="RadioGroup (ui)"}
:component-preset{label="Radio" slug="Radio"}
:component-preset{label="RadioGroup"}
::

View File

@@ -4,7 +4,7 @@ description: Display a select menu with advanced features.
links:
- label: 'Listbox'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/v1/vue/listbox'
to: 'https://headlessui.com/vue/listbox'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/SelectMenu.vue
@@ -87,7 +87,7 @@ Use the `searchable` prop to enable search.
Use the `searchable-placeholder` prop to set a different placeholder.
This will use Headless UI [Combobox](https://headlessui.com/v1/vue/combobox) component instead of [Listbox](https://headlessui.com/v1/vue/listbox).
This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) component instead of [Listbox](https://headlessui.com/vue/listbox).
::component-card
---
@@ -150,8 +150,6 @@ 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.
::component-example
---
component: 'select-menu-example-search-async'
@@ -188,18 +186,6 @@ componentProps:
---
::
Pass a function to the `show-create-option-when` prop to control wether or not to show the create option. This function takes two arguments: the query (as the first argument) and an array of current results (as the second argument). It should return true to display the create option. :u-badge{label="New" class="!rounded-full" variant="subtle"}
The example below shows how to make the create option visible when the query is at least three characters long and does not exactly match any of the current results (case insensitive).
::component-example
---
component: 'select-menu-example-creatable-function'
componentProps:
class: 'w-full lg:w-48'
---
::
## Popper
Use the `popper` prop to customize the popper instance.

View File

@@ -3,7 +3,7 @@ description: Display a dialog that slides in from the edge of the screen.
links:
- label: 'Dialog'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/v1/vue/dialog'
to: 'https://headlessui.com/vue/dialog'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/overlays/Slideover.vue
@@ -53,7 +53,7 @@ defineShortcuts({
</script>
```
### Control programmatically
### 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`.

View File

@@ -29,7 +29,6 @@ Use the `columns` prop to configure which columns to display. It's an array of o
- `sortable` - Whether the column is sortable. Defaults to `false`.
- `direction` - The sort direction to use on first click. Defaults to `asc`.
- `class` - The class to apply to the column cells.
- `rowClass` - The class to apply to the data column cells. :u-badge{label="New" class="!rounded-full" variant="subtle"}
- `sort` - Pass your own `sort` function. Defaults to a simple _greater than_ / _less than_ comparison.
::component-example{class="grid"}
@@ -50,7 +49,7 @@ extraClass: 'overflow-hidden'
padding: false
component: 'table-example-columns-selectable'
componentProps:
class: 'flex-1 flex-col overflow-hidden'
class: 'flex-1'
---
::
@@ -148,11 +147,11 @@ const columns = [{
sortable: true
}]
const { data, status } = await useLazyFetch(() => `/api/users?orderBy=${sort.value.column}&order=${sort.value.direction}`)
const { data, pending } = await useLazyFetch(() => `/api/users?orderBy=${sort.value.column}&order=${sort.value.direction}`)
</script>
<template>
<UTable v-model:sort="sort" :loading="status === 'pending'" :columns="columns" :rows="data" sort-mode="manual" />
<UTable v-model:sort="sort" :loading="pending" :columns="columns" :rows="data" sort-mode="manual" />
</template>
```
@@ -256,7 +255,7 @@ componentProps:
::
::callout{icon="i-heroicons-light-bulb"}
You can use the `by` prop to compare objects by a field instead of comparing object instances. We've replicated the behavior of Headless UI [Combobox](https://headlessui.com/v1/vue/combobox#binding-objects-as-values).
You can use the `by` prop to compare objects by a field instead of comparing object instances. We've replicated the behavior of Headless UI [Combobox](https://headlessui.com/vue/combobox#binding-objects-as-values).
::
You can also add a `select` listener on your Table to make the rows clickable. The function will receive the row as the first argument.
@@ -283,7 +282,7 @@ extraClass: 'overflow-hidden'
padding: false
component: 'table-example-searchable'
componentProps:
class: 'flex-1 flex-col overflow-hidden'
class: 'flex-1'
---
::
@@ -296,19 +295,6 @@ You can easily use the [Pagination](/components/pagination) component to paginat
extraClass: 'overflow-hidden'
padding: false
component: 'table-example-paginable'
componentProps:
class: 'flex-1 flex-col overflow-hidden'
---
::
### Expandable :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
You can use the `expand` slot to display extra information about a row. You will have access to the `row` property in the slot scope.
::component-example{class="grid"}
---
padding: false
component: 'table-example-expandable'
componentProps:
class: 'flex-1'
---
@@ -359,11 +345,11 @@ This can be easily used with Nuxt `useAsyncData` composable.
<script setup lang="ts">
const columns = [...]
const { status, data: people } = await useLazyAsyncData('people', () => $fetch('/api/people'))
const { pending, data: people } = await useLazyAsyncData('people', () => $fetch('/api/people'))
</script>
<template>
<UTable :rows="people" :columns="columns" :loading="status === 'pending'" />
<UTable :rows="people" :columns="columns" :loading="pending" />
</template>
```
@@ -464,19 +450,6 @@ 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

View File

@@ -11,7 +11,6 @@ links:
Pass an array to the `items` prop of the Tabs component. Each item can have the following properties:
- `label` - The label of the item.
- `icon` - The icon of the item.
- `slot` - A key to customize the item with a slot.
- `content` - The content to display in the panel by default.
- `disabled` - Determines whether the item is disabled or not.
@@ -64,8 +63,6 @@ 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.
@@ -92,12 +89,6 @@ Use the `#default` slot to customize the content of the trigger buttons. You wil
:component-example{component="tabs-example-default-slot"}
### `icon` :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
Use the `#icon` slot to customize the icon of the trigger buttons. You will have access to the `item`, `index`, `selected` and `disabled` in the slot scope.
:component-example{component="tabs-example-icon-slot"}
### `item`
Use the `#item` slot to customize the items content. You will have access to the `item`, `index` and `selected` properties in the slot scope.

View File

@@ -119,7 +119,8 @@ props:
---
::
Use the `maxrows` prop to set a maximum number of rows when autoresizing. If set to `0`, the Textarea will infinitely grow up.
Use the `maxrows` prop to set a maximum number of rows when autoresizing. If set to `0`, the Textarea will infinitely grow up. :u-badge{label="New" class="!rounded-full" variant="subtle"}
::component-card
---
baseProps:

View File

@@ -3,7 +3,7 @@ description: Display a toggle field.
links:
- label: 'Switch'
icon: i-simple-icons-headlessui
to: 'https://headlessui.com/v1/vue/switch'
to: 'https://headlessui.com/vue/switch'
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/Toggle.vue
@@ -52,7 +52,7 @@ excludedProps:
---
::
### Loading
### 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.

View File

@@ -67,19 +67,14 @@ sections:
color: black
size: lg
icon: i-heroicons-cube
- label: Star on GitHub
to: https://github.com/nuxt/ui
target: _blank
color: white
size: lg
icon: i-simple-icons-github
cta:
title: Built and driven by an <span class="text-primary">amazing community</span>
align: center
users:
- name: Benjamin Canac
username: benjamincanac
to: https://twitter.com/benjamincanac
- name: Romain Hamel
username: romhml
to: https://github.com/romhml
- name: Neil Richter
username: noook
to: https://nook.sh/
title: Trusted and supported by our<br class="hidden lg:block"> amazing community
pro:
title: Upgrade to <span class="text-primary">Nuxt UI Pro</span>
description: 'Nuxt UI Pro is a collection of premium Vue components built on top of Nuxt UI to create beautiful & responsive Nuxt applications in minutes.<br>It includes all primitives to build landing pages, documentations, blogs, dashboards or entire SaaS products.'

View File

@@ -30,7 +30,7 @@
<script setup lang="ts">
import type { NuxtError } from '#app'
import type { ParsedContent } from '@nuxt/content'
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
useSeoMeta({
title: 'Page not found',

View File

@@ -17,7 +17,7 @@
</template>
<script setup lang="ts">
import type { NavItem } from '@nuxt/content'
import type { NavItem } from '@nuxt/content/dist/runtime/types'
const nav = inject<Ref<NavItem[]>>('navigation')

View File

@@ -1,7 +1,7 @@
import { createResolver } from '@nuxt/kit'
import colors from 'tailwindcss/colors'
import module from '../src/module'
import { excludeColors } from '../src/runtime/utils/colors'
import { excludeColors } from '../src/colors'
import pkg from '../package.json'
const { resolve } = createResolver(import.meta.url)
@@ -15,7 +15,6 @@ export default defineNuxtConfig({
'@nuxt/ui-pro',
process.env.NUXT_GITHUB_TOKEN && ['github:nuxt/ui-pro/.docs#dev', { giget: { auth: process.env.NUXT_GITHUB_TOKEN } }]
].filter(Boolean),
modules: [
'@nuxt/content',
'@nuxt/fonts',
@@ -28,18 +27,16 @@ export default defineNuxtConfig({
'nuxt-cloudflare-analytics',
'modules/content-examples-code'
],
runtimeConfig: {
public: {
version: pkg.version
}
},
ui: {
global: true,
icons: ['heroicons', 'simple-icons'],
safelistColors: excludeColors(colors)
},
content: {
highlight: {
langs: [
@@ -74,11 +71,9 @@ export default defineNuxtConfig({
} : undefined
}
},
image: {
provider: 'ipx'
},
nitro: {
prerender: {
routes: [
@@ -92,12 +87,10 @@ export default defineNuxtConfig({
ignore: !process.env.NUXT_UI_PRO_PATH && !process.env.NUXT_GITHUB_TOKEN ? ['/pro'] : []
}
},
routeRules: {
'/components': { redirect: '/components/accordion', prerender: false },
'/dev/components': { redirect: '/dev/components/accordion', prerender: false }
},
componentMeta: {
exclude: [
'@nuxt/content',
@@ -118,12 +111,10 @@ export default defineNuxtConfig({
exposed: false
}
},
cloudflareAnalytics: {
token: '1e2b0c5e9a214f0390b9b94e043d8d4c',
scriptPath: false
},
hooks: {
// Related to https://github.com/nuxt/nuxt/pull/22558
'components:extend': (components) => {
@@ -136,20 +127,12 @@ export default defineNuxtConfig({
})
}
},
typescript: {
strict: false
},
site: {
url: 'https://ui.nuxt.com'
},
vite: {
optimizeDeps: {
include: ['date-fns']
}
},
compatibilityDate: '2024-07-23'
})
}
})

View File

@@ -3,30 +3,32 @@
"private": true,
"type": "module",
"dependencies": {
"@iconify-json/heroicons": "^1.2.0",
"@iconify-json/simple-icons": "^1.2.3",
"@iconify-json/vscode-icons": "^1.2.2",
"@nuxt/content": "^2.13.2",
"@nuxt/eslint-config": "^0.4.0",
"@nuxt/fonts": "^0.8.0",
"@nuxt/image": "^1.8.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@1.3.1-28698404.4d54eb2",
"@nuxtjs/plausible": "^1.0.2",
"@octokit/rest": "^21.0.2",
"@vueuse/nuxt": "^11.1.0",
"date-fns": "^4.1.0",
"@nuxt/ui": "workspace:latest"
},
"devDependencies": {
"@iconify-json/heroicons": "^1.1.20",
"@iconify-json/simple-icons": "^1.1.98",
"@nuxt/content": "^2.12.1",
"@nuxt/eslint-config": "^0.3.0",
"@nuxt/fonts": "^0.6.1",
"@nuxt/image": "^1.5.0",
"@nuxt/ui-pro": "npm:@nuxt/ui-pro-edge@1.1.0-28538540.a353e68",
"@nuxtjs/plausible": "^1.0.0",
"@octokit/rest": "^20.1.0",
"@vueuse/nuxt": "^10.9.0",
"date-fns": "^3.6.0",
"eslint": "^8.57.0",
"joi": "^17.13.3",
"nuxt": "^3.13.2",
"joi": "^17.12.3",
"nuxt": "^3.11.2",
"nuxt-cloudflare-analytics": "^1.0.8",
"nuxt-component-meta": "^0.8.2",
"nuxt-og-image": "^3.0.2",
"prettier": "^3.3.3",
"ufo": "^1.5.4",
"nuxt-component-meta": "^0.6.3",
"nuxt-og-image": "^2.2.4",
"prettier": "^3.2.5",
"typescript": "^5.4.4",
"ufo": "^1.5.3",
"v-calendar": "^3.1.2",
"valibot": "^0.42.0",
"valibot": "^0.30.0",
"yup": "^1.4.0",
"zod": "^3.23.8"
"zod": "^3.22.4"
}
}

View File

@@ -75,7 +75,10 @@ useSeoMeta({
ogDescription: page.value.description
})
defineOgImageComponent('Docs', {
defineOgImage({
component: 'Docs',
title: page.value.title,
description: page.value.description,
headline: headline.value
})

View File

@@ -31,11 +31,10 @@
readonly
autocomplete="off"
icon="i-heroicons-command-line"
class="w-72"
input-class="focus:ring-1 focus:ring-gray-300 dark:focus:ring-gray-700"
input-class="select-none"
aria-label="Install @nuxt/ui"
size="lg"
:ui="{ icon: { trailing: { pointer: '' } } }"
:ui="{ base: 'disabled:cursor-default', icon: { trailing: { pointer: '' } } }"
>
<template #trailing>
<UButton
@@ -102,79 +101,77 @@
<ULandingSection class="!pt-0 dark:bg-gradient-to-b from-gray-950/50 to-gray-900">
<ULandingCTA
align="left"
:card="false"
card
:ui="{
background: 'dark:bg-gradient-to-b from-gray-800 to-gray-900',
shadow: 'dark:shadow-2xl',
body: {
padding: '!p-0'
background: 'bg-gray-50/50 dark:bg-gray-900/50'
},
title: 'text-center lg:text-left lg:text-5xl',
description: 'mt-10 flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-8 lg:gap-16',
links: '-ml-3 justify-center lg:justify-start flex-wrap gap-y-3'
title: 'text-center lg:text-left',
links: 'justify-center lg:justify-start'
}"
>
<template #title>
<span v-html="page.cta.title" />
</template>
<template #description>
<NuxtLink class="text-center lg:text-left group" to="https://npmjs.org/package/@nuxt/ui" target="_blank">
<p class="text-5xl font-semibold text-gray-900 dark:text-white group-hover:text-primary-500 dark:group-hover:text-primary-400">
<template #links>
<ClientOnly>
<UAvatarGroup :max="xlAndLarger ? 13 : lgAndLarger ? 10 : mdAndLarger ? 16 : 8" size="md" class="flex-wrap-reverse [&_span:first-child]:text-xs justify-center">
<UTooltip
v-for="(contributor, index) of module.contributors"
:key="index"
:text="contributor.username"
class="rounded-full"
:ui="{ background: 'bg-gray-50 dark:bg-gray-800/50' }"
:popper="{ offsetDistance: 16 }"
>
<UAvatar
:alt="contributor.username"
:src="`https://ipx.nuxt.com/s_40x40/gh_avatar/${contributor.username}`"
:srcset="`https://ipx.nuxt.com/s_80x80/gh_avatar/${contributor.username} 2x`"
class="lg:hover:scale-125 lg:hover:ring-2 lg:hover:ring-primary-500 dark:lg:hover:ring-primary-400 transition-transform"
width="40"
height="40"
size="md"
loading="lazy"
>
<NuxtLink :to="`https://github.com/${contributor.username}`" :aria-label="contributor.username" target="_blank" class="focus:outline-none" tabindex="-1">
<span class="absolute inset-0" aria-hidden="true" />
</NuxtLink>
</UAvatar>
</UTooltip>
</UAvatarGroup>
</ClientOnly>
</template>
<div class="flex flex-col sm:flex-row items-center justify-center gap-8 lg:gap-16">
<NuxtLink class="text-center group" to="https://npmjs.org/package/@nuxt/ui" target="_blank">
<p class="text-6xl font-semibold text-gray-900 dark:text-white group-hover:text-primary-500 dark:group-hover:text-primary-400">
{{ format(module.stats.downloads) }}+
</p>
<p>monthly downloads</p>
</NuxtLink>
<NuxtLink class="text-center lg:text-left group" to="https://github.com/nuxt/ui" target="_blank">
<p class="text-5xl font-semibold text-gray-900 dark:text-white group-hover:text-primary-500 dark:group-hover:text-primary-400">
<NuxtLink class="text-center group" to="https://github.com/nuxt/ui" target="_blank">
<p class="text-6xl font-semibold text-gray-900 dark:text-white group-hover:text-primary-500 dark:group-hover:text-primary-400">
{{ format(module.stats.stars) }}+
</p>
<p>GitHub stars</p>
<p>stars</p>
</NuxtLink>
</template>
<template #links>
<UButton
v-for="user in page.cta.users"
:key="user.username"
:to="user.to"
size="md"
color="gray"
variant="ghost"
target="_blank"
>
<UAvatar
:alt="user.username"
:src="`https://ipx.nuxt.com/s_80x80/gh_avatar/${user.username}`"
:srcset="`https://ipx.nuxt.com/s_160x160/gh_avatar/${user.username} 2x`"
width="80"
height="80"
size="md"
loading="lazy"
/>
<div class="text-left">
<p class="font-medium">
{{ user.name }}
</p>
<p class="text-gray-500 dark:text-gray-400 leading-4">
{{ `@${user.username}` }}
</p>
</div>
</UButton>
</template>
<div class="p-5 overflow-hidden flex">
<HomeContributors :contributors="module.contributors" />
</div>
</ULandingCTA>
</ULandingSection>
<template v-if="navigation.find(item => item._path === '/pro')">
<ULandingHero id="pro" :links="page.pro.links" :ui="{ title: 'sm:text-6xl' }" class="bg-gradient-to-b from-gray-50 dark:from-gray-950/50 to-white dark:to-gray-900 relative">
<template #top>
<Gradient class="absolute inset-x-0 top-0 w-full block" />
</template>
<div class="relative">
<UDivider class="absolute inset-x-0" />
<div class="w-full relative overflow-hidden h-px bg-gradient-to-r from-gray-800 via-primary-400 to-gray-800 max-w-5xl mx-auto" />
</div>
<ULandingHero id="pro" :links="page.pro.links" :ui="{ title: 'sm:text-6xl' }" class="bg-gradient-to-b from-gray-50 dark:from-gray-950/50 to-white dark:to-gray-900 relative">
<template #title>
<span v-html="page.pro.title" />
</template>
@@ -409,7 +406,7 @@
</template>
<script setup lang="ts">
import type { ParsedContent, NavItem } from '@nuxt/content'
import type { ParsedContent, NavItem } from '@nuxt/content/dist/runtime/types'
import { useElementBounding, useWindowScroll, useElementSize, breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import type { HomeProBlock } from '~/types'
@@ -438,12 +435,11 @@ useSeoMeta({
twitterImage: 'https://ui.nuxt.com/social-card.png'
})
const source = ref('npx nuxi@latest module add ui')
const source = ref('npm i @nuxt/ui')
const sectionRef = ref()
const demoRef = ref(null)
const demoRef = ref()
const start = ref(0)
const { $ui } = useNuxtApp()
const { height } = useElementSize(demoRef)
const { top } = useElementBounding(sectionRef)
const { y } = useWindowScroll()
@@ -451,7 +447,9 @@ const config = useRuntimeConfig().public
const { copy, copied } = useClipboard({ source })
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greaterOrEqual('md')
const lgAndLarger = breakpoints.greaterOrEqual('lg')
const xlAndLarger = breakpoints.greaterOrEqual('xl')
const { format } = Intl.NumberFormat('en', { notation: 'compact' })

View File

@@ -9,7 +9,11 @@ useSeoMeta({
description
})
defineOgImageComponent('Docs')
defineOgImage({
component: 'Docs',
title,
description
})
</script>
<template>

View File

@@ -69,7 +69,11 @@ useSeoMeta({
ogDescription: description
})
defineOgImageComponent('Docs')
defineOgImage({
component: 'Docs',
title,
description
})
</script>
<style scoped>

View File

@@ -9,7 +9,11 @@ useSeoMeta({
description
})
defineOgImageComponent('Docs')
defineOgImage({
component: 'Docs',
title,
description
})
const appConfig = useAppConfig()
const colorMode = useColorMode()

View File

@@ -1,8 +1,6 @@
{
"name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "2.18.5",
"packageManager": "pnpm@9.10.0",
"version": "2.15.1",
"repository": "nuxt/ui",
"homepage": "https://ui.nuxt.com",
"type": "module",
@@ -18,6 +16,9 @@
"files": [
"dist"
],
"engines": {
"node": ">=v16.20.2"
},
"scripts": {
"build": "nuxt-module-build build",
"prepack": "pnpm build",
@@ -32,52 +33,55 @@
"test": "vitest"
},
"dependencies": {
"@headlessui/tailwindcss": "^0.2.1",
"@headlessui/vue": "^1.7.23",
"@iconify-json/heroicons": "^1.2.0",
"@nuxt/icon": "^1.5.1",
"@nuxt/kit": "^3.13.2",
"@nuxtjs/color-mode": "^3.5.1",
"@nuxtjs/tailwindcss": "^6.12.1",
"@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.11.2",
"@nuxtjs/color-mode": "^3.3.3",
"@nuxtjs/tailwindcss": "^6.11.4",
"@popperjs/core": "^2.11.8",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@vueuse/core": "^11.1.0",
"@vueuse/integrations": "^11.1.0",
"@vueuse/math": "^11.1.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.12",
"@vueuse/core": "^10.9.0",
"@vueuse/integrations": "^10.9.0",
"@vueuse/math": "^10.9.0",
"defu": "^6.1.4",
"fuse.js": "^7.0.0",
"ohash": "^1.1.4",
"fuse.js": "^6.6.2",
"nuxt-icon": "^0.6.10",
"ohash": "^1.1.3",
"pathe": "^1.1.2",
"scule": "^1.3.0",
"tailwind-merge": "^2.5.2",
"tailwindcss": "^3.4.12"
"tailwind-merge": "^2.2.2",
"tailwindcss": "^3.4.3"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.4.0",
"@nuxt/module-builder": "^0.8.4",
"@nuxt/test-utils": "^3.14.2",
"@release-it/conventional-changelog": "^8.0.2",
"@vue/test-utils": "^2.4.6",
"@nuxt/eslint-config": "^0.3.0",
"@nuxt/module-builder": "^0.5.5",
"@nuxt/test-utils": "^3.12.0",
"@release-it/conventional-changelog": "^8.0.1",
"@vue/test-utils": "^2.4.5",
"eslint": "^8.57.0",
"happy-dom": "^14.12.3",
"joi": "^17.13.3",
"nuxt": "^3.13.2",
"release-it": "^17.6.0",
"happy-dom": "^14.5.1",
"joi": "^17.12.3",
"nuxt": "^3.11.2",
"release-it": "^17.1.1",
"typescript": "^5.4.4",
"unbuild": "^2.0.0",
"valibot": "^0.42.0",
"valibot30": "npm:valibot@0.30.0",
"valibot31": "npm:valibot@0.31.0",
"vitest": "^2.1.1",
"vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.1.6",
"valibot": "^0.30.0",
"vitest": "^1.4.0",
"vitest-environment-nuxt": "^1.0.0",
"vue-tsc": "^2.0.10",
"yup": "^1.4.0",
"zod": "^3.23.8"
"zod": "^3.22.4"
},
"resolutions": {
"@nuxt/ui": "workspace:*",
"@nuxtjs/mdc": "0.9.0"
"@nuxt/kit": "^3.11.2",
"@nuxt/schema": "3.11.2",
"tailwindcss": "^3.4.3",
"@headlessui/vue": "1.7.19",
"vue": "3.4.21"
}
}

View File

@@ -1,7 +1,5 @@
export default defineNuxtConfig({
modules: [
'../src/module'
],
compatibilityDate: '2024-07-23'
})
]
})

View File

@@ -1,14 +0,0 @@
{
"private": true,
"name": "@nuxt/ui-playground",
"type": "module",
"scripts": {
"dev": "nuxi dev",
"build": "nuxi build",
"generate": "nuxi generate"
},
"dependencies": {
"@nuxt/ui": "latest",
"nuxt": "^3.13.2"
}
}

21938
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
packages:
- "./"
- "docs"
- "playground"
- "./"

View File

@@ -1,20 +1,5 @@
{
"extends": [
"github>nuxt/renovate-config-nuxt"
],
"lockFileMaintenance": {
"enabled": true
},
"ignoreDeps": [
"@nuxt/eslint-config",
"eslint",
"happy-dom",
"valibot30",
"valibot31"
],
"baseBranches": ["dev", "v3"],
"packageRules": [{
"matchBaseBranches": ["v3"],
"labels": ["v3"]
}]
]
}

285
src/colors.ts Normal file
View File

@@ -0,0 +1,285 @@
import { omit } from './runtime/utils/lodash'
import { kebabCase, camelCase, upperFirst } from 'scule'
const colorsToExclude = [
'inherit',
'transparent',
'current',
'white',
'black',
'slate',
'gray',
'zinc',
'neutral',
'stone',
'cool'
]
const safelistByComponent = {
alert: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-50`)
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`)
}],
avatar: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}],
badge: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-50`)
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`)
}],
button: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-50`),
variants: ['hover', 'disabled']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-100`),
variants: ['hover']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark', 'dark:disabled']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`),
variants: ['disabled', 'dark:hover']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-600`),
variants: ['hover']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-900`),
variants: ['dark:hover']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-950`),
variants: ['dark', 'dark:hover', 'dark:disabled']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark', 'dark:disabled']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`),
variants: ['dark:hover', 'disabled']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-600`),
variants: ['hover']
}, {
pattern: new RegExp(`outline-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`outline-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
input: (colorsAsRegex) => [{
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark', 'dark:focus']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus']
}],
radio: (colorsAsRegex) => [{
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
checkbox: (colorsAsRegex) => [{
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
toggle: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
range: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
progress: (colorsAsRegex) => [{
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}],
meter: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}],
notification: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}],
chip: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}]
}
const safelistComponentAliasesMap = {
'USelect': 'UInput',
'USelectMenu': 'UInput',
'UTextarea': 'UInput',
'URadioGroup': 'URadio',
'UMeterGroup': 'UMeter'
}
const colorsAsRegex = (colors: string[]): string => colors.join('|')
export const excludeColors = (colors: object): string[] => {
return Object.entries(omit(colors, colorsToExclude))
.filter(([, value]) => typeof value === 'object')
.map(([key]) => kebabCase(key))
}
export const generateSafelist = (colors: string[], globalColors) => {
const baseSafelist = Object.keys(safelistByComponent).flatMap(component => safelistByComponent[component](colorsAsRegex(colors)))
// Ensure `red` color is safelisted for form elements so that `error` prop of `UFormGroup` always works
const formsSafelist = ['input', 'radio', 'checkbox', 'toggle', 'range'].flatMap(component => safelistByComponent[component](colorsAsRegex(['red'])))
return [
...baseSafelist,
...formsSafelist,
// Ensure all global colors are safelisted for the Notification (toast.add)
...safelistByComponent['notification'](colorsAsRegex(globalColors)),
// Gray safelist for Avatar & Notification
'bg-gray-500',
'dark:bg-gray-400',
'text-gray-500',
'dark:text-gray-400'
]
}
export const customSafelistExtractor = (prefix, content: string, colors: string[], safelistColors: string[]) => {
const classes: string[] = []
const regex = /<([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z][A-Za-z0-9]*)*)\s+(?![^>]*:color\b)[^>]*\bcolor=["']([^"']+)["'][^>]*>/gs
const matches = content.matchAll(regex)
const components = Object.keys(safelistByComponent).map(component => `${prefix}${component.charAt(0).toUpperCase() + component.slice(1)}`)
for (const match of matches) {
const [, component, color] = match
const camelComponent = upperFirst(camelCase(component))
if (!colors.includes(color) || safelistColors.includes(color)) {
continue
}
let name = safelistComponentAliasesMap[camelComponent] ? safelistComponentAliasesMap[camelComponent] : camelComponent
if (!components.includes(name)) {
continue
}
name = name.replace(prefix, '').toLowerCase()
const matchClasses = safelistByComponent[name](color).flatMap(group => {
return ['', ...(group.variants || [])].flatMap(variant => {
const matches = group.pattern.source.match(/\(([^)]+)\)/g)
return matches.map(match => {
const colorOptions = match.substring(1, match.length - 1).split('|')
return colorOptions.map(color => `${variant ? variant + ':' : ''}` + group.pattern.source.replace(match, color))
}).flat()
})
})
classes.push(...matchClasses)
}
return classes
}

View File

@@ -1,11 +1,14 @@
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 { 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'
import installTailwind from './tailwind'
import type { DeepPartial, Strategy } from './runtime/types/utils'
const defaultExtractor = createDefaultExtractor({ tailwindConfig: { separator: ':' } })
const _require = createRequire(import.meta.url)
const defaultColors = _require('tailwindcss/colors.js')
@@ -21,8 +24,14 @@ type UI = {
colors?: string[]
strategy?: Strategy
[key: string]: any
} & DeepPartial<typeof config, string>
} & DeepPartial<typeof config>
declare module 'nuxt/schema' {
interface AppConfigInput {
// @ts-ignore
ui?: UI
}
}
declare module '@nuxt/schema' {
interface AppConfigInput {
// @ts-ignore
@@ -41,6 +50,8 @@ export interface ModuleOptions {
*/
global?: boolean
icons: CollectionNames[] | 'all' | IconsPluginOptions
safelistColors?: string[]
/**
* Disables the global css styles added by the module.
@@ -54,11 +65,12 @@ export default defineNuxtModule<ModuleOptions>({
version,
configKey: 'ui',
compatibility: {
nuxt: '>=3.10.0'
nuxt: '^3.10.0'
}
},
defaults: {
prefix: 'U',
icons: ['heroicons'],
safelistColors: ['primary'],
disableGlobalStyles: false
},
@@ -76,13 +88,107 @@ 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('nuxt-icon')
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
await installTailwind(options, nuxt, resolve)
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)
]
}
}
}
}
})
// Plugins

View File

@@ -1,28 +1,13 @@
<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" v-bind="ui.default.checkbox" aria-label="Select all" @change="onChange" />
<UCheckbox :model-value="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" aria-label="Select all" @change="onChange" />
</th>
<th v-if="$slots.expand" scope="col" :class="ui.tr.base">
<span class="sr-only">Expand</span>
</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]"
:aria-sort="getAriaSort(column)"
>
<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]">
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
<UButton
v-if="column.sortable"
@@ -70,39 +55,17 @@
</tr>
<template v-else>
<template v-for="(row, index) in rows" :key="index">
<tr :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" v-bind="ui.default.checkbox" aria-label="Select row" @click.capture.stop="() => onSelect(row)" />
</td>
<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 />
</td>
<td
v-if="$slots.expand"
:class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]"
>
<UButton
v-bind="{ ...(ui.default.expandButton || {}), ...expandButton }"
:ui="{ icon: { base: [ui.expand.icon, openedRows.includes(index) && 'rotate-180'] } }"
@click="toggleOpened(index)"
/>
</td>
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size, column?.rowClass, row[column.key]?.class]">
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" :get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)">
{{ getRowData(row, column.key) }}
</slot>
</td>
</tr>
<tr v-if="openedRows.includes(index)">
<td colspan="100%">
<slot
name="expand"
:row="row"
:index="index"
/>
</td>
</tr>
</template>
<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]">
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" :get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)">
{{ getRowData(row, column.key) }}
</slot>
</td>
</tr>
</template>
</tbody>
</table>
@@ -110,8 +73,8 @@
</template>
<script lang="ts">
import { ref, computed, defineComponent, toRaw, toRef } from 'vue'
import type { PropType, AriaAttributes } from 'vue'
import { computed, defineComponent, toRaw, toRef } from 'vue'
import type { PropType } from 'vue'
import { upperFirst } from 'scule'
import { defu } from 'defu'
import { useVModel } from '@vueuse/core'
@@ -121,7 +84,7 @@ import UProgress from '../elements/Progress.vue'
import UCheckbox from '../forms/Checkbox.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig, get } from '../../utils'
import type { Strategy, Button, ProgressColor, ProgressAnimation } from '../../types/index'
import type { Strategy, Button, ProgressColor, ProgressAnimation } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { table } from '#ui/ui.config'
@@ -144,16 +107,6 @@ 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
rowClass?: string
[key: string]: any
}
export default defineComponent({
components: {
UIcon,
@@ -176,7 +129,7 @@ export default defineComponent({
default: () => []
},
columns: {
type: Array as PropType<Column[]>,
type: Array as PropType<{ key: string, sortable?: boolean, sort?: (a: any, b: any, direction: 'asc' | 'desc') => number, direction?: 'asc' | 'desc', class?: string, [key: string]: any }[]>,
default: null
},
columnAttribute: {
@@ -203,10 +156,6 @@ export default defineComponent({
type: String,
default: () => config.default.sortDescIcon
},
expandButton: {
type: Object as PropType<Button>,
default: () => config.default.expandButton as Button
},
loading: {
type: Boolean,
default: false
@@ -219,10 +168,6 @@ 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
@@ -240,12 +185,10 @@ export default defineComponent({
setup (props, { emit, attrs: $attrs }) {
const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class'))
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: upperFirst(key), sortable: false, class: undefined, sort: defaultSort }) as Column))
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: upperFirst(key), sortable: false, class: undefined, sort: defaultSort })))
const sort = useVModel(props, 'sort', emit, { passive: true, defaultValue: defu({}, props.sort, { column: null, direction: 'asc' }) })
const openedRows = ref([])
const savedSort = { column: sort.value.column, direction: null }
const rows = computed(() => {
@@ -326,18 +269,15 @@ export default defineComponent({
}
function selectAllRows () {
// Create a new array to ensure reactivity
const newSelected = [...selected.value]
// If the row is not already selected, add it to the newSelected array
props.rows.forEach((row) => {
if (!isSelected(row)) {
newSelected.push(row)
// If the row is already selected, don't select it again
if (isSelected(row)) {
return
}
})
// Reassign the array to trigger Vue's reactivity
selected.value = newSelected
// @ts-ignore
selected.value.push(row)
})
}
function onChange (checked: boolean) {
@@ -352,34 +292,6 @@ export default defineComponent({
return get(row, rowKey, defaultValue)
}
function toggleOpened (index: number) {
if (openedRows.value.includes(index)) {
openedRows.value = openedRows.value.filter((i) => i !== index)
} else {
openedRows.value.push(index)
}
}
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,
@@ -396,14 +308,11 @@ export default defineComponent({
emptyState,
// eslint-disable-next-line vue/no-dupe-keys
loadingState,
openedRows,
isSelected,
onSort,
onSelect,
onChange,
getRowData,
toggleOpened,
getAriaSort
getRowData
}
}
})

View File

@@ -73,7 +73,7 @@ import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig, omit } from '../../utils'
import type { AccordionItem, Strategy } from '../../types/index'
import type { AccordionItem, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { accordion, button } from '#ui/ui.config'
@@ -133,7 +133,7 @@ export default defineComponent({
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) {
@@ -194,7 +194,7 @@ export default defineComponent({
attrs,
buttonRefs,
closeOthers,
omit: (omit as any),
omit,
onEnter,
onBeforeLeave,
onAfterEnter,

View File

@@ -2,7 +2,7 @@
<div :class="alertClass" v-bind="attrs">
<div class="flex" :class="[ui.gap, { 'items-start': (description || $slots.description), 'items-center': !description && !$slots.description }]">
<slot name="icon" :icon="icon">
<UIcon v-if="icon" :name="icon" :class="ui.icon.base" />
<UIcon v-if="icon" :name="icon" :ui="ui.icon.base" />
</slot>
<slot name="avatar" :avatar="avatar">
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
@@ -14,23 +14,19 @@
{{ title }}
</slot>
</p>
<div v-if="description || $slots.description" :class="twMerge(ui.description, !title && !$slots.title && 'mt-0 leading-5')">
<p v-if="description || $slots.description" :class="twMerge(ui.description, !(title && $slots.title) && 'mt-0 leading-5')">
<slot name="description" :description="description">
{{ description }}
</slot>
</div>
</p>
<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 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>
</div>
<div v-if="closeButton || (!description && !$slots.description && actions.length)" :class="twMerge(ui.actions, 'mt-0')">
<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 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>
<UButton v-if="closeButton" aria-label="Close" v-bind="{ ...(ui.default.closeButton || {}), ...closeButton }" @click.stop="$emit('close')" />
@@ -47,7 +43,7 @@ import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue'
import { useUI } from '../../composables/useUI'
import type { Avatar, Button, AlertColor, AlertVariant, AlertAction, Strategy } from '../../types/index'
import type { Avatar, Button, AlertColor, AlertVariant, AlertAction, Strategy } from '../../types'
import { mergeConfig } from '../../utils'
// @ts-expect-error
import appConfig from '#build/app.config'

View File

@@ -27,7 +27,7 @@ import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig } from '../../utils'
import type { AvatarSize, AvatarChipColor, AvatarChipPosition, Strategy } from '../../types/index'
import type { AvatarSize, AvatarChipColor, AvatarChipPosition, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { avatar } from '#ui/ui.config'

View File

@@ -4,7 +4,7 @@ import { twMerge, twJoin } from 'tailwind-merge'
import UAvatar from './Avatar.vue'
import { useUI } from '../../composables/useUI'
import { mergeConfig, getSlotsChildren } from '../../utils'
import type { AvatarSize, Strategy } from '../../types/index'
import type { AvatarSize, Strategy } from '../../types'
// @ts-expect-error
import appConfig from '#build/app.config'
import { avatar, avatarGroup } from '#ui/ui.config'

Some files were not shown because too many files have changed in this diff Show More