Compare commits

...

100 Commits

Author SHA1 Message Date
Benjamin Canac
aabf1dd9eb chore(release): 2.0.2 2023-05-11 17:12:50 +02:00
Benjamin Canac
c7c78cb47b fix(LinkCustom): handle button when no to prop 2023-05-11 17:03:02 +02:00
Benjamin Canac
3335a6a32c docs: put back router.options.ts 2023-05-11 16:51:30 +02:00
Benjamin Canac
15f9db9420 chore(release): 2.0.1 2023-05-11 16:26:11 +02:00
Benjamin Canac
aacb7e9841 fix(CommandPalette): put back searchable on v-show to input ref always exists 2023-05-11 15:45:33 +02:00
Benjamin Canac
3ded73194d docs: improve command palettes 2023-05-11 15:24:26 +02:00
Benjamin Canac
82adedf764 docs: improve search 2023-05-11 15:18:01 +02:00
Benjamin Canac
192bf4c375 fix(CommandPalette): expose input ref to template 2023-05-11 15:17:47 +02:00
Benjamin Canac
59fc14e93f chore(SelectMenu): add prop for trailingIcon and fix padding when selected 2023-05-11 14:28:58 +02:00
Benjamin Canac
0d83366427 chore(Select): add prop for trailingIcon 2023-05-11 14:21:30 +02:00
Benjamin Canac
cc65afafbd chore(CommandPaletteGroup): rename ui.group.command.selected.icon to ui.group.command.selectedIcon.base 2023-05-11 14:21:01 +02:00
Benjamin Canac
c7e0cb40e7 chore(CommandPaletteGroup): style <mark> with primary 2023-05-11 14:18:14 +02:00
Benjamin Canac
24434dc561 docs: default value in select menu basic example 2023-05-11 14:17:28 +02:00
Benjamin Canac
6c35ee9270 docs: typo in context-menu 2023-05-11 14:17:12 +02:00
Benjamin Canac
950b341696 docs: prevent aside to scroll up on page change 2023-05-11 14:17:01 +02:00
Benjamin Canac
24e7109959 chore(deps): bump 2023-05-10 18:35:24 +02:00
Benjamin Canac
be96824323 chore(SelectMenu): improve option.selected preset 2023-05-10 18:33:43 +02:00
Benjamin Canac
d5471f4d37 fix(Toggle): wrong icon-off positioning 2023-05-10 18:14:21 +02:00
Benjamin Canac
00b444b3eb chore: rename spacing to padding 2023-05-10 16:24:28 +02:00
Benjamin Canac
76a0d61a0f fix(colors): missing useNuxtApp import 2023-05-10 12:39:08 +02:00
Benjamin Canac
6d79548ee8 chore(Slideover): default side to right 2023-05-10 12:27:20 +02:00
Benjamin Canac
f48ead6faf fix(docs): sticky search button z-index 2023-05-10 11:59:30 +02:00
Benjamin Canac
fd4c80acd4 fix(Avatar): gray missing for chipColor 2023-05-10 11:59:16 +02:00
Benjamin Canac
3df917ae70 chore(deps): fix vue-tsc version to 1.6.3 2023-05-10 00:22:10 +02:00
Benjamin Canac
19a34c44da chore(plugins): move hexToRgb fn to utils 2023-05-10 00:19:27 +02:00
Benjamin Canac
365c843fc0 chore(SelectMenu): put back required input 2023-05-10 00:18:14 +02:00
Benjamin Canac
cd430a4cad fix(Icon): missing import 2023-05-10 00:17:20 +02:00
Benjamin Canac
32ada0b28b chore(deps): bump @nuxtjs/tailwindcss 2023-05-09 18:55:28 +02:00
Benjamin Canac
034a95d3c9 fix(VerticalNavigation): improve focus 2023-05-09 18:55:15 +02:00
Benjamin Canac
530d8a8c27 chore(Dropdown): improve preset 2023-05-09 17:58:07 +02:00
Benjamin Canac
939efba47c fix(app.config): remove old u- classes 2023-05-09 16:26:12 +02:00
Benjamin Canac
c43c212ae1 chore(Toggle): shrink size and move focus to focus-visible 2023-05-09 16:25:09 +02:00
Benjamin Canac
0404c871fb chore(Radio/Checkbox): change focus to focus-visible 2023-05-09 16:24:36 +02:00
Benjamin Canac
ebf5fd6aeb fix(Avatar): shrink chip ring 2023-05-09 16:23:51 +02:00
Benjamin Canac
d5d250b8cf chore(Badge): default size to sm and add lg 2023-05-09 14:30:43 +02:00
Benjamin Canac
410d2351d6 chore(Avatar): default size to sm 2023-05-09 14:30:43 +02:00
Benjamin Canac
949a476125 docs: add fallback on colorMode button 2023-05-09 14:30:43 +02:00
Benjamin Canac
4586eed90c chore(Kbd): new component 2023-05-09 14:30:43 +02:00
Benjamin Canac
b21c55f5c4 chore(Dropdown): move resolve class logic into template 2023-05-09 14:30:43 +02:00
Benjamin Canac
fc11612a49 chore(SelectMenu): move resolve class logic into template 2023-05-09 14:30:05 +02:00
Benjamin Canac
6de57aa1a0 chore(deps): bump 2023-05-09 11:50:28 +02:00
Benjamin Canac
6355b16156 chore(Button): add flex-shrink-0 by default 2023-05-05 18:17:43 +02:00
Benjamin Canac
26fc923ea4 chore(Input): add placeholder color to none appearance 2023-05-05 18:17:32 +02:00
Benjamin Canac
28ee9179f5 fix(VerticalNavigation): improve stacking context 2023-05-05 18:17:13 +02:00
Benjamin Canac
4665563e6f fix(CommandPalette): wrong type usage 2023-05-05 17:19:39 +02:00
Benjamin Canac
f221b890a9 chore(Card): update ring and divide colors 2023-05-05 17:17:46 +02:00
Benjamin Canac
d1d8ab3c64 fix(Button): variant validator takes color into account 2023-05-05 14:38:03 +02:00
Benjamin Canac
5b8ab168ba chore(app.config): remove commented button gray variant 2023-05-05 12:16:18 +02:00
Benjamin Canac
767a2bf3fc chore(app.config): improve modal and slideover overlay background 2023-05-05 12:15:59 +02:00
Benjamin Canac
0c69385771 fix: prefix imported components 2023-05-05 12:15:20 +02:00
Daniel Roe
97b1a85ea1 fix: revert back to runtime app for hmr (#153) 2023-05-04 18:18:36 +02:00
Benjamin Canac
9ce43ac68b fix(Notifications): missing computed from vue 2023-05-04 17:59:10 +02:00
Benjamin Canac
fa05653f23 fix(Select): move types from template 2023-05-04 17:51:23 +02:00
Benjamin Canac
626409e101 fix: put back app.config for hmr 2023-05-04 17:51:08 +02:00
Daniel Roe
f5c0030a19 fix: remove augmentation of app (#152) 2023-05-04 17:32:25 +02:00
Daniel Roe
11e00a10e4 fix: update to fix type issues (#151) 2023-05-04 17:24:22 +02:00
Benjamin Canac
b55a7c58f6 docs: improve installation description 2023-05-04 16:02:26 +02:00
Benjamin Canac
8c8bc0b751 docs: hide unfinished pages 2023-05-04 16:00:20 +02:00
Benjamin Canac
a076cae4bf fix(module): remove .ts ext from app.config 2023-05-04 15:38:32 +02:00
Benjamin Canac
5facfee76c Revert "chore(package): volta pin node 18"
This reverts commit a38ef00fb8.
2023-05-04 14:59:45 +02:00
Benjamin Canac
3c5256c462 chore(release): 2.0.0 2023-05-04 14:56:33 +02:00
Benjamin Canac
a38ef00fb8 chore(package): volta pin node 18 2023-05-04 14:55:18 +02:00
Benjamin Canac
6da0db0113 feat: rewrite to use app config and rework docs (#143)
Co-authored-by: Daniel Roe <daniel@roe.dev>
Co-authored-by: Sébastien Chopin <seb@nuxt.com>
2023-05-04 14:49:19 +02:00
Benjamin Canac
56230ea915 chore(release): 1.2.11 2023-05-04 11:01:28 +02:00
Benjamin Canac
126b5fcfd4 Revert "chore(github): add v1 branch to ci dev"
This reverts commit d3536d8768.
2023-05-04 11:00:12 +02:00
Benjamin Canac
d3536d8768 chore(github): add v1 branch to ci dev 2023-05-04 10:52:54 +02:00
Sylvain Marroufin
59f62d322f fix(defineShortcuts): use useEventListener (#150) 2023-05-04 10:50:10 +02:00
Sylvain Marroufin
b85a8e7203 chore(defineShortcuts): config prop whenever more flexible (#149) 2023-05-02 14:47:31 +02:00
Benjamin Canac
8830d848fd chore(release): 1.2.10 2023-04-07 19:23:36 +02:00
Benjamin Canac
cfce1524b2 fix(CommandPalette): typecheck 2023-04-07 19:23:25 +02:00
Benjamin Canac
f845e89a76 chore(release): 1.2.9 2023-04-07 19:21:55 +02:00
Benjamin Canac
d9ca5d188a chore(CommandPalette): improve command highlight 2023-04-07 18:30:25 +02:00
Benjamin Canac
2429bcf5a7 chore(SelectCustom): right padding only when selected 2023-04-05 13:01:16 +02:00
Benjamin Canac
f45f4a3e56 chore(release): 1.2.8 2023-04-04 15:14:06 +02:00
Benjamin Canac
09e957e702 chore(deps): remove @tailwindcss/line-clamp as its included by default in tailwind 3.3 2023-04-04 13:55:38 +02:00
Benjamin Canac
1ecd7cefde chore(release): 1.2.7 2023-04-04 13:36:43 +02:00
Benjamin Canac
aafdfdb59c fix(useTimer): remaining after pause 2023-04-04 13:36:24 +02:00
Benjamin Canac
453ff6ca20 chore(release): 1.2.6 2023-04-04 11:26:00 +02:00
Benjamin Canac
55832b6b99 docs: ts ignore 2023-04-04 11:25:33 +02:00
Benjamin Canac
6b93bbe5cd chore(release): 1.2.5 2023-04-04 11:17:30 +02:00
Benjamin Canac
1402553145 chore(deps): bump 2023-04-04 11:17:14 +02:00
Benjamin Canac
5d84dfd05b chore(release): 1.2.4 2023-04-04 11:08:58 +02:00
Benjamin Canac
7dc59a05ec chore(useTimer): pass options to useTimestamp 2023-04-04 11:08:41 +02:00
Benjamin Canac
4bd994985d chore(release): 1.2.3 2023-03-22 16:21:49 +01:00
Benjamin Canac
c83d3b7147 chore(Avatar): remove useless chipVariant prop 2023-03-22 16:21:29 +01:00
Benjamin Canac
f022665351 chore(release): 1.2.2 2023-03-20 16:17:55 +01:00
Benjamin Canac
f29c325dc7 chore(deps): fix @headlessui/vue to 1.7.10 because of inert dialogs 2023-03-20 16:17:36 +01:00
Benjamin Canac
876f9578c2 chore(release): 1.2.1 2023-03-20 15:59:15 +01:00
Benjamin Canac
f69f584188 chore(deps): bump 2023-03-20 15:58:53 +01:00
Sylvain Marroufin
377b4189ca fix(defineShortcuts): shift + alphabetic character handling (#140) 2023-03-13 14:11:52 +01:00
Benjamin Canac
f76a9f0ab0 chore(release): 1.2.0 2023-03-09 16:55:06 +01:00
Sylvain Marroufin
37b2271bf0 fix(defineShortcuts): add missing import 2023-03-09 16:09:31 +01:00
Sylvain Marroufin
fa49d52f17 fix(Tooltip): shortcutsClass prop type 2023-03-09 15:37:42 +01:00
Sylvain Marroufin
fd4b608150 chore: add more composables (#138) 2023-03-09 11:42:22 +01:00
Sylvain Marroufin
fef93f3198 chore: allow preset override of components shortcuts (#139)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-03-09 11:40:04 +01:00
Benjamin Canac
0826ef8d59 chore(deps): bump 2023-03-03 16:51:41 +01:00
Benjamin Canac
0e3066d865 chore(release): 1.1.4 2023-03-02 12:37:17 +01:00
Benjamin Canac
fb9d6cb544 chore(deps): freeze @headlessui/vue to 1.7.10 2023-03-02 12:36:55 +01:00
Benjamin Canac
531a89cdb8 chore(release): 1.1.3 2023-03-02 11:02:04 +01:00
Benjamin Canac
6970c2d665 chore(CommandPalette): add slot for command icon 2023-02-28 16:49:50 +01:00
150 changed files with 11397 additions and 8209 deletions

View File

@@ -15,10 +15,10 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest] # macos-latest, windows-latest
node: [16]
node: [18]
steps:
- uses: actions/setup-node@v2
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
@@ -29,7 +29,7 @@ jobs:
fetch-depth: 0
- name: Cache
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: node_modules
key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}

View File

@@ -15,10 +15,10 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest] # macos-latest, windows-latest
node: [16]
node: [18]
steps:
- uses: actions/setup-node@v2
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
@@ -29,7 +29,7 @@ jobs:
fetch-depth: 0
- name: Cache
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: node_modules
key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}

View File

@@ -2,6 +2,101 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [2.0.2](https://github.com/nuxtlabs/ui/compare/v2.0.1...v2.0.2) (2023-05-11)
### Bug Fixes
* **LinkCustom:** handle `button` when no `to` prop ([c7c78cb](https://github.com/nuxtlabs/ui/commit/c7c78cb47b00963c8a9ea0c0599fbc7e128cff66))
### [2.0.1](https://github.com/nuxtlabs/ui/compare/v2.0.0...v2.0.1) (2023-05-11)
### Bug Fixes
* **app.config:** remove old `u-` classes ([939efba](https://github.com/nuxtlabs/ui/commit/939efba47ceb660e5448a3ea42f2acd71b9837ee))
* **Avatar:** `gray` missing for `chipColor` ([fd4c80a](https://github.com/nuxtlabs/ui/commit/fd4c80acd4c70c7d378ebf780cd843115d8f434d))
* **Avatar:** shrink chip ring ([ebf5fd6](https://github.com/nuxtlabs/ui/commit/ebf5fd6aeb2a5363e80457cf8245fbab5fbc17ca))
* **Button:** `variant` validator takes color into account ([d1d8ab3](https://github.com/nuxtlabs/ui/commit/d1d8ab3c647d50f37832d1ae531550944d5aa8e3))
* **colors:** missing `useNuxtApp` import ([76a0d61](https://github.com/nuxtlabs/ui/commit/76a0d61a0f7b3936b0eceff16e17bc6540fb946c))
* **CommandPalette:** expose input ref to template ([192bf4c](https://github.com/nuxtlabs/ui/commit/192bf4c375293b16d952b94cc098a0260f47996a))
* **CommandPalette:** put back searchable on `v-show` to input ref always exists ([aacb7e9](https://github.com/nuxtlabs/ui/commit/aacb7e98412d2973c6fc61d9cb3b6da9bd433eb0))
* **CommandPalette:** wrong type usage ([4665563](https://github.com/nuxtlabs/ui/commit/4665563e6f9c4054cb1c859991369fe2cc844047))
* **docs:** sticky search button `z-index` ([f48ead6](https://github.com/nuxtlabs/ui/commit/f48ead6faf6fd14deeff84ca7b25d6bb7fae6f12))
* **Icon:** missing import ([cd430a4](https://github.com/nuxtlabs/ui/commit/cd430a4cad5143c5bd45c003086091f769e4f015))
* **module:** remove `.ts` ext from app.config ([a076cae](https://github.com/nuxtlabs/ui/commit/a076cae4bfa387e1fd9800741b10702896c21ad2))
* **Notifications:** missing `computed` from vue ([9ce43ac](https://github.com/nuxtlabs/ui/commit/9ce43ac68bcef3fb7fff8a9e317ad6d4a5ac2cb9))
* prefix imported components ([0c69385](https://github.com/nuxtlabs/ui/commit/0c69385771ff1815cdcbff812962056da381a541))
* put back app.config for hmr ([626409e](https://github.com/nuxtlabs/ui/commit/626409e1014ddcacaf6ee155830bd9862b335058))
* remove augmentation of app ([#152](https://github.com/nuxtlabs/ui/issues/152)) ([f5c0030](https://github.com/nuxtlabs/ui/commit/f5c0030a198579e5929fd517b80e2e20c9bac769))
* revert back to runtime app for hmr ([#153](https://github.com/nuxtlabs/ui/issues/153)) ([97b1a85](https://github.com/nuxtlabs/ui/commit/97b1a85ea12499289866a6baf15661c1f15279ce))
* **Select:** move types from template ([fa05653](https://github.com/nuxtlabs/ui/commit/fa05653f23c4e9b1732eb4b9cd5e034f9bdca272))
* **Toggle:** wrong `icon-off` positioning ([d5471f4](https://github.com/nuxtlabs/ui/commit/d5471f4d371b72df0ca5fac36e698066aca3864e))
* update to fix type issues ([#151](https://github.com/nuxtlabs/ui/issues/151)) ([11e00a1](https://github.com/nuxtlabs/ui/commit/11e00a10e4781881e293e5fcd382331008c15346))
* **VerticalNavigation:** improve focus ([034a95d](https://github.com/nuxtlabs/ui/commit/034a95d3c92eee9a54bd266e02d7446f7792d051))
* **VerticalNavigation:** improve stacking context ([28ee917](https://github.com/nuxtlabs/ui/commit/28ee9179f5fbc006a47719ee632adf54f0e0ec4d))
## [2.0.0](https://github.com/nuxtlabs/ui/compare/v1.2.11...v2.0.0) (2023-05-04)
### Features
* rewrite to use app config and rework docs ([#143](https://github.com/nuxtlabs/ui/issues/143)) ([6da0db0](https://github.com/nuxtlabs/ui/commit/6da0db0113733df1a03220cb528bea862b553f37))
### [1.2.11](https://github.com/nuxtlabs/ui/compare/v1.2.10...v1.2.11) (2023-05-04)
### Bug Fixes
* **defineShortcuts:** use `useEventListener` ([#150](https://github.com/nuxtlabs/ui/issues/150)) ([59f62d3](https://github.com/nuxtlabs/ui/commit/59f62d322f07919d16a8d35340c3aa038cd09520))
### [1.2.10](https://github.com/nuxtlabs/ui/compare/v1.2.9...v1.2.10) (2023-04-07)
### Bug Fixes
* **CommandPalette:** typecheck ([cfce152](https://github.com/nuxtlabs/ui/commit/cfce1524b209212d9ce635b61376ff0d6bc3601b))
### [1.2.9](https://github.com/nuxtlabs/ui/compare/v1.2.8...v1.2.9) (2023-04-07)
### [1.2.8](https://github.com/nuxtlabs/ui/compare/v1.2.7...v1.2.8) (2023-04-04)
### [1.2.7](https://github.com/nuxtlabs/ui/compare/v1.2.6...v1.2.7) (2023-04-04)
### Bug Fixes
* **useTimer:** remaining after pause ([aafdfdb](https://github.com/nuxtlabs/ui/commit/aafdfdb59c365c542f93703dd52b4306ac935040))
### [1.2.6](https://github.com/nuxtlabs/ui/compare/v1.2.5...v1.2.6) (2023-04-04)
### [1.2.5](https://github.com/nuxtlabs/ui/compare/v1.2.4...v1.2.5) (2023-04-04)
### [1.2.4](https://github.com/nuxtlabs/ui/compare/v1.2.3...v1.2.4) (2023-04-04)
### [1.2.3](https://github.com/nuxtlabs/ui/compare/v1.2.2...v1.2.3) (2023-03-22)
### [1.2.2](https://github.com/nuxtlabs/ui/compare/v1.2.1...v1.2.2) (2023-03-20)
### [1.2.1](https://github.com/nuxtlabs/ui/compare/v1.2.0...v1.2.1) (2023-03-20)
### Bug Fixes
* **defineShortcuts:** shift + alphabetic character handling ([#140](https://github.com/nuxtlabs/ui/issues/140)) ([377b418](https://github.com/nuxtlabs/ui/commit/377b4189ca85603db0b7f040949260ba7494c46f))
## [1.2.0](https://github.com/nuxtlabs/ui/compare/v1.1.4...v1.2.0) (2023-03-09)
### Bug Fixes
* **defineShortcuts:** add missing import ([37b2271](https://github.com/nuxtlabs/ui/commit/37b2271bf04adfe6bee4d984fa12452b2168318c))
* **Tooltip:** `shortcutsClass` prop type ([fa49d52](https://github.com/nuxtlabs/ui/commit/fa49d52f17752eaa06f997a9b6e8df8adcab983f))
### [1.1.4](https://github.com/nuxtlabs/ui/compare/v1.1.3...v1.1.4) (2023-03-02)
### [1.1.3](https://github.com/nuxtlabs/ui/compare/v1.1.2...v1.1.3) (2023-03-02)
### [1.1.2](https://github.com/nuxtlabs/ui/compare/v1.1.1...v1.1.2) (2023-02-28)

View File

@@ -1,6 +1,6 @@
# @nuxthq/ui
Components library as a Nuxt module using [TailwindCSS](https://tailwindcss.com).
Components library as a Nuxt module using [TailwindCSS](https://tailwindcss.com) and [HeadlessUI](https://headlessui.com).
## Installation
@@ -29,43 +29,3 @@ If you want latest updates, please use `@nuxthq/ui-edge` in your `package.json`:
}
}
```
## Options
- `primary`
Define the primary variant. Defaults to `indigo`. You can specify your own object of colors like here:
**Example:**
```js
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
buildModules: [
'@nuxthq/ui'
],
ui: {
primary: 'blue'
}
})
```
- `prefix`
Define the prefix of the imported components. Defaults to `u`.
**Example:**
```js
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
buildModules: [
'@nuxthq/ui'
],
ui: {
prefix: 'tw'
}
})
```

View File

@@ -1,12 +0,0 @@
import fs from 'node:fs/promises'
import { join, resolve } from 'node:path'
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
hooks: {
'rollup:done': async (ctx) => {
// copy env.d.ts to dist
await fs.copyFile(resolve('src/env.d.ts'), join(ctx.options.outDir, 'env.d.ts'))
}
}
})

View File

@@ -1,95 +1,57 @@
<template>
<div>
<nav class="u-bg-white border-b u-border-gray-200 fixed top-0 inset-x-0 z-10">
<UContainer padded>
<div class="flex items-center justify-between h-16">
<div class="flex items-center">
<NuxtLink to="/" class="block font-bold text-lg u-text-gray-900">
@nuxthq/ui
</NuxtLink>
</div>
<Header />
<div class="flex items-center">
<ColorScheme placeholder="" tag="span">
<UButton variant="transparent" :icon="colorMode.value === 'dark' ? 'i-heroicons-moon' : 'i-heroicons-sun'" @click="toggleDark" />
</ColorScheme>
<UButton to="https://github.com/nuxtlabs/ui" target="_blank" variant="transparent" icon="i-mdi-github" />
</div>
</div>
</UContainer>
</nav>
<UContainer>
<div class="relative grid lg:grid-cols-10 lg:gap-8">
<DocsAside class="lg:col-span-2" />
<UContainer class="mt-16">
<div class="lg:grid lg:grid-cols-10 lg:gap-10 lg:relative">
<aside class="lg:flex lg:flex-col pb-8 lg:pb-0 lg:sticky lg:top-0 px-4 sm:px-6 lg:px-0 lg:pt-16 lg:-mt-16 lg:self-start lg:col-span-2 lg:overflow-hidden lg:h-screen">
<nav class="overflow-y-auto h-auto pt-8 lg:py-12">
<ul class="space-y-6">
<li v-for="section of sections" :key="section">
<h5 class="mb-3 uppercase tracking-wide font-semibold text-xs u-text-gray-900">
{{ section.label }}
</h5>
<ul class="space-y-1.5">
<li v-for="(link, index) of section.links" :key="index">
<ULink
:to="link.to"
class="relative block text-sm rounded-md"
active-class="text-primary-600"
inactive-class="u-text-gray-500 hover:u-text-gray-700"
exact
>
{{ link.label }}
</ULink>
</li>
</ul>
</li>
</ul>
</nav>
</aside>
<div class="relative lg:col-span-6 pt-8 pb-16">
<DocsPageHeader />
<div class="space-y-6 sm:px-6 lg:px-0 lg:col-span-8 lg:py-12">
<NuxtPage />
<hr class="border-gray-200 dark:border-gray-800 my-12">
<DocsPrevNext />
</div>
<DocsToc class="lg:col-span-2 order-first lg:order-last" />
</div>
</UContainer>
<ClientOnly>
<UNotifications />
</ClientOnly>
<DocsSearch />
<UNotifications />
</div>
</template>
<script setup>
<script setup lang="ts">
const colorScheme = usePreferredColorScheme()
const colorMode = useColorMode()
// Computed
const href = computed(() => colorScheme.value === 'dark' ? '/icon-dark.svg' : '/icon-light.svg')
const color = computed(() => colorMode.value === 'dark' ? '#18181b' : 'white')
// Head
useHead({
title: '@nuxthq/ui',
titleTemplate: title => title && title !== 'nuxthq/ui' ? `${title} - nuxthq/ui` : 'nuxthq/ui',
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' }
{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' },
{ key: 'theme-color', name: 'theme-color', content: color }
],
link: [
{ rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' }
{ rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' },
{ rel: 'icon', type: 'image/svg+xml', href }
],
htmlAttrs: {
lang: 'en'
},
bodyAttrs: {
class: 'antialiased font-sans text-gray-700 bg-gray-50 dark:bg-gray-900 dark:text-gray-200 bg-white dark:bg-black'
class: 'antialiased font-sans text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-900'
}
})
const colorMode = useColorMode()
const toggleDark = () => {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
const sections = [
{ label: 'Getting Started', links: [{ label: 'Usage', to: '/' }, { label: 'Examples', to: '/examples' }, { label: 'Migration', to: '/migration' }, { label: 'Dark mode', to: '/dark' }] },
{ label: 'Elements', links: [{ label: 'Avatar', to: '/components/Avatar' }, { label: 'AvatarGroup', to: '/components/AvatarGroup' }, { label: 'Badge', to: '/components/Badge' }, { label: 'Button', to: '/components/Button' }, { label: 'Dropdown', to: '/components/Dropdown' }] },
{ label: 'Feedback', links: [{ label: 'Alert', to: '/components/Alert' }, { label: 'AlertDialog', to: '/components/AlertDialog' }] },
{ label: 'Forms', links: [{ label: 'Checkbox', to: '/components/Checkbox' }, { label: 'Input', to: '/components/Input' }, { label: 'FormGroup', to: '/components/FormGroup' }, { label: 'Radio', to: '/components/Radio' }, { label: 'Select', to: '/components/Select' }, { label: 'SelectCustom', to: '/components/SelectCustom' }, { label: 'Textarea', to: '/components/Textarea' }, { label: 'Toggle', to: '/components/Toggle' }] },
{ label: 'Layout', links: [{ label: 'Card', to: '/components/Card' }, { label: 'Container', to: '/components/Container' }] },
{ label: 'Navigation', links: [{ label: 'Pills', to: '/components/Pills' }, { label: 'Tabs', to: '/components/Tabs' }, { label: 'VerticalNavigation', to: '/components/VerticalNavigation' }, { label: 'CommandPalette', to: '/components/CommandPalette' }] },
{ label: 'Overlays', links: [{ label: 'ContextMenu', to: '/components/ContextMenu' }, { label: 'Modal', to: '/components/Modal' }, { label: 'Notification', to: '/components/Notification' }, { label: 'Popover', to: '/components/Popover' }, { label: 'Slideover', to: '/components/Slideover' }, { label: 'Tooltip', to: '/components/Tooltip' }] }
]
</script>
<style>
html.dark {
@apply bg-black;
}
</style>

View File

@@ -0,0 +1,48 @@
import type { RouterConfig } from '@nuxt/schema'
function findHashPosition (hash): { el: any, behavior: ScrollBehavior, top: number } {
const el = document.querySelector(hash)
// vue-router does not incorporate scroll-margin-top on its own.
if (el) {
const top = parseFloat(getComputedStyle(el).scrollMarginTop)
return {
el: hash,
behavior: 'smooth',
top
}
}
}
// https://router.vuejs.org/api/#routeroptions
export default <RouterConfig>{
scrollBehavior (to, from, savedPosition) {
const nuxtApp = useNuxtApp()
// If history back
if (savedPosition) {
// Handle Suspense resolution
return new Promise((resolve) => {
nuxtApp.hooks.hookOnce('page:finish', () => {
setTimeout(() => resolve(savedPosition), 50)
})
})
}
// Scroll to heading on click
if (to.hash) {
return new Promise((resolve) => {
if (to.path === from.path) {
setTimeout(() => resolve(findHashPosition(to.hash)), 50)
} else {
nuxtApp.hooks.hookOnce('page:finish', () => {
setTimeout(() => resolve(findHashPosition(to.hash)), 50)
})
}
})
}
// Scroll to top of window
return { top: 0 }
}
}

117
docs/components/Header.vue Normal file
View File

@@ -0,0 +1,117 @@
<template>
<header class="sticky top-0 z-50 w-full backdrop-blur flex-none border-b border-gray-900/10 dark:border-gray-50/[0.06] bg-white/75 dark:bg-gray-900/75">
<UContainer>
<div class="flex items-center justify-between h-16">
<div class="flex items-center gap-3">
<NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
nuxthq/ui
</NuxtLink>
</div>
<div class="flex items-center -mr-1.5">
<div class="mr-1.5 hidden lg:block">
<ThemeSelect />
</div>
<UButton
color="gray"
variant="ghost"
class="lg:hidden"
icon="i-heroicons-magnifying-glass-20-solid"
@click="openDocsSearch"
/>
<ClientOnly>
<UButton
:icon="isDark ? 'i-heroicons-moon' : 'i-heroicons-sun'"
color="gray"
variant="ghost"
aria-label="Theme"
@click="isDark = !isDark"
/>
<template #fallback>
<div class="w-8 h-8" />
</template>
</ClientOnly>
<UButton
to="https://github.com/nuxtlabs/ui"
target="_blank"
color="gray"
variant="ghost"
icon="i-simple-icons-github"
/>
<UButton
color="gray"
variant="ghost"
class="lg:hidden"
icon="i-heroicons-bars-3-20-solid"
@click="isDialogOpen = true"
/>
</div>
</div>
</UContainer>
<TransitionRoot :show="isDialogOpen" as="template">
<Dialog as="div" @close="isDialogOpen = false">
<DialogPanel class="fixed inset-0 z-50 overflow-y-auto bg-white dark:bg-gray-900 lg:hidden">
<div class="px-4 sm:px-6 sticky top-0 border-b border-gray-900/10 dark:border-gray-50/[0.06] bg-white/75 dark:bg-gray-900/75 backdrop-blur z-10">
<div class="flex items-center justify-between h-16">
<div class="flex items-center gap-3">
<NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
nuxthq/ui
</NuxtLink>
</div>
<div class="flex -mr-1.5">
<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
@click="isDialogOpen = false"
/>
</div>
</div>
</div>
<div class="px-4 sm:px-6 py-4 sm:py-6">
<ThemeSelect class="mb-4 sm:mb-6 w-full" />
<DocsAsideLinks @click="isDialogOpen = false" />
</div>
</DialogPanel>
</Dialog>
</TransitionRoot>
</header>
</template>
<script setup lang="ts">
import { Dialog, DialogPanel, TransitionRoot } from '@headlessui/vue'
const { isSearchModalOpen } = useDocs()
const colorMode = useColorMode()
const isDialogOpen = ref(false)
const isDark = computed({
get () {
return colorMode.value === 'dark'
},
set () {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
function openDocsSearch () {
isDialogOpen.value = false
setTimeout(() => {
isSearchModalOpen.value = true
}, 100)
}
</script>

5
docs/components/Logo.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="currentColor" />
</svg>
</template>

View File

@@ -0,0 +1,87 @@
<template>
<div class="flex items-center shadow-sm">
<USelectMenu
v-model="primary"
name="primary"
class="w-full [&>div>button]:!rounded-r-none"
appearance="gray"
:ui="{ width: 'w-[194px]' }"
:popper="{ placement: 'bottom-start' }"
:options="primaryOptions"
>
<template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${primary.hex}`}" />
{{ primary.text }}
</template>
<template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
{{ option.text }}
</template>
</USelectMenu>
<USelectMenu
v-model="gray"
name="gray"
class="w-full [&>div>button]:!rounded-l-none [&>div>button]:-ml-px"
appearance="gray"
:ui="{ width: 'w-[194px]' }"
:popper="{ placement: 'bottom-end' }"
:options="grayOptions"
>
<template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${gray.hex}`}" />
{{ gray.text }}
</template>
<template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
{{ option.text }}
</template>
</USelectMenu>
</div>
</template>
<script setup lang="ts">
import colors from '#tailwind-config/theme/colors'
const appConfig = useAppConfig()
const colorMode = useColorMode()
const primaryCookie = useCookie('primary', { path: '/', default: () => appConfig.ui.primary })
const grayCookie = useCookie('gray', { path: '/', default: () => appConfig.ui.gray })
watch(primaryCookie, (primary) => {
appConfig.ui.primary = primary
}, { immediate: true })
watch(grayCookie, (gray) => {
appConfig.ui.gray = gray
}, { immediate: true })
// Computed
const primaryOptions = computed(() => useWithout(appConfig.ui.colors, 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const primary = computed({
get () {
return primaryOptions.value.find(option => option.value === primaryCookie.value)
},
set (option) {
primaryCookie.value = option.value
}
})
const grayOptions = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const gray = computed({
get () {
return grayOptions.value.find(option => option.value === grayCookie.value)
},
set (option) {
grayCookie.value = option.value
}
})
</script>

View File

@@ -0,0 +1,33 @@
<template>
<component
:is="to ? NuxtLink : 'div'"
:to="to"
class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-200 text-sm leading-6 my-5 last:mb-0 font-normal group relative prose-code:bg-gray-200 dark:prose-code:bg-gray-800"
:class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed' : '']"
>
<UIcon v-if="!!to" name="i-heroicons-link-20-solid" class="w-3 h-3 absolute right-2 top-2 text-gray-400 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400" />
<UIcon v-if="icon" :name="icon" class="w-4 h-4 mr-2 inline-flex items-center align-text-top" :class="color" />
<ContentSlot :use="$slots.default" unwrap="p" />
</component>
</template>
<script setup lang="ts">
const NuxtLink = resolveComponent('NuxtLink')
defineProps({
icon: {
type: String,
default: null
},
color: {
type: String,
default: 'text-primary-500 dark:text-primary-400'
},
to: {
type: String,
default: null
}
})
</script>

View File

@@ -0,0 +1,53 @@
<template>
<div :selected-index="selectedIndex" @change="changeTab">
<div class="flex border border-gray-200 dark:border-gray-700 border-b-0 rounded-t-md overflow-hidden -mb-px">
<div
v-for="(tab, index) in tabs"
:key="index"
as="template"
@click="selectedIndex = index"
>
<button
class="px-4 py-2 focus:outline-none text-sm border-r border-r-gray-200 dark:border-r-gray-700 transition-colors"
tabindex="-1"
:class="[selectedIndex === index ? 'font-medium text-primary-500 dark:text-primary-400 bg-gray-50 dark:bg-gray-800' : 'hover:bg-gray-50 dark:hover:bg-gray-800']"
>
{{ tab.label }}
</button>
</div>
</div>
<div class="[&>div>pre]:!rounded-t-none">
<component :is="selectedTab.component" />
</div>
</div>
</template>
<script setup lang="ts">
const slots = useSlots()
const selectedIndex = ref(0)
// Computed
const tabs = computed(() => slots.default?.().map((slot, index) => {
return {
label: slot.props?.filename || slot.props?.label || `${index}`,
component: slot
}
}) || [])
const selectedTab = computed(() => tabs.value.find((_, index) => index === selectedIndex.value))
// Methods
function changeTab (index) {
selectedIndex.value = index
}
</script>
<script lang="ts">
export default {
inheritAttrs: false
}
</script>

View File

@@ -0,0 +1,184 @@
<template>
<div>
<div v-if="propsToSelect.length" class="relative flex border border-gray-200 dark:border-gray-700 rounded-t-md overflow-hidden not-prose">
<div v-for="prop in propsToSelect" :key="prop.name" class="flex flex-col gap-0.5 justify-between py-1.5 font-medium bg-gray-50 dark:bg-gray-800 border-r border-r-gray-200 dark:border-r-gray-700">
<label :for="prop.name" class="block text-xs px-3 font-medium text-gray-400 dark:text-gray-500 -my-px">{{ prop.label }}</label>
<UCheckbox
v-if="prop.type === 'boolean'"
v-model="componentProps[prop.name]"
:name="prop.name"
appearance="none"
class="justify-center"
/>
<USelectMenu
v-else-if="prop.type === 'string' && prop.options.length"
v-model="componentProps[prop.name]"
:options="prop.options"
:name="prop.name"
:label="componentProps[prop.name]"
appearance="none"
class="inline-flex"
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md' }"
:ui-select="{ custom: '!py-0' }"
:popper="{ strategy: 'fixed', placement: 'bottom-start' }"
/>
<UInput
v-else
:model-value="componentProps[prop.name]"
:type="prop.type === 'number' ? 'number' : 'text'"
:name="prop.name"
appearance="none"
autocomplete="off"
:ui="{ custom: '!py-0' }"
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
/>
</div>
</div>
<div class="flex border border-b-0 border-gray-200 dark:border-gray-700 relative not-prose" :class="[{ 'p-4': padding }, propsToSelect.length ? 'border-t-0' : 'rounded-t-md', backgroundClass]">
<component :is="name" v-model="vModel" v-bind="fullProps">
<ContentSlot v-if="$slots.default" :use="$slots.default" />
</component>
</div>
<ContentRenderer :value="ast" class="[&>div>pre]:!rounded-t-none" />
</div>
</template>
<script setup lang="ts">
// @ts-expect-error
import { transformContent } from '@nuxt/content/transformers'
const props = defineProps({
slug: {
type: String,
default: null
},
padding: {
type: Boolean,
default: true
},
props: {
type: Object,
default: () => ({})
},
code: {
type: String,
default: null
},
baseProps: {
type: Object,
default: () => ({})
},
ui: {
type: Object,
default: () => ({})
},
excludedProps: {
type: Array,
default: () => []
},
backgroundClass: {
type: String,
default: 'bg-white dark:bg-gray-900'
}
})
const baseProps = reactive({ ...props.baseProps })
const componentProps = reactive({ ...props.props })
const appConfig = useAppConfig()
const route = useRoute()
const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`
const meta = await fetchComponentMeta(name)
// Computed
const ui = computed(() => ({ ...appConfig.ui[camelName], ...props.ui }))
const fullProps = computed(() => ({ ...props.baseProps, ...componentProps }))
const vModel = computed({
get: () => baseProps.modelValue,
set: (value) => {
baseProps.modelValue = value
}
})
const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
if (props.excludedProps.includes(key)) {
return null
}
const prop = meta?.meta?.props?.find((prop: any) => prop.name === key)
const dottedKey = useKebabCase(key).replaceAll('-', '.')
const keys = useGet(ui.value, dottedKey, {})
let options = typeof keys === 'object' && Object.keys(keys)
if (key.toLowerCase().endsWith('color')) {
options = appConfig.ui.colors
}
return {
type: prop?.type || 'string',
name: key,
label: key === 'modelValue' ? 'value' : useCamelCase(key),
options
}
}).filter(Boolean))
const code = computed(() => {
let code = `\`\`\`html
<${name}`
for (const [key, value] of Object.entries(componentProps)) {
if (value === 'undefined' || value === null) {
continue
}
const prop = meta?.meta?.props?.find((prop: any) => prop.name === key)
code += ` ${(prop?.type === 'boolean' && value !== true) || typeof value === 'object' ? ':' : ''}${key === 'modelValue' ? 'value' : useKebabCase(key)}${prop?.type === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}`
}
if (props.code) {
const lineBreaks = (props.code.match(/\n/g) || []).length
if (lineBreaks > 1) {
code += `>
${props.code}</${name}>`
} else {
code += `>${props.code}</${name}>`
}
} else {
code += ' />'
}
code += `
\`\`\`
`
return code
})
function renderObject (obj: any) {
if (Array.isArray(obj)) {
return `[${obj.map(renderObject).join(', ')}]`
}
if (typeof obj === 'object') {
return `{ ${Object.entries(obj).map(([key, value]) => `${key}: ${renderObject(value)}`).join(', ')} }`
}
if (typeof obj === 'string') {
return `'${obj}'`
}
return obj
}
const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify(componentProps)}`, () => transformContent('content:_markdown.md', code.value, {
highlight: {
theme: {
light: 'material-lighter',
dark: 'material-palenight'
}
}
}), { watch: [code] })
</script>

View File

@@ -0,0 +1,22 @@
<template>
<div class="[&>div>pre]:!rounded-t-none">
<div class="flex border border-gray-200 dark:border-gray-700 relative not-prose rounded-t-md" :class="[{ 'p-4': padding, 'rounded-b-md': !$slots.code, 'border-b-0': !!$slots.code }, backgroundClass]">
<ContentSlot v-if="$slots.default" :use="$slots.default" />
</div>
<ContentSlot v-if="$slots.code" :use="$slots.code" />
</div>
</template>
<script setup lang="ts">
defineProps({
padding: {
type: Boolean,
default: true
},
backgroundClass: {
type: String,
default: 'bg-white dark:bg-gray-900'
}
})
</script>

View File

@@ -0,0 +1,36 @@
<template>
<ContentRenderer :value="ast" />
</template>
<script setup lang="ts">
// @ts-expect-error
import { transformContent } from '@nuxt/content/transformers'
const props = defineProps({
slug: {
type: String,
default: null
}
})
const appConfig = useAppConfig()
const route = useRoute()
const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`
const preset = appConfig.ui[camelName]
const { data: ast } = await useAsyncData(`${name}-preset`, () => transformContent('content:_markdown.md', `
\`\`\`json
${JSON.stringify(preset, null, 2)}
\`\`\`\
`, {
highlight: {
theme: {
light: 'material-lighter',
dark: 'material-palenight'
}
}
}))
</script>

View File

@@ -0,0 +1,55 @@
<template>
<div>
<table class="table-fixed">
<thead>
<tr>
<th class="w-[25%]">
Prop
</th>
<th class="w-[50%]">
Default
</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr v-for="prop in metaProps" :key="prop.name">
<td class="relative flex-shrink-0">
<code>{{ prop.name }}</code><span v-if="prop.required" class="font-bold text-red-500 dark:text-red-400 absolute top-0 ml-1">*</span>
</td>
<td>
<code v-if="prop.default">{{ prop.default }}</code>
</td>
<td>
<a v-if="prop.name === 'ui'" href="#preset">
<code>{{ prop.type }}</code>
</a>
<code v-else class="break-all">
{{ prop.type }}
</code>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
slug: {
type: String,
default: null
}
})
const route = useRoute()
const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`
const meta = await fetchComponentMeta(name)
const metaProps = computed(() => useSortBy(meta?.meta?.props || [], [
prop => ['string', 'number', 'boolean', 'any'].indexOf(prop.type)
]))
</script>

View File

@@ -0,0 +1,34 @@
<template>
<div>
<table>
<thead>
<tr>
<th>Slot</th>
</tr>
</thead>
<tbody>
<tr v-for="slot in (meta.meta.slots as any[])" :key="slot.name">
<td class="whitespace-nowrap">
<code>{{ slot.name }}</code>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
slug: {
type: String,
default: null
}
})
const route = useRoute()
const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}`
const meta = await fetchComponentMeta(name)
</script>

View File

@@ -0,0 +1,21 @@
<template>
<div class="relative overflow-hidden rounded border border-dashed border-gray-400 dark:border-gray-500 opacity-75">
<svg class="absolute inset-0 h-full w-full stroke-gray-900/10 dark:stroke-white/10" fill="none">
<defs>
<pattern
id="pattern-5c1e4f0e-62d5-498b-8ff0-cf77bb448c8e"
x="0"
y="0"
width="10"
height="10"
patternUnits="userSpaceOnUse"
>
<path d="M-3 13 15-5M-5 5l18-18M-1 21 17 3" />
</pattern>
</defs>
<rect stroke="none" fill="url(#pattern-5c1e4f0e-62d5-498b-8ff0-cf77bb448c8e)" width="100%" height="100%" />
</svg>
<slot />
</div>
</template>

View File

@@ -0,0 +1,20 @@
<template>
<kbd class="inline-flex items-center justify-center font-sans font-semibold px-1 h-5 min-w-[20px] text-[11px] rounded !my-0 align-text-top ring-1 ring-gray-300 dark:ring-gray-700">
<ClientOnly>
{{ shortcut }}
</ClientOnly>
</kbd>
</template>
<script setup lang="ts">
const props = defineProps({
value: {
type: String,
required: true
}
})
const { metaSymbol } = useShortcuts()
const shortcut = computed(() => props.value === 'meta' ? metaSymbol.value : props.value)
</script>

View File

@@ -0,0 +1,13 @@
<template>
<UCard>
<template #header>
<Placeholder class="h-8" />
</template>
<Placeholder class="h-32" />
<template #footer>
<Placeholder class="h-8" />
</template>
</UCard>
</template>

View File

@@ -0,0 +1,27 @@
<script setup>
const people = [
{ id: 1, label: 'Wade Cooper' },
{ id: 2, label: 'Arlene Mccoy' },
{ id: 3, label: 'Devon Webb' },
{ id: 4, label: 'Tom Cook' },
{ id: 5, label: 'Tanya Fox' },
{ id: 6, label: 'Hellen Schmidt' },
{ id: 7, label: 'Caroline Schultz' },
{ id: 8, label: 'Mason Heaney' },
{ id: 9, label: 'Claudie Smitham' },
{ id: 10, label: 'Emil Schaefer' }
]
const selected = ref([people[3]])
</script>
<template>
<UCommandPalette
v-model="selected"
multiple
nullable
:autoselect="false"
:groups="[{ key: 'people', commands: people }]"
:fuse="{ resultLimit: 6, fuseOptions: { threshold: 0.1 } }"
/>
</template>

View File

@@ -0,0 +1,46 @@
<script setup>
const router = useRouter()
const commandPaletteRef = ref()
const users = [
{ id: 'benjamincanac', label: 'benjamincanac', href: 'https://github.com/benjamincanac', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' } },
{ id: 'Atinux', label: 'Atinux', href: 'https://github.com/Atinux', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' } },
{ id: 'smarroufin', label: 'smarroufin', href: 'https://github.com/smarroufin', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' } }
]
const actions = [
{ id: 'new-file', label: 'Add new file', icon: 'i-heroicons-document-plus', click: () => alert('New file'), shortcuts: ['⌘', 'N'] },
{ id: 'new-folder', label: 'Add new folder', icon: 'i-heroicons-folder-plus', click: () => alert('New folder'), shortcuts: ['⌘', 'F'] },
{ id: 'hashtag', label: 'Add hashtag', icon: 'i-heroicons-hashtag', click: () => alert('Add hashtag'), shortcuts: ['⌘', 'H'] },
{ id: 'label', label: 'Add label', icon: 'i-heroicons-tag', click: () => alert('Add label'), shortcuts: ['⌘', 'L'] }
]
const groups = computed(() => commandPaletteRef.value?.query
? [{
key: 'users',
commands: users
}]
: [{
key: 'recent',
label: 'Recent searches',
commands: users.slice(0, 1)
}, {
key: 'actions',
commands: actions
}])
function onSelect (option) {
if (option.click) {
option.click()
} else if (option.to) {
router.push(option.to)
} else if (option.href) {
window.open(option.href, '_blank')
}
}
</script>
<template>
<UCommandPalette ref="commandPaletteRef" :groups="groups" :autoselect="false" @update:model-value="onSelect" />
</template>

View File

@@ -0,0 +1,33 @@
<script setup>
const open = ref(false)
const people = [
{ id: 1, label: 'Wade Cooper' },
{ id: 2, label: 'Arlene Mccoy' },
{ id: 3, label: 'Devon Webb' },
{ id: 4, label: 'Tom Cook' },
{ id: 5, label: 'Tanya Fox' },
{ id: 6, label: 'Hellen Schmidt' },
{ id: 7, label: 'Caroline Schultz' },
{ id: 8, label: 'Mason Heaney' },
{ id: 9, label: 'Claudie Smitham' },
{ id: 10, label: 'Emil Schaefer' }
]
const selected = ref([])
</script>
<template>
<div>
<UButton label="Open" @click="open = true" />
<UModal v-model="open">
<UCommandPalette
v-model="selected"
multiple
nullable
:groups="[{ key: 'people', commands: people }]"
/>
</UModal>
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<UContainer>
<Placeholder class="h-32" />
</UContainer>
</template>

View File

@@ -0,0 +1,34 @@
<script setup>
const { x, y } = useMouse()
const isOpen = ref(false)
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
function openContextMenu () {
const top = unref(y)
const left = unref(x)
virtualElement.value.getBoundingClientRect = () => ({
width: 0,
height: 0,
top,
left
})
isOpen.value = true
}
</script>
<template>
<div class="w-full" @contextmenu.prevent="openContextMenu">
<Placeholder class="h-20 w-full flex items-center justify-center">
Right click here
</Placeholder>
<UContextMenu v-model="isOpen" :virtual-element="virtualElement">
<div class="p-4">
Menu
</div>
</UContextMenu>
</div>
</template>

View File

@@ -0,0 +1,34 @@
<script setup>
const items = [
[{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
}], [{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
shortcuts: ['E']
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid',
shortcuts: ['D']
}], [{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'i-heroicons-arrow-right-circle-20-solid'
}], [{
label: 'Delete',
icon: 'i-heroicons-trash-20-solid',
shortcuts: ['⌘', 'D']
}]
]
</script>
<template>
<UDropdown :items="items" :popper="{ placement: 'bottom-start' }">
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
</UDropdown>
</template>

View File

@@ -0,0 +1,10 @@
<script setup>
const { metaSymbol } = useShortcuts()
</script>
<template>
<div class="flex items-center gap-0.5">
<UKbd>{{ metaSymbol }}</UKbd>
<UKbd>U</UKbd>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
const open = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="open = true" />
<UModal v-model="open">
<div class="p-4">
<Placeholder class="h-48" />
</div>
</UModal>
</div>
</template>

View File

@@ -0,0 +1,23 @@
<script setup>
const open = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="open = true" />
<UModal v-model="open">
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<Placeholder class="h-8" />
</template>
<Placeholder class="h-32" />
<template #footer>
<Placeholder class="h-8" />
</template>
</UCard>
</UModal>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
const toast = useToast()
const actions = ref([{
label: 'Action 1',
click: () => alert('Action 1 clicked!')
}, {
label: 'Action 2',
click: () => alert('Action 2 clicked!')
}])
</script>
<template>
<UButton label="Show toast" @click="toast.add({ title: 'With actions', actions })" />
</template>

View File

@@ -0,0 +1,7 @@
<script setup>
const toast = useToast()
</script>
<template>
<UButton label="Show toast" @click="toast.add({ title: 'Hello world!' })" />
</template>

View File

@@ -0,0 +1,11 @@
<script setup>
const toast = useToast()
function onCallback () {
alert('Notification expired!')
}
</script>
<template>
<UButton label="Show toast" @click="toast.add({ title: 'Expires soon...', timeout: 1000, callback: onCallback })" />
</template>

View File

@@ -0,0 +1,11 @@
<script setup>
const toast = useToast()
function onClick () {
alert('Clicked!')
}
</script>
<template>
<UButton label="Show toast" @click="toast.add({ title: 'Click me', click: onClick })" />
</template>

View File

@@ -0,0 +1,11 @@
<template>
<UPopover>
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
<template #panel>
<div class="p-4">
<Placeholder class="h-20 w-48" />
</div>
</template>
</UPopover>
</template>

View File

@@ -0,0 +1,9 @@
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people" />
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[3])
</script>
<template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton>
{{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
</UButton>
</USelectMenu>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref([])
</script>
<template>
<USelectMenu v-model="selected" :options="people" multiple>
<template #label>
<span v-if="selected.length" class="font-medium truncate">{{ selected.join(', ') }}</span>
<span v-else class="block truncate text-gray-400 dark:text-gray-500">Select people</span>
</template>
</USelectMenu>
</template>

View File

@@ -0,0 +1,41 @@
<script setup>
const people = [{
id: 'benjamincanac',
label: 'benjamincanac',
href: 'https://github.com/benjamincanac',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
},
{
id: 'Atinux',
label: 'Atinux',
href: 'https://github.com/Atinux',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
},
{
id: 'smarroufin',
label: 'smarroufin',
href: 'https://github.com/smarroufin',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' }
},
{
id: 'nobody',
label: 'Nobody',
icon: 'i-heroicons-user-circle'
}]
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people">
<template #label>
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4" />
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" />
{{ selected.label }}
</template>
</USelectMenu>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
const open = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="open = true" />
<USlideover v-model="open">
<div class="p-4 h-full">
<Placeholder class="w-full h-full" />
</div>
</USlideover>
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<UTooltip text="Tooltip example" :shortcuts="['⌘', 'O']">
<UButton color="gray" label="Hover me" />
</UTooltip>
</template>

View File

@@ -0,0 +1,24 @@
<script setup>
const links = [{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
}, {
label: 'Installation',
icon: 'i-heroicons-home',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
icon: 'i-heroicons-chart-bar',
to: '/navigation/vertical-navigation'
}, {
label: 'Command Palette',
icon: 'i-heroicons-command-line',
to: '/navigation/command-palette'
}]
</script>
<template>
<UVerticalNavigation :links="links" />
</template>

View File

@@ -0,0 +1,71 @@
<script lang="ts">
import { defineComponent } from '#imports'
export default defineComponent({
props: {
code: {
type: String,
default: ''
},
language: {
type: String,
default: null
},
filename: {
type: String,
default: null
},
highlights: {
type: Array as () => number[],
default: () => []
},
meta: {
type: String,
default: null
}
},
setup (props) {
const clipboard = useCopyToClipboard({ timeout: 2000 })
const icon = ref('i-heroicons-clipboard-document')
function copy () {
clipboard.copy(props.code, { title: 'Copied to clipboard!' })
icon.value = 'i-heroicons-clipboard-document-check'
setTimeout(() => {
icon.value = 'i-heroicons-clipboard-document'
}, 2000)
}
return {
icon,
copy
}
}
})
</script>
<template>
<div class="group relative" :class="`language-${language}`">
<UButton
:icon="icon"
variant="link"
class="absolute right-3 top-3 opacity-0 group-hover:opacity-100 transition-opacity z-[1]"
size="xs"
tabindex="-1"
@click="copy"
/>
<span v-if="filename" class="text-gray-400 dark:text-gray-500 absolute right-3 bottom-3 text-sm group-hover:opacity-0 transition-opacity">{{ filename }}</span>
<slot />
</div>
</template>
<style>
pre code .line {
display: block;
min-height: 1rem;
}
</style>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
defineProps<{ id: string }>()
</script>
<template>
<h2 :id="id" class="scroll-mt-[161px] lg:scroll-mt-[112px]">
<NuxtLink :href="`#${id}`" class="group">
<div class="-ml-6 pr-2 py-2 inline-flex opacity-0 group-hover:opacity-100 transition-opacity absolute">
<UIcon name="i-heroicons-hashtag-20-solid" class="w-4 h-4 text-primary-500 dark:text-primary-400" />
</div>
<slot />
</NuxtLink>
</h2>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
defineProps<{ id: string }>()
</script>
<template>
<h3 :id="id" class="scroll-mt-[145px] lg:scroll-mt-[96px]">
<NuxtLink :href="`#${id}`" class="group">
<div class="-ml-6 pr-2 py-2 inline-flex opacity-0 group-hover:opacity-100 transition-opacity absolute">
<UIcon name="i-heroicons-hashtag-20-solid" class="w-4 h-4 text-primary-500 dark:text-primary-400" />
</div>
<slot />
</NuxtLink>
</h3>
</template>

View File

@@ -0,0 +1,76 @@
<script setup>
const commandPaletteRef = ref()
const { navigation } = useContent()
const { data: files } = await useLazyAsyncData('search', () => queryContent().where({ _type: 'markdown' }).find(), { default: () => [] })
const groups = computed(() => navigation.value.map(item => ({
key: item._path,
label: item.title,
commands: files.value.filter(file => file._path.startsWith(item._path)).map(file => ({
id: file._id,
icon: 'i-heroicons-document',
title: file.navigation?.title || file.title,
category: item.title,
to: file._path
}))
})))
const close = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-x-mark', color: 'black', variant: 'ghost', size: 'lg', padded: false }) : null)
const empty = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-magnifying-glass', queryLabel: 'No results' }) : ({ icon: '', label: 'No recent searches' }))
const ui = {
wrapper: 'flex flex-col flex-1 min-h-0 bg-gray-50 dark:bg-gray-800',
input: {
wrapper: 'relative flex items-center mx-3 py-3',
base: 'w-full rounded border-2 border-primary-500 placeholder-gray-400 dark:placeholder-gray-500 focus:border-primary-500 focus:outline-none focus:ring-0 bg-white dark:bg-gray-900',
padding: 'px-4',
height: 'h-14',
size: 'text-lg',
icon: {
base: 'pointer-events-none absolute left-3 text-primary-500 dark:text-primary-400',
size: 'h-6 w-6'
}
},
group: {
wrapper: 'p-3 relative',
label: '-mx-3 px-3 -mt-4 mb-2 py-1 text-sm font-semibold text-primary-500 dark:text-primary-400 font-semibold sticky top-0 bg-gray-50 dark:bg-gray-800 z-10',
container: 'space-y-1',
command: {
base: 'flex justify-between select-none items-center rounded px-2 py-4 gap-2 relative font-medium text-sm group shadow',
active: 'bg-primary-500 dark:bg-primary-400 text-white',
inactive: 'bg-white dark:bg-gray-900',
label: 'flex flex-col min-w-0',
suffix: 'text-xs',
icon: {
base: 'flex-shrink-0 w-6 h-6',
active: 'text-white',
inactive: 'text-gray-400 dark:text-gray-500'
}
}
},
empty: {
wrapper: 'flex flex-col items-center justify-center flex-1 py-9',
label: 'text-sm text-center text-gray-500 dark:text-gray-400',
queryLabel: 'text-lg text-center text-gray-900 dark:text-white',
icon: 'w-12 h-12 mx-auto text-gray-400 dark:text-gray-500 mb-4'
}
}
</script>
<template>
<UCommandPalette
ref="commandPaletteRef"
:groups="groups"
:ui="ui"
:close="close"
:empty="empty"
:autoselect="false"
command-attribute="title"
:fuse="{
fuseOptions: { keys: ['title', 'category'] },
}"
placeholder="Search docs"
/>
</template>

View File

@@ -0,0 +1,64 @@
<script setup>
const commandPaletteRef = ref()
const suggestions = [
{ id: 'linear', label: 'Linear', icon: 'i-simple-icons-linear' },
{ id: 'figma', label: 'Figma', icon: 'i-simple-icons-figma' },
{ id: 'slack', label: 'Slack', icon: 'i-simple-icons-slack' },
{ id: 'youtube', label: 'YouTube', icon: 'i-simple-icons-youtube' },
{ id: 'github', label: 'GitHub', icon: 'i-simple-icons-github' }
]
const commands = [
{ id: 'clipboard-history', label: 'Clipboard History', icon: 'i-heroicons-clipboard', click: () => alert('New file') },
{ id: 'import-extension', label: 'Import Extension', icon: 'i-heroicons-wrench-screwdriver', click: () => alert('New folder') },
{ id: 'manage-extensions', label: 'Manage Extensions', icon: 'i-heroicons-wrench-screwdriver', click: () => alert('Add hashtag') }
]
const groups = [{
key: 'suggestions',
label: 'Suggestions',
inactive: 'Application',
commands: suggestions
}, {
key: 'commands',
label: 'Commands',
inactive: 'Command',
commands
}]
const ui = {
wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-200 dark:divide-gray-700 bg-gray-50 dark:bg-gray-800',
container: 'relative flex-1 overflow-y-auto divide-y divide-gray-200 dark:divide-gray-700 scroll-py-2',
input: {
base: 'w-full h-14 px-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 text-gray-900 dark:text-white focus:ring-0'
},
group: {
label: 'px-2 my-2 text-xs font-semibold text-gray-500 dark:text-gray-400',
command: {
base: 'flex justify-between select-none cursor-default items-center rounded-md px-2 py-2 gap-2 relative',
active: 'bg-gray-200 dark:bg-gray-700/50 text-gray-900 dark:text-white',
container: 'flex items-center gap-3 min-w-0',
icon: {
base: 'flex-shrink-0 w-5 h-5',
active: 'text-gray-900 dark:text-white',
inactive: 'text-gray-400 dark:text-gray-500'
},
avatar: {
size: '2xs'
}
}
}
}
</script>
<template>
<UCommandPalette
ref="commandPaletteRef"
:groups="groups"
icon=""
:ui="ui"
:autoselect="false"
placeholder="Search for apps and commands"
/>
</template>

View File

@@ -0,0 +1,32 @@
<template>
<aside class="hidden pb-8 overflow-y-auto lg:block lg:self-start lg:top-16 lg:max-h-[calc(100vh-64px)] lg:sticky lg:pr-8 lg:pl-[2px]">
<div class="relative">
<div class="sticky top-0 pointer-events-none z-[1]">
<div class="h-8 bg-white dark:bg-gray-900" />
<div class="bg-white dark:bg-gray-900 relative pointer-events-auto">
<UButton
icon="i-heroicons-magnifying-glass-20-solid"
class="w-full"
color="gray"
@click="isSearchModalOpen = true"
>
Search
<div class="hidden lg:flex items-center gap-0.5 ml-auto -my-1">
<UKbd>{{ metaSymbol }}</UKbd>
<UKbd>K</UKbd>
</div>
</UButton>
</div>
<div class="h-8 bg-gradient-to-b from-white dark:from-gray-900" />
</div>
<DocsAsideLinks />
</div>
</aside>
</template>
<script setup lang="ts">
const { isSearchModalOpen } = useDocs()
const { metaSymbol } = useShortcuts()
</script>

View File

@@ -0,0 +1,31 @@
<template>
<div class="space-y-8">
<div v-for="(group, index) in navigation" :key="index" class="space-y-3">
<div class="text-sm font-semibold text-gray-900 dark:text-gray-200">
<span class="truncate">{{ group.title }}</span>
</div>
<UVerticalNavigation
:links="mapContentLinks(group.children)"
class="mt-1"
:ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
padding: 'pl-4',
base: 'group text-sm block border-l -ml-px lg:leading-6',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}"
/>
</div>
</div>
</template>
<script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
const { navigation } = useContent() as { navigation: NavItem[] }
function mapContentLinks (links: NavItem[]) {
return links?.map(link => ({ label: link.title, icon: link.icon, to: link._path })) || []
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<header v-if="page" class="relative border-b border-gray-200 dark:border-gray-800 pb-8 mb-12">
<p class="mb-4 text-sm leading-6 font-semibold text-primary-500 dark:text-primary-400 capitalize">
{{ useLowerCase(page._dir) }}
</p>
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 tracking-tight dark:text-white">
{{ page.title }}
</h1>
<div class="flex items-center gap-2 mt-4 lg:mt-0">
<UButton
v-if="page.headlessui"
:label="page.headlessui.label"
:to="page.headlessui.to"
icon="i-simple-icons-headlessui"
color="white"
/>
<UButton
v-if="page.github"
label="GitHub"
icon="i-simple-icons-github"
color="white"
:to="`https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/${page._dir}/U${page.title}.vue`"
/>
</div>
</div>
<p v-if="page.description" class="mt-4 text-lg">
{{ page.description }}
</p>
</header>
</template>
<script setup lang="ts">
const { page } = useContent()
</script>

View File

@@ -0,0 +1,11 @@
<template>
<div class="flex items-center justify-between">
<UButton v-if="prev" :label="prev.navigation?.title || prev.title" :to="prev._path" icon="i-heroicons-arrow-small-left-20-solid" color="white" />
<span v-else>&nbsp;</span>
<UButton v-if="next" :label="next.navigation?.title || next.title" :to="next._path" trailing-icon="i-heroicons-arrow-small-right-20-solid" color="white" />
</div>
</template>
<script setup lang="ts">
const { prev, next } = useContent()
</script>

View File

@@ -0,0 +1,172 @@
<template>
<UModal
v-model="isSearchModalOpen"
:ui="{
padding: 'sm:p-4',
rounded: 'sm:rounded-lg',
width: 'sm:max-w-3xl',
height: 'h-screen sm:h-[28rem]'
}"
>
<UCommandPalette
ref="commandPaletteRef"
:groups="groups"
command-attribute="title"
:fuse="{
fuseOptions: { ignoreLocation: true, includeMatches: true, threshold: 0, keys: ['title', 'description', 'children.children.value', 'children.children.children.value'] },
resultLimit: 10
}"
@update:model-value="onSelect"
@close="isSearchModalOpen = false"
/>
</UModal>
</template>
<script setup lang="ts">
import type { Command } from '../../../src/runtime/types'
const { navigation } = useContent()
const router = useRouter()
const { usingInput } = useShortcuts()
const { isSearchModalOpen } = useDocs()
const commandPaletteRef = ref<HTMLElement & { query: Ref<string>, results: { item: Command }[] }>()
const { data: files } = await useLazyAsyncData('search', () => queryContent().where({ _type: 'markdown' }).find(), { default: () => [] })
// Computed
const defaultGroups = computed(() => navigation.value.map(item => ({
key: item._path,
label: item.title,
commands: files.value.filter(file => file._path.startsWith(item._path)).map(file => ({
id: file._id,
title: file.navigation?.title || file.title,
to: file._path,
suffix: file.description,
icon: file.icon
}))
})))
const queryGroups = computed(() => navigation.value.map(item => ({
key: item._path,
label: item.title,
commands: files.value.filter(file => file._path.startsWith(item._path)).flatMap((file) => {
return [{
id: file._id,
title: file.navigation?.title || file.title,
to: file._path,
description: file.description,
icon: file.icon
},
// @ts-ignore
...Object.entries(groupByHeading(file.body.children)).map(([hash, { title, children }]) => ({
id: `${file._path}${hash}`,
title,
prefix: `${file.navigation?.title || file.title} ->`,
prefixClass: 'text-gray-700 dark:text-gray-200',
to: `${file._path}${hash}`,
children: concatChildren(children),
icon: file.icon
}))]
})
})))
const groups = computed(() => commandPaletteRef.value?.query ? queryGroups.value : defaultGroups.value)
// avoid conflicts between multiple meta_k shortcuts
const canToggleModal = computed(() => isSearchModalOpen.value || !usingInput.value)
// Methods
function remapChildren (children: any[]) {
return children?.map((grandChild) => {
if (['code-inline', 'em', 'a', 'strong'].includes(grandChild.tag)) {
return { type: 'text', value: grandChild.children.find(child => child.type === 'text')?.value || '' }
}
return grandChild
})
}
function concatChildren (children: any[]) {
return children.map((child) => {
if (['alert'].includes(child.tag)) {
child.children = concatChildren(child.children)
}
if (child.tag === 'p') {
child.children = remapChildren(child.children)
child.children = child.children?.reduce((acc, grandChild) => {
if (grandChild.type === 'text') {
if (acc.length && acc[acc.length - 1].type === 'text') {
acc[acc.length - 1].value += grandChild.value
} else {
acc.push(grandChild)
}
} else {
acc.push(grandChild)
}
return acc
}, [])
}
if (['style'].includes(child.tag)) {
return null
}
return child
})
}
function groupByHeading (children: any[]) {
const groups = {} // grouped by path
let hash = '' // file.page with potential `#anchor` concat
let title: string | null
for (const node of children) {
// if heading found, udpate current path
if (['h2', 'h3'].includes(node.tag)) {
// find heading text value
title = node.children?.find(child => child.type === 'text')?.value
if (title) {
hash = `#${node.props.id}`
}
}
// push to existing/new group based on path
if (groups[hash]) {
groups[hash].children.push(node)
} else {
groups[hash] = { children: [node], title }
}
}
return groups
}
function onSelect (option) {
isSearchModalOpen.value = false
if (option.click) {
option.click()
} else if (option.to) {
router.push(option.to)
} else if (option.href) {
window.open(option.href, '_blank')
}
}
// Shortcuts
defineShortcuts({
meta_k: {
usingInput: true,
whenever: [canToggleModal],
handler: () => {
isSearchModalOpen.value = !isSearchModalOpen.value
}
},
escape: {
usingInput: true,
whenever: [isSearchModalOpen],
handler: () => { isSearchModalOpen.value = false }
}
})
</script>

View File

@@ -0,0 +1,19 @@
<template>
<div v-if="toc" class="sticky top-16 bg-white/75 dark:bg-gray-900/75 backdrop-blur group lg:self-start -mx-4 sm:-mx-6 lg:mx-0 px-4 sm:px-6 lg:pl-8 lg:pr-0">
<div class="py-3 lg:py-8 border-b border-dashed border-gray-200 dark:border-gray-800 lg:border-0">
<button class="flex items-center gap-2" tabindex="-1" @click="isTocOpen = !isTocOpen">
<span class="text-sm text-slate-900 font-semibold text-sm leading-6 dark:text-slate-100 truncate">Table of Contents</span>
<UIcon name="i-heroicons-chevron-right-20-solid" class="lg:hidden w-4 h-4 transition-transform duration-100 transform text-gray-400 dark:text-gray-500" :class="[isTocOpen ? 'rotate-90' : 'rotate-0']" />
</button>
<DocsTocLinks class="mt-2 lg:mt-4" :links="toc.links" :class="[isTocOpen ? 'lg:block' : 'hidden lg:block']" />
</div>
</div>
</template>
<script setup lang="ts">
const { toc } = useContent()
const isTocOpen = ref(false)
</script>

View File

@@ -0,0 +1,49 @@
<template>
<ul>
<li v-for="link in links" :key="link.text" :class="{ 'ml-3': link.depth === 3 }">
<a
:href="`#${link.id}`"
class="block py-1 font-medium text-sm"
:class="[activeHeadings.includes(link.id) ? 'text-primary-500 dark:text-primary-400' : 'hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300']"
@click.prevent="scrollToHeading(link.id)"
>
{{ link.text }}
</a>
<DocsTocLinks v-if="link.children" :links="link.children" />
</li>
</ul>
</template>
<script setup lang="ts">
import type { TocLink } from '@nuxt/content/dist/runtime/types'
defineProps({
links: {
type: Array as PropType<TocLink[]>,
default: () => []
}
})
const emit = defineEmits(['move'])
const route = useRoute()
const router = useRouter()
const { activeHeadings, updateHeadings } = useScrollspy()
watch(() => route.path, () => {
setTimeout(() => {
if (process.client) {
updateHeadings([
...document.querySelectorAll('h2'),
...document.querySelectorAll('h3')
])
}
}, 300)
}, { immediate: true })
const scrollToHeading = (id: string) => {
router.push(`#${id}`)
emit('move', id)
}
</script>

View File

@@ -0,0 +1,19 @@
const useComponentsMetaState = () => useState('components-meta', () => ({}))
export async function fetchComponentMeta (name: string) {
const state = useComponentsMetaState()
if (state.value[name]?.then) {
await state.value[name]
return state.value[name]
}
if (state.value[name]) { return state.value[name] }
// Store promise to avoid multiple calls
state.value[name] = $fetch(`/api/component-meta/${name}`).then((meta) => {
state.value[name] = meta
})
await state.value[name]
return state.value[name]
}

View File

@@ -0,0 +1,11 @@
import { createSharedComposable } from '@vueuse/core'
const _useDocs = () => {
const isSearchModalOpen = ref(false)
return {
isSearchModalOpen
}
}
export const useDocs = createSharedComposable(_useDocs)

View File

@@ -0,0 +1,37 @@
/**
* Scrollspy allows you to watch visible headings in a specific page.
* Useful for table of contents live style updates.
*/
export const useScrollspy = () => {
const observer = ref() as Ref<IntersectionObserver>
const visibleHeadings = ref([]) as Ref<string[]>
const activeHeadings = ref([]) as Ref<string[]>
const observerCallback = (entries: IntersectionObserverEntry[]) =>
entries.forEach((entry) => {
const id = entry.target.id
if (entry.isIntersecting) { visibleHeadings.value.push(id) } else { visibleHeadings.value = visibleHeadings.value.filter(t => t !== id) }
})
const updateHeadings = (headings: Element[]) =>
headings.forEach((heading) => {
observer.value.observe(heading)
})
watch(visibleHeadings, (val, oldVal) => {
if (val.length === 0) { activeHeadings.value = oldVal } else { activeHeadings.value = val }
})
// Create intersection observer
onBeforeMount(() => (observer.value = new IntersectionObserver(observerCallback)))
// Destroy it
onBeforeUnmount(() => observer.value?.disconnect())
return {
visibleHeadings,
activeHeadings,
updateHeadings
}
}

View File

@@ -0,0 +1,62 @@
---
title: 'nuxthq/ui'
description: 'Components library as a Nuxt module using TailwindCSS and HeadlessUI.'
navigation:
title: Installation
---
## Installation
1. Install `@nuxthq/ui` dependency to your project:
::code-group
```bash [yarn]
yarn add -D @nuxthq/ui
```
```bash [npm]
npm install -D @nuxthq/ui
```
```sh [pnpm]
pnpm i -D @nuxthq/ui
```
::
2. Add it to your `modules` section in your `nuxt.config`:
```ts [nuxt.config]
export default defineNuxtConfig({
modules: ['@nuxthq/ui']
})
```
::alert
That's it! You can now use all the components and composables in your Nuxt app ✨
::
## Options
| Key | Default | Description |
| ------------------------ | ---------------------- | ------------------------------------------------ |
| `prefix` | `u` | Define the prefix of the imported components. |
| `global` | `false` | Expose components globally. |
| `icons` | `['heroicons']` | Icon collections to load. |
## Edge channel
To use the latest updates pushed on the [`dev`](https://github.com/nuxtlabs/ui/tree/dev) branch, you can use `@nuxthq/ui-edge`.
Update your `package.json` to the following:
```json [package.json]
{
"devDependencies": {
"@nuxthq/ui": "npm:@nuxthq/ui-edge@latest"
}
}
```
Then run `npm install` or `yarn install`.

View File

@@ -0,0 +1,5 @@
---
navigation: false
---
## Overview

View File

@@ -0,0 +1,11 @@
---
navigation: false
---
## Overview
## Composables
## `defineShortcuts`
## `useShortcuts`

View File

@@ -0,0 +1,100 @@
---
github: true
---
## Usage
::component-card
---
props:
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
alt: 'Avatar'
---
::
### Size
Use the `size` prop to change the size of the Avatar.
::component-card
---
props:
size: 'sm'
baseProps:
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
alt: 'Avatar'
---
::
### Chip
Use the `chipColor`, `chipVariant` and `chipPosition` props to display a chip on the Avatar.
::component-card
---
props:
chipColor: 'primary'
chipVariant: 'solid'
chipPosition: 'top-right'
baseProps:
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
alt: 'Avatar'
---
::
### Placeholder
If there is an error loading the `src` of the avatar or `src` is null a background placeholder will be displayed, customizable in `ui.avatar.background`.
If there's an `alt` prop initials will be displayed on top of the background, customizable in `ui.avatar.placeholder`.
::component-card
---
props:
alt: 'Benjamin Canac'
---
::
### Group
To stack avatars as a group, use the `AvatarGroup` component.
- To limit the amount of avatars to show, use the `max` prop. It'll truncate the avatars and show a "+X" label (where X is the remaining avatars)
- To size all the avatars equally, pass the `size` prop
- To adjust the spacing or the ring between avatars, customize with `ui.avatarGroup.margin` or `ui.avatarGroup.ring`
::component-card{slug="AvatarGroup"}
---
props:
size: 'sm'
max: 2
ui:
size:
'3xs': ''
'2xs': ''
xs: ''
sm: ''
md: ''
lg: ''
xl: ''
'2xl': ''
'3xl': ''
code: |
<UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" alt="benjamincanac" />
<UAvatar src="https://avatars.githubusercontent.com/u/904724?v=4" alt="Atinux" />
<UAvatar src="https://avatars.githubusercontent.com/u/7547335?v=4" alt="smarroufin" />
---
#default
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" alt="Avatar"}
:u-avatar{src="https://avatars.githubusercontent.com/u/904724?v=4" alt="Avatar"}
:u-avatar{src="https://avatars.githubusercontent.com/u/7547335?v=4" alt="Avatar"}
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,59 @@
---
github: true
---
## Usage
Use the default slot to set the text of the Badge.
::component-card
---
code: Badge
---
Badge
::
You can also use the `label` prop:
::component-card
---
props:
label: Badge
---
::
### Style
Use the `color` and `variant` props to change the visual style of the Badge.
::component-card
---
props:
color: 'primary'
variant: 'solid'
---
Badge
::
### Size
Use the `size` prop to change the size of the Badge.
::component-card
---
props:
size: 'sm'
---
Badge
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,280 @@
---
github: true
---
## Usage
Use the default slot to set the text of the Button.
::component-card
---
code: Button
---
Button
::
You can also use the `label` prop.
::component-card
---
props:
label: Button
---
::
### Style
Use the `color` and `variant` props to change the visual style of the Button.
::component-card
---
props:
color: 'primary'
variant: 'solid'
---
Button
::
Besides all the colors from the `ui.colors` object, you can also use the `white` and `gray` and `black` colors with their pre-defined variants.
#### White
::component-card
---
backgroundClass: 'bg-gray-50 dark:bg-gray-800'
props:
color: 'white'
variant: 'solid'
ui:
variant:
solid: 1
ghost: 1
excludedProps:
- color
---
Button
::
#### Gray
::component-card
---
props:
color: 'gray'
variant: 'solid'
ui:
variant:
solid: 1
ghost: 1
link: 1
excludedProps:
- color
---
Button
::
#### Black
::component-card
---
props:
color: 'black'
variant: 'solid'
ui:
variant:
solid: 1
link: 1
excludedProps:
- color
---
Button
::
### Size
Use the `size` prop to change the size of the Button.
::component-card
---
props:
size: 'sm'
---
Button
::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `leading` and `trailing` props to set the icon position or the `leadingIcon` and `trailingIcon` props to set a different icon for each position.
::component-card
---
props:
icon: 'i-heroicons-pencil-square'
size: 'sm'
color: 'primary'
variant: 'solid'
label: Button
trailing: false
excludedProps:
- icon
---
::
The `label` as prop or slot is optional so you can use the Button as an icon-only button.
::component-card
---
props:
icon: 'i-heroicons-pencil-square'
size: 'sm'
color: 'primary'
square: true
variant: 'solid'
excludedProps:
- icon
- square
---
::
### Disabled
Use the `disabled` prop to disable the Button.
::component-card
---
props:
disabled: true
---
Button
::
### Loading
Use the `loading` prop to show a loading icon and disable the Button.
::component-card
---
props:
loading: true
---
Button
::
### Block
Use the `block` prop to make the Button fill the width of its container.
::component-card
---
props:
block: true
---
Button
::
### Link
Use the `to` prop to make the Button a link.
::component-card
---
props:
to: 'https://volta.net'
target: '_blank'
---
Button
::
### Padded
Use the `padded` prop to remove the padding of the Button.
::component-card
---
props:
padded: false
baseProps:
color: 'gray'
variant: 'link'
icon: 'i-heroicons-x-mark-20-solid'
---
::
### Square
Use the `square` prop to force the Button to have the same padding horizontally and vertically.
::component-card
---
props:
square: true
baseProps:
label: 'Button'
color: 'gray'
variant: 'solid'
---
::
### Truncate
Use the `truncate` prop to truncate the label of the Button.
::component-card
---
props:
truncate: true
class: 'w-20'
label: 'Button with a long text'
excludedProps:
- class
---
::
### Group
To stack buttons as a group, use the `ButtonGroup` component.
- To size all the buttons equally, pass the `size` prop
- To adjust the rounded or the shadow around buttons, customize with `ui.buttonGroup.rounded` or `ui.buttonGroup.shadow`
::component-card{slug="ButtonGroup"}
---
props:
size: 'sm'
ui:
size:
xxs: ''
xs: ''
sm: ''
md: ''
lg: ''
xl: ''
code: |
<UButton label="Action" color="white" />
<UButton icon="i-heroicons-chevron-down-20-solid" color="gray" />
---
#default
:u-button{label="Action" color="white"}
:u-button{icon="i-heroicons-chevron-down-20-solid" color="gray"}
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,59 @@
---
github: true
headlessui:
label: 'Menu'
to: 'https://headlessui.com/vue/menu'
---
## Usage
::component-example
#default
:dropdown-example
#code
```vue
<script setup>
const items = [
[{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
}], [{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
shortcuts: ['E']
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid',
shortcuts: ['D']
}], [{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'i-heroicons-arrow-right-circle-20-solid'
}], [{
label: 'Delete',
icon: 'i-heroicons-trash-20-solid',
shortcuts: ['⌘', 'D']
}]
]
</script>
<template>
<UDropdown :items="items" :popper="{ placement: 'bottom-start' }">
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
</UDropdown>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,48 @@
---
github: true
title: 'Keyboard Key'
navigation:
title: 'Kbd'
---
## Usage
::component-example
#default
:kbd-example
#code
```vue
<script setup>
const { metaSymbol } = useShortcuts()
</script>
<template>
<div class="flex items-center gap-0.5">
<UKbd>{{ metaSymbol }}</UKbd>
<UKbd>U</UKbd>
</div>
</template>
```
::
### Size
Use the `size` prop to change the size of the Kbd.
::component-card
---
props:
size: 'sm'
---
U
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,91 @@
---
github: true
---
## Usage
::component-card
---
baseProps:
name: 'input'
---
::
### Size
Use the `size` prop to change the size of the Input.
::component-card
---
baseProps:
name: 'input'
props:
size: 'sm'
---
::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `leading` and `trailing` props to set the icon position or the `leadingIcon` and `trailingIcon` props to set a different icon for each position.
::component-card
---
baseProps:
name: 'input'
props:
icon: 'i-heroicons-magnifying-glass-20-solid'
appearance: 'white'
size: 'sm'
trailing: false
placeholder: 'Search...'
excludedProps:
- icon
- placeholder
---
::
### Disabled
Use the `disabled` prop to disable the Input.
::component-card
---
baseProps:
name: 'input'
props:
placeholder: 'Search...'
appearance: 'white'
disabled: true
excludedProps:
- placeholder
---
::
### Loading
Use the `loading` prop to show a loading icon and disable the Input.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search'
props:
loading: true
icon: 'i-heroicons-magnifying-glass-20-solid'
excludedProps:
- icon
---
::
### Group
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,20 @@
---
github: true
---
## Usage
::component-card
---
baseProps:
name: 'textarea'
---
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,25 @@
---
github: true
---
## Usage
::component-card
---
baseProps:
name: 'select'
modelValue: 'Canada'
options:
- 'United States'
- 'Canada'
- 'Mexico'
---
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,185 @@
---
github: true
headlessui:
label: 'Listbox'
to: 'https://headlessui.com/vue/listbox'
---
## Usage
::component-example
#default
:select-menu-example-basic{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people" />
</template>
```
::
You can use multiple values but you have to override the `#label` slot and handle the display yourself.
::component-example
#default
:select-menu-example-multiple{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref([])
</script>
<template>
<USelectMenu v-model="selected" :options="people" multiple>
<template #label>
<span v-if="selected.length" class="font-medium truncate">{{ selected.join(', ') }}</span>
<span v-else class="block truncate text-gray-400 dark:text-gray-500">Select people</span>
</template>
</USelectMenu>
</template>
```
::
You can also override the default slot entirely.
::component-example
#default
:select-menu-example-button{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[3])
</script>
<template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton>
{{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
</UButton>
</USelectMenu>
</template>
```
::
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key.
::component-example
#default
:select-menu-example-objects{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const people = [{
id: 'benjamincanac',
label: 'benjamincanac',
href: 'https://github.com/benjamincanac',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
},
{
id: 'Atinux',
label: 'Atinux',
href: 'https://github.com/Atinux',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
},
{
id: 'smarroufin',
label: 'smarroufin',
href: 'https://github.com/smarroufin',
target: '_blank',
avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' }
},
{
id: 'nobody',
label: 'Nobody',
icon: 'i-heroicons-user-circle'
}]
const selected = ref(people[0])
</script>
<template>
<USelectMenu v-model="selected" :options="people">
<template #label>
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4" />
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" />
{{ selected.label }}
</template>
</USelectMenu>
</template>
```
::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
::component-card
---
baseProps:
class: 'max-w-[12rem] w-full'
placeholder: 'Select a person'
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
icon: 'i-heroicons-magnifying-glass-20-solid'
excludedProps:
- icon
---
::
### Search
Use the `searchable` prop to enable search.
This will use HeadlessUI [Combobox](https://headlessui.com/vue/combobox) component instead of [Listbox](https://headlessui.com/vue/listbox).
::component-card
---
baseProps:
class: 'max-w-[12rem] w-full'
placeholder: 'Select a person'
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
searchable: true
---
::
### Disabled
Use the `disabled` prop to disable the SelectMenu.
::component-card
---
baseProps:
class: 'max-w-[12rem] w-full'
placeholder: 'Select a person'
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
disabled: true
---
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,16 @@
---
github: true
---
## Usage
::component-card
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,16 @@
---
github: true
---
## Usage
::component-card
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,34 @@
---
github: true
headlessui:
label: 'Switch'
to: 'https://headlessui.com/vue/switch'
---
## Usage
::component-card
::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon-on` and `icon-off` props by using this pattern: `i-{collection_name}-{icon_name}`.
::component-card
---
props:
iconOn: 'i-heroicons-check-20-solid'
iconOff: 'i-heroicons-x-mark-20-solid'
excludedProps:
- iconOn
- iconOff
---
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,46 @@
---
github: true
---
## Usage
::component-example
#default
:vertical-navigation-example
#code
```vue
<script setup>
const links = [{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
}, {
label: 'Installation',
icon: 'i-heroicons-home',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
icon: 'i-heroicons-chart-bar',
to: '/navigation/vertical-navigation'
}, {
label: 'Command Palette',
icon: 'i-heroicons-command-line',
to: '/navigation/command-palette'
}]
</script>
<template>
<UVerticalNavigation :links="links" />
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,273 @@
---
github: true
headlessui:
label: 'Combobox'
to: 'https://headlessui.com/vue/combobox'
---
## Usage
Use a `v-model` to display a searchable and selectable list of commands.
::component-example
---
padding: false
---
#default
:command-palette-example-basic{class="h-[257px]"}
#code
```vue
<script setup>
const people = [
{ id: 1, label: 'Wade Cooper' },
{ id: 2, label: 'Arlene Mccoy' },
{ id: 3, label: 'Devon Webb' },
{ id: 4, label: 'Tom Cook' },
{ id: 5, label: 'Tanya Fox' },
{ id: 6, label: 'Hellen Schmidt' },
{ id: 7, label: 'Caroline Schultz' },
{ id: 8, label: 'Mason Heaney' },
{ id: 9, label: 'Claudie Smitham' },
{ id: 10, label: 'Emil Schaefer' }
]
const selected = ref([people[3]])
</script>
<template>
<UCommandPalette
v-model="selected"
multiple
nullable
:groups="[{ key: 'people', commands: people }]"
:fuse="{ resultLimit: 6, fuseOptions: { threshold: 0.1 } }"
/>
</template>
```
::
You can put a `CommandPalette` anywhere you want but it's most commonly used inside of a modal.
::component-example
#default
:command-palette-example-modal
#code
```vue
<script setup>
const open = ref(false)
const people = [
{ id: 1, label: 'Wade Cooper' },
{ id: 2, label: 'Arlene Mccoy' },
{ id: 3, label: 'Devon Webb' },
{ id: 4, label: 'Tom Cook' },
{ id: 5, label: 'Tanya Fox' },
{ id: 6, label: 'Hellen Schmidt' },
{ id: 7, label: 'Caroline Schultz' },
{ id: 8, label: 'Mason Heaney' },
{ id: 9, label: 'Claudie Smitham' },
{ id: 10, label: 'Emil Schaefer' }
]
const selected = ref([])
</script>
<template>
<div>
<UButton label="Open" @click="open = true" />
<UModal v-model="open">
<UCommandPalette
v-model="selected"
multiple
nullable
:groups="[{ key: 'people', commands: people }]"
/>
</UModal>
</div>
</template>
```
::
You can pass multiple groups of commands to the component. Each group will be separated by a divider and will display a label.
Without a `v-model`, you can also listen on `@update:model-value` to navigate to a link or do something else when a command is clicked.
::component-example
---
padding: false
---
#default
:command-palette-example-groups{class="h-[274px]"}
#code
```vue
<script setup>
const router = useRouter()
const commandPaletteRef = ref()
const users = [
{ id: 'benjamincanac', label: 'benjamincanac', href: 'https://github.com/benjamincanac', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' } },
{ id: 'Atinux', label: 'Atinux', href: 'https://github.com/Atinux', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' } },
{ id: 'smarroufin', label: 'smarroufin', href: 'https://github.com/smarroufin', target: '_blank', avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' } }
]
const actions = [
{ id: 'new-file', label: 'Add new file', icon: 'i-heroicons-document-plus', click: () => alert('New file'), shortcuts: ['⌘', 'N'] },
{ id: 'new-folder', label: 'Add new folder', icon: 'i-heroicons-folder-plus', click: () => alert('New folder'), shortcuts: ['⌘', 'F'] },
{ id: 'hashtag', label: 'Add hashtag', icon: 'i-heroicons-hashtag', click: () => alert('Add hashtag'), shortcuts: ['⌘', 'H'] },
{ id: 'label', label: 'Add label', icon: 'i-heroicons-tag', click: () => alert('Add label'), shortcuts: ['⌘', 'L'] }
]
const groups = computed(() => commandPaletteRef.value?.query
? [{
key: 'users',
commands: users
}]
: [{
key: 'recent',
label: 'Recent searches',
commands: users.slice(0, 1)
}, {
key: 'actions',
commands: actions
}])
function onSelect (option) {
if (option.click) {
option.click()
} else if (option.to) {
router.push(option.to)
} else if (option.href) {
window.open(option.href, '_blank')
}
}
</script>
<template>
<UCommandPalette ref="commandPaletteRef" :groups="groups" @update:model-value="onSelect" />
</template>
```
::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
::component-card
---
padding: false
baseProps:
empty: null
props:
icon: 'i-heroicons-command-line'
excludedProps:
- icon
---
::
### Placeholder
Use the `placeholder` prop to change the input placeholder
::component-card
---
padding: false
baseProps:
empty: null
props:
placeholder: 'Type a command...'
excludedProps:
- icon
---
::
### Close
Use the `close` prop to display a close button on the right side of the input.
You can pass all the props of the [Button](/elements/button) component to customize it through the `close` prop or globally through `ui.commandPalette.default.close`.
::component-card
---
padding: false
baseProps:
empty: null
props:
close:
icon: 'i-heroicons-x-mark-20-solid'
color: 'gray'
variant: 'link'
padded: false
excludedProps:
- close
---
::
### Empty
Use the `empty` prop to display a message when there are no results.
You can pass an `object` through the `empty` prop or globally through `ui.commandPalette.default.empty`. Here is the default:
::component-card
---
padding: false
baseProps:
placeholder: 'Type something to see the empty label change'
props:
empty:
icon: 'i-heroicons-magnifying-glass-20-solid'
label: "We couldn't find any items."
queryLabel: "We couldn't find any items with that term. Please try again."
excludedProps:
- empty
---
::
## Themes
Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do.
### Algolia
::component-example
---
padding: false
---
#default
:command-palette-theme-algolia{class="max-h-[480px] rounded-md"}
::
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/docs/rework/docs/components/content/themes/CommandPaletteThemeAlgolia.vue#L23"}
Take a look at the component!
::
### Raycast
::component-example
---
padding: false
---
#default
:command-palette-theme-raycast{class="max-h-[480px] rounded-md"}
::
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/docs/rework/docs/components/content/themes/CommandPaletteThemeRaycast.vue#L30"}
Take a look at the component!
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,72 @@
---
github: true
headlessui:
label: 'Dialog'
to: 'https://headlessui.com/vue/dialog'
---
## Usage
::component-example
#default
:modal-example-basic
#code
```vue
<script setup>
const open = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="open = true" />
<UModal v-model="open">
<!-- Content -->
</UModal>
</div>
</template>
```
::
You can put a [Card](/layout/card) component inside your Modal to handle forms and take advantage of `header` and `footer` slots:
::component-example
#default
:modal-example-card
#code
```vue
<script setup>
const open = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="open = true" />
<UModal v-model="open">
<UCard :ui="{ divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<!-- Content -->
</template>
<!-- Content -->
<template #footer>
<!-- Content -->
</template>
</UCard>
</UModal>
</div>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,37 @@
---
github: true
headlessui:
label: 'Dialog'
to: 'https://headlessui.com/vue/dialog'
---
## Usage
::component-example
#default
:slideover-example
#code
```vue
<script setup>
const open = ref(false)
</script>
<template>
<div>
<UButton label="Open" @click="open = true" />
<USlideover v-model="open">
<!-- Content -->
</USlideover>
</div>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,33 @@
---
github: true
headlessui:
label: 'Popover'
to: 'https://headlessui.com/vue/popover'
---
## Usage
::component-example
#default
:popover-example
#code
```vue
<template>
<UPopover>
<UButton color="white" label="Open" trailing-icon="i-heroicons-chevron-down-20-solid" />
<template #panel>
<!-- Content -->
</template>
</UPopover>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,26 @@
---
github: true
---
## Usage
::component-example
#default
:tooltip-example
#code
```vue
<template>
<UTooltip text="Tooltip example" :shortcuts="['⌘', 'O']">
<UButton color="gray" label="Hover me" />
</UTooltip>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,49 @@
---
github: true
---
## Usage
::component-example
#default
:context-menu-example
#code
```vue
<script setup>
const { x, y } = useMouse()
const isOpen = ref(false)
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
function openContextMenu () {
const top = unref(y)
const left = unref(x)
virtualElement.value.getBoundingClientRect = () => ({
width: 0,
height: 0,
top,
left
})
isOpen.value = true
}
</script>
<template>
<div @contextmenu.prevent="openContextMenu">
<UContextMenu v-model="isOpen" :virtual-element="virtualElement">
<!-- Content -->
</UContextMenu>
</div>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,249 @@
---
github: true
---
## Usage
First of all, add the `Notifications` component to your app, preferably inside `app.vue`.
This component will render by default the notifications at the bottom right of the screen. You can configure its behaviour in the `app.config.ts` through `ui.notifications`.
```vue [app.vue]
<template>
<div>
<UContainer>
<NuxtPage />
</UContainer>
<UNotifications />
</div>
</template>
```
Then, you can use the `useToast` composable to add notifications to your app:
::component-example
#default
:notification-example-basic
#code
```vue
<script setup>
const toast = useToast()
</script>
<template>
<UButton label="Show toast" @click="toast.add({ title: 'Hello world!' })" />
</template>
```
::
### Description
You can add a `description` in addition of the `title`.
::component-card
---
baseProps:
id: 2
timeout: 0
props:
title: 'Notification'
description: 'This is a notification.'
---
::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
::component-card
---
baseProps:
id: 3
timeout: 0
title: 'Notification'
props:
icon: 'i-heroicons-check-circle'
description: 'This is a notification.'
excludedProps:
- icon
---
::
### Avatar
Use the [avatar](/elements/avatar) prop as an `object` and configure it with any of its props.
::component-card
---
baseProps:
id: 4
timeout: 0
title: 'Notification'
description: 'This is a notification.'
props:
avatar:
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
excludedProps:
- avatar
---
::
### Timeout
Use the `timeout` prop to configure how long the Notification will remain. Set it to `0` to disable the timeout.
You will see a progress bar at the bottom of the Notification which will indicate the remaining time. When hovering the Notification, the progress bar will be paused.
::component-card
---
baseProps:
id: 5
title: 'Notification'
description: 'This is a notification.'
props:
timeout: 10000
---
::
### Click
Use the `click` prop to execute a function when the Notification is clicked.
::component-example
#default
:notification-example-click
#code
```vue
<script setup>
const toast = useToast()
function onClick () {
alert('Clicked!')
}
</script>
<template>
<UButton label="Show toast" @click="toast.add({ title: 'Click me', click: onClick })" />
</template>
```
::
### Callback
Use the `callback` prop to execute a function when the Notification expires.
::component-example
#default
:notification-example-callback
#code
```vue
<script setup>
const toast = useToast()
function onCallback () {
alert('Notification expired!')
}
</script>
<template>
<UButton label="Show toast" @click="toast.add({ title: 'Expires soon...', timeout: 1000, callback: onCallback })" />
</template>
```
::
### Close
Use the `close` prop to hide or customize the close button on the Notification.
You can pass all the props of the [Button](/elements/button) component to customize it through the `close` prop or globally through `ui.notifications.default.close`.
::component-card
---
baseProps:
id: 6
title: 'Notification'
timeout: 0
props:
close:
icon: 'i-heroicons-archive-box-x-mark'
color: 'primary'
variant: 'outline'
padded: true
size: '2xs'
ui:
rounded: 'rounded-full'
excludedProps:
- close
---
::
### Actions
Use the `actions` prop to add actions to the Notification.
::component-example
#default
:notification-example-actions
#code
```vue
<script setup>
const toast = useToast()
</script>
<template>
<UButton label="Show toast" @click="toast.add({ title: 'Click me', click: () => alert('Clicked!') })" />
</template>
```
::
Like for `close`, you can pass all the props of the [Button](/elements/button) component inside the action or globally through `ui.notifications.default.action`.
::component-card
---
baseProps:
id: 6
title: 'Notification'
timeout: 0
props:
actions:
- variant: 'ghost'
color: 'gray'
label: Action 1
- variant: 'solid'
color: 'gray'
label: Action 2
excludedProps:
- actions
---
::
Actions will render differently whether you have a `description` set.
::component-card
---
baseProps:
id: 6
title: 'Notification'
description: 'This is a notification.'
timeout: 0
props:
actions:
- variant: 'solid'
color: 'primary'
label: Action 1
- variant: 'outline'
color: 'primary'
label: Action 2
excludedProps:
- actions
---
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,35 @@
---
github: true
---
## Usage
::component-example
#default
:card-example{class="w-full"}
#code
```vue
<template>
<UCard>
<template #header />
Body
<template #footer />
</UCard>
</template>
```
::
## Props
:component-props
## Slots
:component-slots
## Preset
:component-preset

View File

@@ -0,0 +1,25 @@
---
github: true
---
## Usage
::component-example
#default
:container-example{class="w-full"}
#code
```vue
<template>
<UContainer>Content</UContainer>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

5
docs/layouts/default.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<div class="prose prose-primary dark:prose-invert max-w-none">
<slot />
</div>
</template>

View File

@@ -1,38 +1,38 @@
import defaultTheme from 'tailwindcss/defaultTheme'
import nuxtUI from '../src/module'
import ui from '../src/module'
// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
app: {
head: {
htmlAttrs: {
lang: 'en'
}
// @ts-ignore
modules: [
ui,
'@vueuse/nuxt',
'@nuxt/content',
'@nuxtjs/plausible',
'nuxt-lodash',
'nuxt-component-meta'
],
content: {
documentDriven: true,
highlight: {
theme: {
light: 'material-lighter',
dark: 'material-palenight'
},
preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'diff', 'shell', 'markdown', 'yaml', 'bash', 'ini']
}
},
modules: [
// @ts-ignore
nuxtUI
],
components: {
global: true
},
ui: {
colors: {
primary: 'blue',
gray: 'zinc'
},
preset: {
},
icons: ['heroicons', 'mdi'],
tailwindcss: {
theme: {
extend: {
fontFamily: {
sans: ['Inter var', ...defaultTheme.fontFamily.sans]
}
}
}
global: true,
icons: ['heroicons', 'simple-icons']
},
typescript: {
strict: false,
includeWorkspace: true
},
// @ts-ignore
$production: {
routeRules: {
'/api/_content/**': { isr: true, static: true },
'/api/component-meta/**': { isr: true, static: true }
}
}
})

View File

@@ -1,509 +0,0 @@
<!-- eslint-disable vue/no-template-shadow -->
<!-- eslint-disable vue/no-v-html -->
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<template>
<UCard v-if="component" class="relative flex flex-col" body-class="px-4 py-5 sm:p-6 relative" footer-class="flex flex-col flex-1 overflow-hidden">
<div class="flex justify-center">
<component :is="`U${defaultProps[params.component].component.name}`" v-if="defaultProps[params.component] && defaultProps[params.component].component" v-bind="defaultProps[params.component].component.props" />
<component :is="is" v-bind="{ ...boundProps, ...eventProps }">
<template v-for="[key, slot] of Object.entries(defaultProps[params.component]?.slots || {}) || []" #[key]>
<template v-if="Array.isArray(slot)">
<div :key="key">
<component
:is="slot.component ? `U${slot.component.name}` : slot.tag"
v-for="(slot, index) of slot"
:key="index"
:class="slot.class"
v-bind="slot.component?.props || defaultProps[slot.component]"
v-html="slot.html"
/>
</div>
</template>
<template v-else>
<component :is="`U${slot.component.name}`" v-if="slot.component" :key="`${key}-component`" v-bind="slot.component?.props || defaultProps[slot.component]" />
<component :is="slot.tag" v-else :key="`${key}-tag`" :class="slot.class" v-html="slot.html" />
</template>
</template>
</component>
</div>
<template v-if="props.length" #footer>
<div class="border-b u-border-gray-200">
<pre class="text-sm leading-6 u-text-gray-900 flex-1 relative flex ligatures-none overflow-x-hidden px-4 sm:px-6 py-5 sm:py-6">
<code class="flex-none min-w-full whitespace-pre-wrap break-all">{{ code }}</code>
<UButton
class="absolute top-0 right-0"
:icon="copied ? 'i-heroicons-clipboard-document-check' : 'i-heroicons-clipboard-document'"
variant="transparent"
size="sm"
:custom-class="copied ? '!text-green-500' : ''"
@click="onCopy"
/>
</pre>
</div>
<div class="flex-1 px-4 py-5 sm:p-6 space-y-3">
<UFormGroup
v-for="prop of props"
:key="prop.key"
class="capitalize"
:name="prop.key"
:label="prop.key"
>
<UToggle
v-if="prop.type === 'Boolean'"
v-model="prop.value"
:name="prop.key"
/>
<USelect
v-else-if="prop.values"
v-model="prop.value"
:name="prop.key"
placeholder="Choose one..."
:options="prop.values"
size="sm"
/>
<UInput
v-else-if="prop.type === 'String'"
v-model="prop.value"
:name="prop.key"
size="sm"
autocomplete="off"
/>
<UInput
v-else-if="prop.type === 'Number'"
v-model="prop.value"
type="number"
:name="prop.key"
size="sm"
/>
<UTextarea
v-else
:model-value="prop.value && JSON.stringify(prop.value)"
:name="prop.key"
size="sm"
:rows="8"
autoresize
@update:model-value="value => prop.value = JSON.parse(value)"
/>
</UFormGroup>
</div>
</template>
</UCard>
</template>
<script setup>
import { useClipboard } from '@vueuse/core'
import $ui from '#build/ui'
const nuxtApp = useNuxtApp()
const { params } = useRoute()
const is = `U${params.component}`
const component = nuxtApp.vueApp.component(is)
const people = [
{ id: 1, label: 'Durward Reynolds', disabled: false },
{ id: 2, label: 'Kenton Towne', disabled: false },
{ id: 3, label: 'Therese Wunsch', disabled: false },
{ id: 4, label: 'Benedict Kessler', disabled: true },
{ id: 5, label: 'Katelyn Rohan', disabled: false }
]
const selectCustom = ref(people[0])
const commandPalette = ref(people[0])
const alertDialog = ref(false)
const toggle = ref(false)
const modal = ref(false)
const slideover = ref(false)
const x = ref(0)
const y = ref(0)
const isContextMenuOpen = ref(false)
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
onMounted(() => {
document.addEventListener('mousemove', ({ clientX, clientY }) => {
x.value = clientX
y.value = clientY
})
})
function openContextMenu () {
const top = unref(y)
const left = unref(x)
virtualElement.value.getBoundingClientRect = () => ({
width: 0,
height: 0,
top,
left
})
isContextMenuOpen.value = true
}
const defaultProps = {
Button: {
label: 'Button text'
},
Badge: {
label: 'Badge'
},
Alert: {
title: 'A new software update is available. See whats new in version 2.0.4.'
},
AlertDialog: {
icon: 'i-heroicons-exclamation-triangle',
title: 'Deactivate account',
description: 'Are you sure you want to deactivate your account? All of your data will be permanently removed from our servers forever. This action cannot be undone.',
modelValue: alertDialog,
'onUpdate:modelValue': (v) => { alertDialog.value = v },
component: {
name: 'Button',
props: {
variant: 'secondary',
label: 'Open modal',
onClick: () => { alertDialog.value = true }
}
}
},
Avatar: {
src: 'https://picsum.photos/200/300'
},
AvatarGroup: {
group: ['https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', 'https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.25&w=256&h=256&q=80', 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80']
},
Dropdown: {
items: [
[{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid'
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid'
}],
[{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'i-heroicons-arrow-right-circle-20-solid'
}],
[{
label: 'Delete',
icon: 'i-heroicons-trash-20-solid'
}]
]
},
VerticalNavigation: {
links: [
{
label: 'Home',
icon: 'i-heroicons-home',
to: '/'
},
{
label: 'Examples',
icon: 'i-heroicons-book-open',
to: '/examples'
},
{
label: 'Migration',
icon: 'i-heroicons-arrow-path',
to: '/migration'
},
{
label: 'External link',
icon: 'i-heroicons-link',
to: 'https://google.fr',
target: '_blank'
}
]
},
CommandPalette: {
modelValue: commandPalette,
'onUpdate:modelValue': (v) => { commandPalette.value = v },
groups: [{
key: 'people',
label: 'People',
commands: people
}]
},
Icon: {
name: 'i-heroicons-bell'
},
Input: {
name: 'input',
placeholder: 'Enter text'
},
FormGroup: {
name: 'input',
label: 'Input group',
slots: {
default: {
component: {
name: 'Input',
props: {
name: 'input',
placeholder: 'Works with every form element'
}
}
}
}
},
Toggle: {
modelValue: toggle,
'onUpdate:modelValue': (v) => { toggle.value = v }
},
Checkbox: {
name: 'checkbox'
},
Radio: {
name: 'radio'
},
Select: {
name: 'select',
options: ['English', 'Spanish', 'French', 'German', 'Chinese']
},
SelectCustom: {
modelValue: selectCustom,
'onUpdate:modelValue': (v) => { selectCustom.value = v },
textAttribute: 'label',
options: people
},
Textarea: {
name: 'textarea'
},
Tooltip: {
text: 'Tooltip text'
},
Notification: {
id: '1',
title: 'Notification title',
callback: 'console.log(\'Timer expired\')'
},
ContextMenu: {
modelValue: isContextMenuOpen,
'onUpdate:modelValue': (v) => { isContextMenuOpen.value = v },
virtualElement,
component: {
name: 'Card',
props: {
variant: 'secondary',
label: 'Open context menu',
onClick: () => { isContextMenuOpen.value = false },
onContextmenu: (e) => {
e?.preventDefault()
openContextMenu()
},
class: 'relative w-[300px] h-[100px]'
}
},
slots: {
default: {
tag: 'div',
html: 'Context menu content',
class: 'rounded border u-border-gray-200 p-2'
}
}
},
Modal: {
modelValue: modal,
'onUpdate:modelValue': (v) => { modal.value = v },
component: {
name: 'Button',
props: {
variant: 'secondary',
label: 'Open modal',
onClick: () => { modal.value = true }
}
},
slots: {
default: {
tag: 'div',
html: 'Modal content'
},
footer: {
component: {
name: 'Button',
props: {
label: 'Close',
onClick: () => { modal.value = false }
}
}
}
}
},
Slideover: {
modelValue: slideover,
'onUpdate:modelValue': (v) => { slideover.value = v },
component: {
name: 'Button',
props: {
variant: 'secondary',
label: 'Open slideover',
onClick: () => { slideover.value = true }
}
},
slots: {
default: {
tag: 'div',
html: 'Slideover content'
}
}
},
Popover: {
slots: {
panel: {
tag: 'div',
class: 'u-bg-gray-100 rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 p-6',
html: 'Popover content'
}
}
},
Tabs: {
links: [{
label: 'Usage',
to: '/',
exact: true
}, {
label: 'Examples',
to: '/examples'
}, {
label: 'Migration',
to: '/migration'
}, {
label: 'Tabs',
to: '/components/Tabs'
}]
},
Pills: {
links: [{
label: 'Usage',
to: '/',
exact: true
}, {
label: 'Examples',
to: '/examples'
}, {
label: 'Migration',
to: '/migration'
}, {
label: 'Pills',
to: '/components/Pills'
}]
}
}
const componentDefaultProps = defaultProps[params.component] || {}
const { props: componentProps } = await component.__asyncLoader()
function lowercaseFirstLetter (string) {
return string.charAt(0).toLowerCase() + string.slice(1)
}
const refProps = Object.entries(componentProps).map(([key, prop]) => {
const defaultValue = componentDefaultProps[key]
const propDefault = (typeof prop.default === 'function' ? prop.default() : prop.default)
let value = defaultValue !== undefined ? defaultValue : propDefault
let type = prop.type
if (Array.isArray(type)) {
type = type[0].name
} else {
type = type.name
}
let values
if (prop.validator) {
const arrayRegex = prop.validator.toString().match(/\[.*\]/g, '')
if (arrayRegex) {
values = JSON.parse(arrayRegex[0].replace(/'/g, '"')).filter(Boolean)
} else {
const $uiProp = $ui[lowercaseFirstLetter(params.component)][key]
if ($uiProp) {
values = Object.keys($uiProp).filter(Boolean)
}
}
}
if (value) {
if (type === 'String' && typeof value === 'string') {
value = value.replace(/^'(.*)'$/, '$1')
} else if (type === 'Array') {
value = JSON.stringify(value)
}
}
return {
key,
type,
value,
values,
default: propDefault
}
})
const eventProps = Object.entries(componentDefaultProps)
.filter(([key]) => !refProps.find(prop => prop.key === key))
.filter(([key]) => !['slots'].includes(key))
.reduce((acc, [key, value]) => {
acc[key] = value
return acc
}, {})
const props = ref(refProps)
const boundProps = computed(() => {
const bound = {}
for (const prop of props.value) {
let value = prop.value
if (value === null) {
continue
}
try {
if (prop.type === 'Array') {
value = JSON.parse(value)
} else if (prop.type === 'Number') {
value = Number(value)
} else if (prop.type === 'Function') {
// eslint-disable-next-line no-new-func
value = Function(value)
}
bound[prop.key] = value
} catch (e) {
continue
}
}
return bound
})
function toKebabCase (str) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}
const { copy, copied } = useClipboard({ copiedDuring: 2000 })
const onCopy = () => {
copy(code.value)
}
const code = computed(() => {
let code = `<U${params.component}`
for (const prop of props.value) {
if (prop.value === null) {
continue
}
if (prop.value === prop.default) {
continue
}
const key = toKebabCase(prop.key)
code += `\n ${prop.type === 'Boolean' ? ':' : ''}${key === 'model-value' ? 'v-model' : key}="${prop.value}"`
}
code += '\n/>'
return code
})
</script>

View File

@@ -1,65 +0,0 @@
<template>
<div class="space-y-4">
<div class="pb-10 border-b u-border-gray-200 mb-10">
<div>
<h1 class="inline-block text-3xl font-extrabold u-text-gray-900 tracking-tight">
Dark mode
</h1>
</div>
<p class="mt-1 text-lg u-text-gray-500">
Dark mode implementation with <a href="https://color-mode.nuxtjs.org/" target="_blank" class="underline">Color Mode</a> module.
</p>
</div>
<h2 class="font-bold text-2xl u-text-gray-900">
Usage
</h2>
<p>TailwindCSS takes advantage of the `dark` class on the html tag:</p>
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
<code class="text-sm">
{{ code4 }}
</code>
</pre>
<p>The `@nuxtjs/color-mode` module is now installed by default, you can easily implement a toggle button:</p>
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
<code class="text-sm">
{{ code1 }}
</code>
</pre>
<h2 class="font-bold text-2xl u-text-gray-900">
Shortcuts
</h2>
<p>A number of shortcuts are available to make your life with colors easier.</p>
<p>For each color utilities: `bg`, `text`, `border`, `ring` and `divide`, shortcuts for `white`, `black` and `gray` colors are generated (based on your prefix `u` by default) that handles the dark mode automatically:</p>
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
<code class="text-sm">
{{ code3 }}
</code>
</pre>
<p>For example `u-bg-gray-100` is a shortcut for `bg-gray-100 dark:bg-gray-800`. Take a look at the <a href="https://github.com/nuxtlabs/ui/blob/dev/src/index.ts#L61" target="_blank" class="underline">shortcuts definitions</a>.</p>
</div>
</template>
<script setup>
const code1 = `
const colorMode = useColorMode()
const toggleDark = () => {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}`
const code3 = `
<div class="u-bg-gray-100 border u-border-gray-200 u-text-gray-700"></div>
`
const code4 = `
<div class="bg-white dark:bg-black"></div>
`
</script>

View File

@@ -1,351 +0,0 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="space-y-4">
<div class="pb-10 border-b u-border-gray-200 mb-10">
<div>
<h1 class="inline-block text-3xl font-extrabold u-text-gray-900 tracking-tight">
Examples
</h1>
</div>
<p class="mt-1 text-lg u-text-gray-500">
Examples of real-life usage of components.
</p>
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Avatar:
</div>
<UAvatar src="https://picsum.photos/200/300" />
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Button:
</div>
<UButton variant="primary" icon="i-heroicons-envelope">
Button text
</UButton>
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Modal:
</div>
<UButton @click="toggleModalIsOpen()">
Toggle modal!
</UButton>
<UModal v-model="isModalOpen" @submit.prevent="onSubmit">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<UIcon name="i-heroicons-exclamation" class="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium u-text-gray-900">
Deactivate account
</h3>
<div class="mt-2">
<p class="text-sm u-text-gray-500">
Are you sure you want to deactivate your account? All of your data will be permanently removed from our servers forever. This action cannot be undone.
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm" @click="open = false">
Deactivate
</button>
<button ref="cancelButtonRef" type="button" class="mt-3 w-full inline-flex justify-center rounded-md border u-border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium u-text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm" @click="open = false">
Cancel
</button>
</div>
</UModal>
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Dropdown:
</div>
<UDropdown v-slot="{ open }" :items="dropdownItems" placement="bottom-start">
<UButton variant="white" :icon="open ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'" trailing icon-class="transition">
Open menu!
</UButton>
</UDropdown>
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Dropdown with avatar:
</div>
<UDropdown :items="customDropdownItems" placement="bottom-end">
<button class="flex">
<UAvatar src="https://picsum.photos/200/300" />
</button>
</UDropdown>
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Popover:
</div>
<UPopover wrapper-class="inline-block relative">
<template #default="{ open }">
<UButton variant="secondary" :icon="open ? 'i-heroicons-chevron-up-20-solid-20' : 'i-heroicons-chevron-down-20-solid'" trailing icon-class="transition">
Open popover!
</UButton>
</template>
<template #panel>
<div class="p-2">
Panel
</div>
</template>
</UPopover>
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Tooltip:
</div>
<UTooltip text="Hello tooltip!" :shortcuts="['⌘', 'G']">
<UIcon name="i-heroicons-information-circle" class="w-6 h-6 u-text-gray-900 cursor-pointer" />
</UTooltip>
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Notifications:
</div>
<UButton icon="i-heroicons-bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Context menu:
</div>
<UCard class="relative" body-class="h-64" @click="isContextMenuOpen = false" @contextmenu.prevent="openContextMenu">
<UContextMenu v-model="isContextMenuOpen" :virtual-element="virtualElement" width-class="w-48">
<div class="p-2">
Menu
</div>
</UContextMenu>
</UCard>
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Command palette:
</div>
<UCard body-class="">
<UCommandPalette
:placeholder="false"
:options="{
fuseOptions: {
includeMatches: true
}
}"
:groups="commandPaletteGroups"
command-attribute="name"
/>
</UCard>
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Card:
</div>
<UCard body-class="flex">
<div class="flex-1 px-4 py-5 sm:p-6 space-y-3">
<UFormGroup label="Email" name="email" required>
<UInput v-model="form.email" type="email" name="email" required icon="i-heroicons-mail" />
</UFormGroup>
<UFormGroup label="Description" name="description">
<UTextarea v-model="form.description" type="description" name="description" autoresize />
</UFormGroup>
<UFormGroup label="Person" name="person" required>
<USelect
v-model="form.personId"
name="person"
:options="people"
placeholder="Select a person"
text-attribute="name"
value-attribute="id"
icon="i-heroicons-user"
/>
</UFormGroup>
<UFormGroup label="People" name="people" required>
<USelectCustom v-model="form.person" name="people" :options="people" text-attribute="name" searchable />
</UFormGroup>
<UFormGroup label="Toggle" name="toggle">
<UToggle v-model="form.toggle" name="toggle" icon-off="i-heroicons-x-mark-20-solid" icon-on="i-heroicons-check-20-solid" />
</UFormGroup>
<UFormGroup label="Notifications" label-class="text-base font-medium u-text-gray-900" description="How do you prefer to receive notifications?">
<div class="space-y-4 mt-3">
<URadio v-model="form.notification" value="email" label="Email" help="Email" />
<URadio v-model="form.notification" value="phone" label="Phone (SMS)" help="Phone (SMS)" />
<URadio v-model="form.notification" value="push" label="Push notification" help="Push notification" />
</div>
</UFormGroup>
<UCard body-class="px-4 py-5 space-y-5">
<UCheckbox v-model="form.notifications" name="comments" value="comments" label="Comments" help="Get notified when someones posts a comment on a posting." />
<UCheckbox v-model="form.notifications" name="candidates" value="candidates" label="Candidates" help="Get notified when a candidate applies for a job." />
<UCheckbox v-model="form.notifications" name="offers" value="offers" label="Offers" help="Get notified when a candidate accepts or rejects an offer." />
</UCard>
<div>
<UCheckbox v-model="form.terms" label="I agree to the terms and conditions" name="terms" />
</div>
<div class="flex justify-end">
<UButton type="submit" label="Submit" class="ml-auto" />
</div>
</div>
<div class="w-1/3 px-4 py-5 sm:p-6 border-l u-border-gray-200 u-bg-gray-50">
<pre class="whitespace-pre-wrap break-all">
{{ form }}
</pre>
</div>
</UCard>
</div>
</div>
</template>
<script setup>
const isModalOpen = ref(false)
const people = ref([
{ id: 1, name: 'Durward Reynolds', disabled: false },
{ id: 2, name: 'Kenton Towne', disabled: false },
{ id: 3, name: 'Therese Wunsch', disabled: false },
{ id: 4, name: 'Benedict Kessler', disabled: true },
{ id: 5, name: 'Katelyn Rohan', disabled: false }
])
const form = reactive({
email: '',
description: '',
toggle: false,
notification: 'email',
notifications: [],
terms: false,
personId: null,
person: ref(people.value[0]),
persons: ref([people.value[0]])
})
const { $toast } = useNuxtApp()
const x = ref(0)
const y = ref(0)
const isContextMenuOpen = ref(false)
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
const commandPaletteGroups = computed(() => ([{
key: 'people',
commands: people.value
}, {
key: 'search',
label: q => q && `Search results for "${q}"...`,
search: async (q) => {
if (!q) { return [] }
return await $fetch(`https://jsonplaceholder.typicode.com/users?q=${q}`)
}
}]))
onMounted(() => {
document.addEventListener('mousemove', ({ clientX, clientY }) => {
x.value = clientX
y.value = clientY
})
})
function openContextMenu () {
const top = unref(y)
const left = unref(x)
virtualElement.value.getBoundingClientRect = () => ({
width: 0,
height: 0,
top,
left
})
isContextMenuOpen.value = true
}
function toggleModalIsOpen () {
isModalOpen.value = !isModalOpen.value
}
function onClick () {
// eslint-disable-next-line no-console
console.warn('click')
}
function onSubmit () {
// eslint-disable-next-line no-console
console.warn('submit')
}
const dropdownItems = [
[{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
click: (e) => {
e.preventDefault()
onClick()
}
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid'
}],
[{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'i-heroicons-arrow-right-circle-20-solid',
to: 'https://www.google.fr',
target: '_blank'
}],
[{
label: 'Delete',
icon: 'i-heroicons-trash-20-solid'
}]
]
const customDropdownItems = [
[{
label: 'benjamincanac',
avatar: { src: 'https://picsum.photos/200/300' },
href: 'https://google.fr',
target: '_blank'
}],
[{
label: 'About',
icon: 'i-heroicons-plus-20-solid',
to: '/about'
}]
]
const onNotificationClick = () => {
$toast.error({ title: 'Error', description: 'This is an error message' })
}
</script>

View File

@@ -1,117 +1,7 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="space-y-4">
<div class="pb-10 border-b u-border-gray-200 mb-10">
<div>
<h1 class="inline-block text-3xl font-extrabold u-text-gray-900 tracking-tight">
Documentation
</h1>
</div>
<p class="mt-1 text-lg u-text-gray-500">
Components library as a Nuxt3 module using <a href="https://tailwindcss.com" target="_blank" class="underline">TailwindCSS</a>.
</p>
</div>
<h2 class="font-bold text-2xl u-text-gray-900">
Installation
</h2>
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
<code class="text-sm" v-html="code1" />
</pre>
<p>Then, register the module in your `nuxt.config.js`:</p>
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
<code class="text-sm" v-html="code2" />
</pre>
<p>If you want latest updates, please use `@nuxthq/ui-edge` in your `package.json`:</p>
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
<code class="text-sm" v-html="code3" />
</pre>
<h2 class="font-bold text-2xl u-text-gray-900">
Options
</h2>
<p>- `preset`</p>
<p>Choose preset. Defaults to `tailwindui`. An object can also be used to override some parts of the default preset.</p>
<p>- `prefix`</p>
<p>Define the prefix of the imported components. Defaults to `u`.</p>
<p class="font-medium">
Example:
</p>
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
<code class="text-sm" v-html="code4" />
</pre>
<p>- `colors.primary`</p>
<p>Define the primary variant. Defaults to `indigo`. You can specify your own object of colors like here:</p>
<p class="font-medium">
Example:
</p>
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
<code class="text-sm" v-html="code5" />
</pre>
<p>- `colors.gray`</p>
<p>Define the gray variant. Defaults to `zinc`. You can like the `primary` color specify your own object. https://tailwindcss.com/docs/customizing-colors#default-color-palette</p>
<p>- `tailwindcss.theme`. Defaults to `{}`.</p>
<p>Define TailwindCSS theme: https://tailwindcss.com/docs/theme.</p>
</div>
<div />
</template>
<script setup>
const code1 = `
yarn add --dev @nuxthq/ui`
const code2 = `
import { defineNuxtConfig } from 'nuxt'
defineNuxtConfig({
buildModules: [
'@nuxthq/ui'
]
})`
const code3 = `
{
"devDependencies": {
"@nuxthq/ui": "npm:@nuxthq/ui-edge@latest"
}
}`
const code4 = `
import { defineNuxtConfig } from 'nuxt'
defineNuxtConfig({
ui: {
prefix: 'tw'
}
})`
const code5 = `
import { defineNuxtConfig } from 'nuxt'
defineNuxtConfig({
ui: {
colors: {
primary: 'blue'
}
}
})`
<script setup lang="ts">
await navigateTo('/getting-started/installation')
</script>

View File

@@ -1,285 +0,0 @@
<template>
<div class="space-y-4">
<div class="pb-10 border-b u-border-gray-200 mb-10">
<div>
<h1 class="inline-block text-3xl font-extrabold u-text-gray-900 tracking-tight">
Migration
</h1>
</div>
<p class="mt-1 text-lg u-text-gray-500">
Check out the components that have been migrated to Vue3 coming from `@nuxthq/volta-ui`.
</p>
</div>
<UCard body-class>
<table class="min-w-full divide-y u-divide-gray-200">
<thead class="u-bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
Component
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
Nuxt3 ready
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
Composition API
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
Preset system
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
Typescript
</th>
</tr>
</thead>
<tbody>
<tr v-for="(component, index) of components" :key="index" :class="index % 2 === 0 ? 'u-bg-white' : 'u-bg-gray-50'">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium u-text-gray-900">
<NuxtLink :to="component.to" class="hover:underline">
{{ component.label }}
</NuxtLink>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm u-text-gray-500">
<span v-if="component.nuxt3 || component.capi"></span>
<span v-else></span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm u-text-gray-500">
<span v-if="component.capi"></span>
<span v-else></span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm u-text-gray-500">
<span v-if="component.preset"></span>
<span v-else></span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm u-text-gray-500">
<span v-if="component.typescript"></span>
<span v-else></span>
</td>
</tr>
</tbody>
</table>
</UCard>
</div>
</template>
<script setup>
const components = [
{
label: 'Avatar',
to: '/components/Avatar',
nuxt3: true,
preset: true,
capi: true,
typescript: true
},
{
label: 'AvatarGroup',
to: '/components/AvatarGroup',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Badge',
to: '/components/Badge',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Button',
to: '/components/Button',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Dropdown',
to: '/components/Dropdown',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Link',
to: '/components/Link',
nuxt3: true,
capi: true,
typescript: true
},
{
label: 'Toggle',
to: '/components/Toggle',
nuxt3: true,
preset: true,
capi: true,
typescript: true
},
{
label: 'Alert',
to: '/components/Alert',
nuxt3: true,
capi: true,
typescript: true
},
{
label: 'AlertDialog',
to: '/components/AlertDialog',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Input',
to: '/components/Input',
capi: true,
preset: true,
typescript: true
},
{
label: 'FormGroup',
to: '/components/FormGroup',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Checkbox',
to: '/components/Checkbox',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Radio',
to: '/components/Radio',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Select',
to: '/components/Select',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'SelectCustom',
to: '/components/SelectCustom',
capi: true,
preset: true,
typescript: true
},
{
label: 'Textarea',
to: '/components/Textarea',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Card',
to: '/components/Card',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Container',
to: '/components/Container',
nuxt3: true,
preset: true,
capi: true,
typescript: true
},
{
label: 'CommandPalette',
to: '/components/CommandPalette',
nuxt3: true,
capi: true,
preset: false,
typescript: true
},
{
label: 'Tabs',
to: '/components/Tabs',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Pills',
to: '/components/Pills',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'VerticalNavigation',
to: '/components/VerticalNavigation',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Modal',
to: '/components/Modal',
nuxt3: true,
preset: true,
capi: true,
typescript: true
},
{
label: 'Notification',
to: '/components/Notification',
nuxt3: true,
capi: true,
typescript: true
},
{
label: 'Notifications',
to: '/components/Notifications',
nuxt3: true,
capi: true,
typescript: true
},
{
label: 'Popover',
to: '/components/Popover',
nuxt3: true,
capi: true,
typescript: true
},
{
label: 'Slideover',
to: '/components/Slideover',
nuxt3: true,
capi: true,
preset: true,
typescript: true
},
{
label: 'Tooltip',
to: '/components/Tooltip',
nuxt3: true,
capi: true,
typescript: true
}
]
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="#0C0C0D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

187
docs/tailwind.config.ts Normal file
View File

@@ -0,0 +1,187 @@
import type { Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
export default <Partial<Config>> {
darkMode: 'class',
content: [
'docs/content/**/*.md'
],
theme: {
extend: {
fontFamily: {
sans: ['Inter var', ...defaultTheme.fontFamily.sans]
},
maxWidth: {
'8xl': '90rem'
},
typography: (theme) => {
return {
DEFAULT: {
css: {
'h1, h2, h3, h4': {
fontWeight: theme('fontWeight.bold'),
'scroll-margin-top': 'var(--scroll-mt)'
},
'h1 a, h2 a, h3 a, h4 a': {
borderBottom: 'none !important',
color: 'inherit',
fontWeight: 'inherit'
},
a: {
fontWeight: theme('fontWeight.medium'),
textDecoration: 'none',
borderBottom: '1px solid transparent'
},
'a:hover': {
borderColor: 'var(--tw-prose-links)'
},
'a:has(> code)': {
borderColor: 'transparent !important'
},
'a code': {
color: 'var(--tw-prose-code)',
border: '1px dashed var(--tw-prose-pre-border)'
},
'a:hover code': {
color: 'var(--tw-prose-links)',
borderColor: 'var(--tw-prose-links)'
},
pre: {
margin: '0',
borderRadius: '0.375rem',
border: '1px solid var(--tw-prose-pre-border)',
whiteSpace: 'pre-wrap',
wordBreak: 'break-words'
},
code: {
backgroundColor: 'var(--tw-prose-pre-bg)',
padding: '0.25rem 0.375rem',
borderRadius: '0.375rem',
border: '1px solid var(--tw-prose-pre-border)'
},
'code::before': {
content: ''
},
'code::after': {
content: ''
},
'blockquote p:first-of-type::before': {
content: ''
},
'blockquote p:last-of-type::after': {
content: ''
},
'input[type="checkbox"]': {
color: 'rgb(var(--color-primary-500))',
borderRadius: theme('borderRadius.DEFAULT'),
borderColor: 'rgb(var(--color-gray-300))',
height: theme('spacing.4'),
width: theme('spacing.4'),
marginTop: '-3.5px !important',
marginBottom: '0 !important',
'&:focus': {
'--tw-ring-offset-width': 0
}
},
'input[type="checkbox"]:checked': {
borderColor: 'rgb(var(--color-primary-500))'
},
'input[type="checkbox"]:disabled': {
opacity: 0.5,
cursor: 'not-allowed'
},
'ul.contains-task-list': {
marginLeft: '-1.625em'
},
'ul ul': {
paddingLeft: theme('padding.6')
},
'ul ol': {
paddingLeft: theme('padding.6')
},
'ul > li.task-list-item': {
paddingLeft: '0 !important'
},
'ul > li.task-list-item input': {
marginRight: '7px'
},
'ul > li.task-list-item > ul.contains-task-list': {
marginLeft: 'initial'
},
'ul > li.task-list-item a': {
marginBottom: 0
},
'ul > li.task-list-item::marker': {
content: 'none'
},
'ul > li > p': {
margin: 0
},
'ul > li > span.issue-badge, p > span.issue-badge': {
verticalAlign: 'text-top',
margin: '0 !important'
},
'ul > li > button': {
verticalAlign: 'baseline !important'
},
table: {
wordBreak: 'break-all'
}
}
},
primary: {
css: {
'--tw-prose-body': 'rgb(var(--color-gray-700))',
'--tw-prose-headings': 'rgb(var(--color-gray-900))',
'--tw-prose-lead': 'rgb(var(--color-gray-600))',
'--tw-prose-links': 'rgb(var(--color-primary-500))',
'--tw-prose-bold': 'rgb(var(--color-gray-900))',
'--tw-prose-counters': 'rgb(var(--color-gray-500))',
'--tw-prose-bullets': 'rgb(var(--color-gray-300))',
'--tw-prose-hr': 'rgb(var(--color-gray-100))',
'--tw-prose-quotes': 'rgb(var(--color-gray-900))',
'--tw-prose-quote-borders': 'rgb(var(--color-gray-200))',
'--tw-prose-captions': 'rgb(var(--color-gray-500))',
'--tw-prose-code': 'rgb(var(--color-gray-900))',
'--tw-prose-pre-code': 'rgb(var(--color-gray-900))',
'--tw-prose-pre-bg': 'rgb(var(--color-gray-50))',
'--tw-prose-pre-border': 'rgb(var(--color-gray-200))',
'--tw-prose-th-borders': 'rgb(var(--color-gray-300))',
'--tw-prose-td-borders': 'rgb(var(--color-gray-200))',
'--tw-prose-invert-body': 'rgb(var(--color-gray-200))',
'--tw-prose-invert-headings': theme('colors.white'),
'--tw-prose-invert-lead': 'rgb(var(--color-gray-400))',
'--tw-prose-invert-links': 'rgb(var(--color-primary-400))',
'--tw-prose-invert-bold': theme('colors.white'),
'--tw-prose-invert-counters': 'rgb(var(--color-gray-400))',
'--tw-prose-invert-bullets': 'rgb(var(--color-gray-600))',
'--tw-prose-invert-hr': 'rgb(var(--color-gray-800))',
'--tw-prose-invert-quotes': 'rgb(var(--color-gray-100))',
'--tw-prose-invert-quote-borders': 'rgb(var(--color-gray-700))',
'--tw-prose-invert-captions': 'rgb(var(--color-gray-400))',
'--tw-prose-invert-code': theme('colors.white'),
'--tw-prose-invert-pre-code': theme('colors.white'),
'--tw-prose-invert-pre-bg': 'rgb(var(--color-gray-800))',
'--tw-prose-invert-pre-border': 'rgb(var(--color-gray-700))',
'--tw-prose-invert-th-borders': 'rgb(var(--color-gray-700))',
'--tw-prose-invert-td-borders': 'rgb(var(--color-gray-800))'
}
},
invert: {
css: {
'--tw-prose-pre-border': 'var(--tw-prose-invert-pre-border)',
'input[type="checkbox"]': {
backgroundColor: 'rgb(var(--color-gray-800))',
borderColor: 'rgb(var(--color-gray-700))'
},
'input[type="checkbox"]:checked': {
backgroundColor: 'rgb(var(--color-primary-400))',
borderColor: 'rgb(var(--color-primary-400))'
}
}
}
}
}
}
}
}

3
docs/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@nuxthq/ui",
"version": "1.1.2",
"version": "2.0.2",
"repository": "https://github.com/nuxtlabs/ui",
"license": "MIT",
"exports": {
@@ -14,11 +14,14 @@
"files": [
"dist"
],
"engines": {
"node": ">=18 <19"
},
"scripts": {
"build": "nuxt-module-build",
"prepack": "yarn build",
"dev": "nuxi dev docs",
"build:docs": "nuxi generate docs",
"build:docs": "nuxi build docs",
"lint": "eslint --ext .ts,.js,.vue .",
"typecheck": "nuxi typecheck",
"prepare": "nuxi prepare docs",
@@ -26,33 +29,38 @@
},
"dependencies": {
"@egoist/tailwindcss-icons": "^1.0.7",
"@headlessui/vue": "^1.7.11",
"@headlessui/vue": "1.7.10",
"@iconify-json/heroicons": "^1.1.10",
"@nuxt/kit": "^3.2.2",
"@nuxt/kit": "^3.4.3",
"@nuxtjs/color-mode": "^3.2.0",
"@nuxtjs/tailwindcss": "^6.4.1",
"@popperjs/core": "^2.11.6",
"@nuxtjs/tailwindcss": "^6.6.8",
"@popperjs/core": "^2.11.7",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.9",
"@vueuse/core": "^9.13.0",
"@vueuse/integrations": "^9.13.0",
"@vueuse/core": "^10.1.2",
"@vueuse/integrations": "^10.1.2",
"@vueuse/math": "^10.1.2",
"defu": "^6.1.2",
"fuse.js": "^6.6.2",
"lodash-es": "^4.17.21",
"tailwindcss": "^3.2.7"
"tailwindcss": "^3.3.2"
},
"devDependencies": {
"@iconify-json/mdi": "^1.1.47",
"@nuxt/module-builder": "^0.2.1",
"@iconify-json/simple-icons": "^1.1.52",
"@nuxt/content": "^2.6.0",
"@nuxt/module-builder": "^0.3.1",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
"@types/lodash-es": "^4.17.6",
"@types/node": "^18.14.2",
"eslint": "^8.35.0",
"nuxt": "^3.2.2",
"@nuxtjs/plausible": "^0.2.1",
"@types/lodash-es": "^4.17.7",
"@types/node": "^20.1.2",
"@vueuse/nuxt": "^10.1.2",
"eslint": "^8.40.0",
"nuxt": "^3.4.3",
"nuxt-component-meta": "^0.5.1",
"nuxt-lodash": "^2.4.1",
"standard-version": "^9.5.0",
"unbuild": "^1.1.2",
"vue-tsc": "^1.2.0"
"unbuild": "^1.2.1",
"vue-tsc": "1.6.3"
}
}

6
src/env.d.ts vendored
View File

@@ -1,6 +0,0 @@
import type { DefaultPreset } from './runtime/presets/default'
declare module '#build/ui' {
declare const preset: DefaultPreset
export default preset
}

View File

@@ -1,11 +1,11 @@
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, addTemplate, addPlugin, createResolver } from '@nuxt/kit'
import { defu } from 'defu'
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, createResolver, addPlugin, resolvePath } from '@nuxt/kit'
import colors from 'tailwindcss/colors.js'
import type { Config } from 'tailwindcss'
import { iconsPlugin, getIconCollections } from '@egoist/tailwindcss-icons'
import { name, version } from '../package.json'
import { colorsAsRegex, excludeColors } from './runtime/utils/colors'
import defaultPreset from './runtime/presets/default'
import appConfig from './runtime/app.config'
type DeepPartial<T> = Partial<{ [P in keyof T]: DeepPartial<T[P]> | { [key: string]: string } }>
// @ts-ignore
delete colors.lightBlue
@@ -18,44 +18,28 @@ delete colors.coolGray
// @ts-ignore
delete colors.blueGray
interface ColorsOptions {
/**
* @default 'indigo'
*/
primary?: string
/**
* @default 'gray'
*/
gray?: string
declare module 'nuxt/schema' {
interface AppConfigInput {
ui?: {
primary?: string
gray?: string
colors?: string[]
} & DeepPartial<typeof appConfig.ui>
}
}
export interface ModuleOptions {
preset?: object
/**
* @default 'u'
*/
prefix?: string
colors?: ColorsOptions
/**
* @default false
*/
global?: boolean
icons: string[]
tailwindcss?: Partial<Config>
}
const defaults = {
preset: {},
prefix: 'u',
colors: {
primary: 'indigo',
gray: 'gray'
},
icons: ['heroicons'],
tailwindcss: {
theme: {}
}
}
export default defineNuxtModule<ModuleOptions>({
@@ -67,10 +51,11 @@ export default defineNuxtModule<ModuleOptions>({
nuxt: '^3.0.0-rc.8'
}
},
defaults,
defaults: {
prefix: 'u',
icons: ['heroicons']
},
async setup (options, nuxt) {
const { preset = {}, prefix, colors: { primary = 'indigo', gray = 'gray' } = {}, tailwindcss: { theme = {} } = {} } = options
const { resolve } = createResolver(import.meta.url)
// Transpile runtime
@@ -78,6 +63,13 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.build.transpile.push(runtimeDir)
nuxt.options.build.transpile.push('@popperjs/core', '@headlessui/vue')
nuxt.options.css.push(resolve(runtimeDir, 'ui.css'))
const appConfigFile = await resolvePath(resolve(runtimeDir, 'app.config'))
nuxt.hook('app:resolve', (app) => {
app.configs.push(appConfigFile)
})
// @ts-ignore
nuxt.hook('tailwindcss:config', function (tailwindConfig: TailwindConfig) {
const globalColors = {
@@ -85,107 +77,144 @@ export default defineNuxtModule<ModuleOptions>({
...tailwindConfig.theme.extend?.colors
}
// @ts-ignore
tailwindConfig.theme.extend.colors = tailwindConfig.theme.extend.colors || {}
// @ts-ignore
globalColors.primary = tailwindConfig.theme.extend.colors.primary = globalColors[primary] || colors[primary]
// @ts-ignore
globalColors.gray = tailwindConfig.theme.extend.colors.gray = globalColors[gray] || colors[gray]
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>)'
}
if (globalColors.gray) {
globalColors.cool = tailwindConfig.theme.extend.colors.cool = colors.gray
}
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 variantColors = excludeColors(globalColors)
const safeColorsAsRegex = colorsAsRegex(variantColors)
nuxt.options.appConfig.ui = {
...nuxt.options.appConfig.ui,
primary: 'sky',
gray: 'cool',
colors: variantColors
}
tailwindConfig.safelist = tailwindConfig.safelist || []
tailwindConfig.safelist.push(...[{
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`)
},
{
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(100|600|700)`),
variants: ['hover', 'disabled', 'dark']
},
{
pattern: new RegExp(`text-(${safeColorsAsRegex})-(100|800)`),
tailwindConfig.safelist.push(...['bg-gray-400', {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|400|500)`)
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-500`),
variants: ['disabled']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(400|950)`),
variants: ['dark']
},
{
pattern: new RegExp(`ring-(${safeColorsAsRegex})-(500)`),
variants: ['focus']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(500|900|950)`),
variants: ['dark:hover']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`),
variants: ['dark:disabled']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|100|600)`),
variants: ['hover']
}, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-500`),
variants: ['focus-visible']
}, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-500`),
variants: ['focus-visible']
}, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-400`),
variants: ['dark', 'dark:focus-visible']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-600`),
variants: ['hover']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-500`),
variants: ['dark:hover']
}])
tailwindConfig.plugins = tailwindConfig.plugins || []
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
const ui: object = defu(preset, defaultPreset(variantColors))
addTemplate({
filename: 'ui.mjs',
getContents: () => `export default ${JSON.stringify(ui)}`
})
addTemplate({
filename: 'ui.d.ts',
write: true,
getContents: () => 'declare const d: any; export default d;'
})
})
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
await installModule('@nuxtjs/tailwindcss', {
viewer: false,
exposeConfig: true,
config: {
darkMode: 'class',
theme,
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/line-clamp'),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/typography')
],
content: [
resolve(runtimeDir, 'components/**/*.{vue,js,ts}'),
resolve(runtimeDir, 'presets/*.{mjs,js,ts}')
],
safelist: [
'dark',
{
pattern: /rounded-(sm|md|lg|xl|2xl|3xl)/,
variants: ['sm']
}
resolve(runtimeDir, '*.{mjs,js,ts}')
]
},
cssPath: resolve(runtimeDir, 'tailwind.css')
}
})
addPlugin(resolve(runtimeDir, 'plugins', 'toast.client'))
addPlugin(resolve(runtimeDir, 'plugins', 'clipboard.client'))
addPlugin({
src: resolve(runtimeDir, 'plugins', 'colors')
})
addComponentsDir({
path: resolve(runtimeDir, 'components', 'elements'),
prefix,
watch: false
})
addComponentsDir({
path: resolve(runtimeDir, 'components', 'feedback'),
prefix,
prefix: options.prefix,
global: options.global,
watch: false
})
addComponentsDir({
path: resolve(runtimeDir, 'components', 'forms'),
prefix,
prefix: options.prefix,
global: options.global,
watch: false
})
addComponentsDir({
path: resolve(runtimeDir, 'components', 'layout'),
prefix,
prefix: options.prefix,
global: options.global,
watch: false
})
addComponentsDir({
path: resolve(runtimeDir, 'components', 'navigation'),
prefix,
prefix: options.prefix,
global: options.global,
watch: false
})
addComponentsDir({
path: resolve(runtimeDir, 'components', 'overlays'),
prefix,
prefix: options.prefix,
global: options.global,
watch: false
})

752
src/runtime/app.config.ts Normal file
View File

@@ -0,0 +1,752 @@
// Elements
const avatar = {
wrapper: 'relative inline-flex items-center justify-center',
background: 'bg-gray-100 dark:bg-gray-800',
rounded: 'rounded-full',
placeholder: 'text-xs font-medium leading-none text-gray-900 dark:text-white truncate',
size: {
'3xs': 'h-4 w-4 text-xs',
'2xs': 'h-5 w-5 text-xs',
xs: 'h-6 w-6 text-xs',
sm: 'h-8 w-8 text-sm',
md: 'h-10 w-10 text-md',
lg: 'h-12 w-12 text-lg',
xl: 'h-14 w-14 text-xl',
'2xl': 'h-16 w-16 text-2xl',
'3xl': 'h-20 w-20 text-3xl'
},
chip: {
base: 'absolute block rounded-full ring-1 ring-white dark:ring-gray-900',
position: {
'top-right': 'top-0 right-0',
'bottom-right': 'bottom-0 right-0',
'top-left': 'top-0 left-0',
'bottom-left': 'bottom-0 left-0'
},
variant: {
solid: 'bg-{color}-400'
},
size: {
'3xs': 'h-1 w-1',
'2xs': 'h-1 w-1',
xs: 'h-1.5 w-1.5',
sm: 'h-2 w-2',
md: 'h-2.5 w-2.5',
lg: 'h-3 w-3',
xl: 'h-3.5 w-3.5',
'2xl': 'h-3.5 w-3.5',
'3xl': 'h-4 w-4'
}
},
default: {
size: 'sm',
chipVariant: 'solid',
chipPosition: 'top-right'
}
}
const avatarGroup = {
wrapper: 'flex flex-row-reverse',
ring: 'ring-2 ring-white dark:ring-gray-900',
margin: '-mr-1.5 first:mr-0'
}
const badge = {
base: 'inline-flex items-center',
rounded: 'rounded-md',
font: 'font-medium',
size: {
xs: 'text-xs px-1.5 py-0.5',
sm: 'text-xs px-2 py-1',
md: 'text-sm px-2 py-1',
lg: 'text-sm px-2.5 py-1.5'
},
variant: {
solid: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-10 dark:ring-opacity-20'
},
default: {
size: 'sm',
variant: 'solid',
color: 'primary'
}
}
const button = {
base: 'focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0',
font: 'font-medium',
rounded: 'rounded-md',
size: {
'2xs': 'text-xs',
xs: 'text-xs',
sm: 'text-sm',
md: 'text-sm',
lg: 'text-base',
xl: 'text-base'
},
gap: {
'2xs': 'gap-x-1',
xs: 'gap-x-1.5',
sm: 'gap-x-2',
md: 'gap-x-2',
lg: 'gap-x-2',
xl: 'gap-x-2'
},
padding: {
'2xs': 'px-2 py-1',
xs: 'px-2.5 py-1.5',
sm: 'px-3 py-1.5',
md: 'px-3 py-2',
lg: 'px-4 py-2',
xl: 'px-4 py-3'
},
square: {
'2xs': 'p-[5px]',
xs: 'p-1.5',
sm: 'p-2',
md: 'p-2',
lg: 'p-2.5',
xl: 'p-3'
},
color: {
white: {
solid: 'shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-900 dark:text-white bg-white hover:bg-gray-50 disabled:bg-white dark:bg-gray-900 dark:hover:bg-gray-800/50 dark:disabled:bg-gray-900 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
ghost: 'text-gray-900 dark:text-white hover:bg-white dark:hover:bg-gray-900 focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
},
gray: {
solid: 'shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-700 dark:text-gray-200 bg-gray-50 hover:bg-gray-100 disabled:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700/50 dark:disabled:bg-gray-800 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
ghost: 'text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
link: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 underline-offset-4 hover:underline focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
},
black: {
solid: 'shadow-sm text-white dark:text-gray-900 bg-gray-900 hover:bg-gray-800 disabled:bg-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:disabled:bg-white focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
link: 'text-gray-900 dark:text-white underline-offset-4 hover:underline focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
}
},
variant: {
solid: 'shadow-sm text-white dark:text-gray-900 bg-{color}-500 hover:bg-{color}-600 disabled:bg-{color}-500 dark:bg-{color}-400 dark:hover:bg-{color}-500 dark:disabled:bg-{color}-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-{color}-500 dark:focus-visible:outline-{color}-400',
outline: 'ring-1 ring-inset ring-current text-{color}-500 dark:text-{color}-400 hover:bg-{color}-50 dark:hover:bg-{color}-950 focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
soft: 'text-{color}-500 dark:text-{color}-400 bg-{color}-50 hover:bg-{color}-100 dark:bg-{color}-950 dark:hover:bg-{color}-900 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
ghost: 'text-{color}-500 dark:text-{color}-400 hover:bg-{color}-50 dark:hover:bg-{color}-950 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
link: 'text-{color}-500 hover:text-{color}-600 dark:text-{color}-400 dark:hover:text-{color}-500 underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400'
},
icon: {
base: 'flex-shrink-0',
size: {
'2xs': 'h-3.5 w-3.5',
xs: 'h-4 w-4',
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-5 w-5',
xl: 'h-6 w-6'
}
},
default: {
size: 'sm',
variant: 'solid',
color: 'primary',
loadingIcon: 'i-heroicons-arrow-path-20-solid'
}
}
const buttonGroup = {
wrapper: 'inline-flex',
rounded: 'rounded-md',
shadow: 'shadow-sm'
}
const dropdown = {
wrapper: 'relative inline-flex text-left',
container: 'z-20',
width: 'w-48',
background: 'bg-white dark:bg-gray-800',
shadow: 'shadow-lg',
rounded: 'rounded-md',
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
base: 'focus:outline-none',
divide: 'divide-y divide-gray-200 dark:divide-gray-700',
padding: 'p-1',
item: {
base: 'group flex items-center gap-2 w-full',
rounded: 'rounded-md',
padding: 'px-2 py-1.5',
size: 'text-sm',
active: 'bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-white',
inactive: 'text-gray-700 dark:text-gray-200',
disabled: 'cursor-not-allowed opacity-50',
icon: {
base: 'flex-shrink-0 h-4 w-4',
active: 'text-gray-500 dark:text-gray-400',
inactive: 'text-gray-400 dark:text-gray-500'
},
avatar: {
base: 'flex-shrink-0',
size: '3xs'
},
shortcuts: 'hidden md:inline-flex flex-shrink-0 gap-0.5 ml-auto'
},
transition: {
enterActiveClass: 'transition duration-100 ease-out',
enterFromClass: 'transform scale-95 opacity-0',
enterToClass: 'transform scale-100 opacity-100',
leaveActiveClass: 'transition duration-75 ease-out',
leaveFromClass: 'transform scale-100 opacity-100',
leaveToClass: 'transform scale-95 opacity-0'
},
popper: {
placement: 'bottom-end',
strategy: 'fixed'
}
}
const kbd = {
base: 'inline-flex items-center justify-center text-gray-900 dark:text-white',
padding: 'px-1',
size: {
xs: 'h-4 min-w-[16px] text-[10px]',
sm: 'h-5 min-w-[20px] text-[11px]',
md: 'h-6 min-w-[24px] text-[12px]'
},
rounded: 'rounded',
font: 'font-medium font-sans',
background: 'bg-gray-100 dark:bg-gray-800',
ring: 'ring-1 ring-gray-300 dark:ring-gray-700 ring-inset',
default: {
size: 'sm'
}
}
// Forms
const input = {
wrapper: 'relative',
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none',
custom: '',
size: {
'2xs': 'text-xs',
xs: 'text-xs',
sm: 'text-sm',
md: 'text-sm',
lg: 'text-base',
xl: 'text-base'
},
gap: {
'2xs': 'gap-x-1',
xs: 'gap-x-1.5',
sm: 'gap-x-2',
md: 'gap-x-2',
lg: 'gap-x-2',
xl: 'gap-x-2'
},
padding: {
'2xs': 'px-2 py-1',
xs: 'px-2.5 py-1.5',
sm: 'px-3 py-1.5',
md: 'px-3 py-2',
lg: 'px-4 py-2',
xl: 'px-4 py-3'
},
leading: {
padding: {
'2xs': 'pl-[26px]',
xs: 'pl-8',
sm: 'pl-9',
md: 'pl-10',
lg: 'pl-11',
xl: 'pl-12'
}
},
trailing: {
padding: {
'2xs': 'pr-[26px]',
xs: 'pr-8',
sm: 'pr-9',
md: 'pr-10',
lg: 'pr-11',
xl: 'pr-12'
}
},
appearance: {
white: 'border-0 bg-white dark:bg-gray-900 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500',
gray: 'border-0 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500',
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none placeholder:text-gray-400 dark:placeholder:text-gray-500'
},
icon: {
base: 'text-gray-400 dark:text-gray-500',
size: {
'2xs': 'h-3.5 w-3.5',
xs: 'h-4 w-4',
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-5 w-5',
xl: 'h-6 w-6'
},
leading: {
wrapper: 'absolute inset-y-0 left-0 flex items-center pointer-events-none',
padding: {
'2xs': 'pl-2',
xs: 'pl-2.5',
sm: 'pl-3',
md: 'pl-3',
lg: 'pl-4',
xl: 'pl-4'
}
},
trailing: {
wrapper: 'absolute inset-y-0 right-0 flex items-center pointer-events-none',
padding: {
'2xs': 'pr-2',
xs: 'pr-2.5',
sm: 'pr-3',
md: 'pr-3',
lg: 'pr-4',
xl: 'pr-4'
}
}
},
default: {
size: 'sm',
appearance: 'white',
loadingIcon: 'i-heroicons-arrow-path-20-solid'
}
}
const inputGroup = {
wrapper: '',
label: 'block text-sm font-medium text-gray-700 dark:text-gray-200',
labelWrapper: 'flex content-center justify-between',
container: 'mt-1 relative',
required: 'text-red-400',
description: 'text-sm leading-5 text-gray-500 dark:text-gray-400',
hint: 'text-sm leading-5 text-gray-500 dark:text-gray-400',
help: 'mt-2 text-sm text-gray-500 dark:text-gray-400'
}
const textarea = {
...input,
default: {
size: 'sm',
appearance: 'white'
}
}
const select = {
...input,
default: {
size: 'sm',
appearance: 'white',
trailingIcon: 'i-heroicons-chevron-down-20-solid'
}
}
const selectMenu = {
wrapper: 'relative',
container: 'z-20',
width: 'w-full',
height: 'max-h-60',
base: 'relative focus:outline-none overflow-y-auto scroll-py-1',
background: 'bg-white dark:bg-gray-800',
shadow: 'shadow-lg',
rounded: 'rounded-md',
padding: 'p-1',
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
input: 'block w-[calc(100%+0.5rem)] focus:ring-transparent text-sm px-3 py-1.5 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border-0 border-b border-gray-200 dark:border-gray-700 focus:border-inherit sticky -top-1 -mt-1 mb-1 -mx-1 z-10 placeholder-gray-400 dark:placeholder-gray-500',
option: {
base: 'cursor-default select-none relative flex items-center justify-between gap-1',
rounded: 'rounded-md',
padding: 'px-2 py-1.5',
size: 'text-sm',
color: 'text-gray-900 dark:text-white',
container: 'flex items-center gap-2 min-w-0',
active: 'bg-gray-100 dark:bg-gray-900',
inactive: '',
selected: 'pr-7',
disabled: 'cursor-not-allowed opacity-50',
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
icon: {
base: 'flex-shrink-0 h-4 w-4',
active: 'text-gray-900 dark:text-white',
inactive: 'text-gray-400 dark:text-gray-500'
},
selectedIcon: {
wrapper: 'absolute inset-y-0 right-0 flex items-center',
padding: 'pr-2',
base: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0'
},
avatar: {
base: 'flex-shrink-0',
size: '3xs'
},
chip: {
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
}
},
transition: {
leaveActiveClass: 'transition ease-in duration-100',
leaveFromClass: 'opacity-100',
leaveToClass: 'opacity-0'
},
popper: {
placement: 'bottom-end'
},
default: {
selectedIcon: 'i-heroicons-check-20-solid',
trailingIcon: 'i-heroicons-chevron-down-20-solid'
}
}
const radio = {
wrapper: 'relative flex items-start',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus:ring-offset-white dark:focus:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent',
label: 'font-medium text-gray-700 dark:text-gray-200',
required: 'text-red-400',
help: 'text-gray-500 dark:text-gray-400'
}
const checkbox = {
...radio,
base: radio.base + ' rounded'
}
const toggle = {
base: 'relative inline-flex flex-shrink-0 h-5 w-9 border-2 border-transparent rounded-full cursor-pointer disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
active: 'bg-primary-500 dark:bg-primary-400',
inactive: 'bg-gray-200 dark:bg-gray-700',
container: {
base: 'pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
active: 'translate-x-4',
inactive: 'translate-x-0'
},
icon: {
base: 'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity',
active: 'opacity-100 ease-in duration-200',
inactive: 'opacity-0 ease-out duration-100',
on: 'h-3 w-3 text-primary-500 dark:text-primary-400',
off: 'h-3 w-3 text-gray-400 dark:text-gray-500'
}
}
// Layout
const card = {
base: 'overflow-hidden',
background: 'bg-white dark:bg-gray-900',
divide: 'divide-y divide-gray-200 dark:divide-gray-800',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
rounded: 'rounded-lg',
shadow: 'shadow',
body: {
base: '',
background: '',
padding: 'px-4 py-5 sm:p-6'
},
header: {
base: '',
background: '',
padding: 'px-4 py-5 sm:px-6'
},
footer: {
base: '',
background: '',
padding: 'px-4 py-4 sm:px-6'
}
}
const container = {
base: 'mx-auto',
padding: 'px-4 sm:px-6 lg:px-8',
constrained: 'max-w-7xl'
}
// Navigation
const verticalNavigation = {
wrapper: 'relative',
base: 'group flex items-center gap-2 text-sm font-medium rounded-md w-full relative focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:before:ring-inset focus-visible:before:ring-1 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 before:absolute before:inset-px before:rounded-md disabled:cursor-not-allowed disabled:opacity-75',
padding: 'px-3 py-1.5',
active: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800 ',
inactive: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50',
label: 'truncate relative',
icon: {
base: 'flex-shrink-0 w-4 h-4',
active: 'text-gray-700 dark:text-gray-200',
inactive: 'text-gray-400 dark:text-gray-500 group-hover:text-gray-700 dark:group-hover:text-gray-200'
},
avatar: {
base: 'flex-shrink-0',
size: '3xs'
},
badge: {
base: 'ml-auto inline-block py-0.5 px-2 text-xs rounded-md -mr-1 -my-0.5',
active: 'bg-white dark:bg-gray-900',
inactive: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 group-hover:bg-white dark:group-hover:bg-gray-900'
}
}
const commandPalette = {
wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800',
container: 'relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2',
input: {
wrapper: 'relative flex items-center',
base: 'w-full placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 text-gray-900 dark:text-white focus:ring-0',
padding: 'px-4',
height: 'h-12',
size: 'sm:text-sm',
icon: {
base: 'pointer-events-none absolute left-4 text-gray-400 dark:text-gray-500',
size: 'h-4 w-4',
padding: 'pl-10'
},
close: 'absolute right-4'
},
empty: {
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
label: 'text-sm text-center text-gray-900 dark:text-white',
queryLabel: 'text-sm text-center text-gray-900 dark:text-white',
icon: 'w-6 h-6 mx-auto text-gray-400 dark:text-gray-500 mb-4'
},
group: {
wrapper: 'p-2',
label: 'px-2 my-2 text-xs font-semibold text-gray-900 dark:text-white',
container: 'text-sm text-gray-700 dark:text-gray-200',
command: {
base: 'flex justify-between select-none items-center rounded-md px-2 py-1.5 gap-2 relative',
active: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white',
inactive: '',
label: 'flex items-center gap-1.5 min-w-0',
prefix: 'text-gray-400 dark:text-gray-500',
suffix: 'text-gray-400 dark:text-gray-500',
container: 'flex items-center gap-2 min-w-0',
icon: {
base: 'flex-shrink-0 w-4 h-4',
active: 'text-gray-900 dark:text-white',
inactive: 'text-gray-400 dark:text-gray-500'
},
selectedIcon: {
base: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0'
},
avatar: {
base: 'flex-shrink-0',
size: '3xs'
},
chip: {
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
},
disabled: 'opacity-50',
shortcuts: 'hidden md:inline-flex flex-shrink-0 gap-0.5'
},
active: 'flex-shrink-0 text-gray-500 dark:text-gray-400',
inactive: 'flex-shrink-0 text-gray-500 dark:text-gray-400'
},
default: {
icon: 'i-heroicons-magnifying-glass-20-solid',
empty: {
icon: 'i-heroicons-magnifying-glass-20-solid',
label: 'We couldn\'t find any items.',
queryLabel: 'We couldn\'t find any items with that term. Please try again.'
},
close: null,
selectedIcon: 'i-heroicons-check-20-solid'
}
}
// Overlays
const modal = {
wrapper: 'relative z-50',
inner: 'fixed inset-0 overflow-y-auto',
container: 'flex min-h-full items-end sm:items-center justify-center text-center',
padding: 'p-4 sm:p-0',
base: 'relative text-left overflow-hidden sm:my-8 w-full flex flex-col',
overlay: {
base: 'fixed inset-0 transition-opacity',
background: 'bg-gray-200/75 dark:bg-gray-800/75',
transition: {
enter: 'ease-out duration-300',
enterFrom: 'opacity-0',
enterTo: 'opacity-100',
leave: 'ease-in duration-200',
leaveFrom: 'opacity-100',
leaveTo: 'opacity-0'
}
},
background: 'bg-white dark:bg-gray-900',
ring: '',
rounded: 'rounded-lg',
shadow: 'shadow-xl',
width: 'sm:max-w-lg',
height: '',
transition: {
enter: 'ease-out duration-300',
enterFrom: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95',
enterTo: 'opacity-100 translate-y-0 sm:scale-100',
leave: 'ease-in duration-200',
leaveFrom: 'opacity-100 translate-y-0 sm:scale-100',
leaveTo: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
}
}
const slideover = {
wrapper: 'fixed inset-0 flex z-50',
overlay: {
base: 'fixed inset-0 transition-opacity',
background: 'bg-gray-200/75 dark:bg-gray-800/75',
transition: {
enter: 'ease-in-out duration-500',
enterFrom: 'opacity-0',
enterTo: 'opacity-100',
leave: 'ease-in-out duration-500',
leaveFrom: 'opacity-100',
leaveTo: 'opacity-0'
}
},
base: 'relative flex-1 flex flex-col w-full focus:outline-none',
background: 'bg-white dark:bg-gray-900',
ring: '',
rounded: '',
padding: '',
shadow: 'shadow-xl',
width: 'w-screen max-w-md',
transition: {
enter: 'transform transition ease-in-out duration-300',
leave: 'transform transition ease-in-out duration-200'
}
}
const tooltip = {
wrapper: 'relative inline-flex',
container: 'z-20',
width: 'max-w-xs',
background: 'bg-white dark:bg-gray-900',
shadow: 'shadow',
rounded: 'rounded',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
base: 'invisible lg:visible h-6 px-2 py-1 text-xs font-normal truncate',
shortcuts: 'hidden md:inline-flex flex-shrink-0 gap-0.5',
transition: {
enterActiveClass: 'transition ease-out duration-200',
enterFromClass: 'opacity-0 translate-y-1',
enterToClass: 'opacity-100 translate-y-0',
leaveActiveClass: 'transition ease-in duration-150',
leaveFromClass: 'opacity-100 translate-y-0',
leaveToClass: 'opacity-0 translate-y-1'
},
popper: {
strategy: 'fixed'
}
}
const popover = {
wrapper: 'relative',
container: 'z-20',
width: '',
background: 'bg-white dark:bg-gray-900',
shadow: 'shadow-lg',
rounded: 'rounded-md',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
base: 'overflow-hidden focus:outline-none',
transition: {
enterActiveClass: 'transition ease-out duration-200',
enterFromClass: 'opacity-0 translate-y-1',
enterToClass: 'opacity-100 translate-y-0',
leaveActiveClass: 'transition ease-in duration-150',
leaveFromClass: 'opacity-100 translate-y-0',
leaveToClass: 'opacity-0 translate-y-1'
},
popper: {
strategy: 'fixed'
}
}
const contextMenu = {
wrapper: 'relative',
container: 'z-20',
width: '',
background: 'bg-white dark:bg-gray-900',
shadow: 'shadow-lg',
rounded: 'rounded-md',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
base: 'overflow-hidden focus:outline-none',
transition: {
enterActiveClass: 'transition ease-out duration-200',
enterFromClass: 'opacity-0 translate-y-1',
enterToClass: 'opacity-100 translate-y-0',
leaveActiveClass: 'transition ease-in duration-150',
leaveFromClass: 'opacity-100 translate-y-0',
leaveToClass: 'opacity-0 translate-y-1'
},
popper: {
placement: 'bottom-start',
scroll: false
}
}
const notification = {
wrapper: 'w-full pointer-events-auto',
container: 'relative overflow-hidden',
title: 'text-sm font-medium text-gray-900 dark:text-white',
description: 'mt-1 text-sm leading-5 text-gray-500 dark:text-gray-400',
background: 'bg-white dark:bg-gray-900',
shadow: 'shadow-lg',
rounded: 'rounded-lg',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
icon: 'flex-shrink-0 w-5 h-5 text-gray-900 dark:text-white',
avatar: 'flex-shrink-0 pt-0.5',
progress: 'absolute bottom-0 left-0 right-0 h-1 bg-primary-500 dark:bg-primary-400',
transition: {
enterActiveClass: 'transform ease-out duration-300 transition',
enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2',
enterToClass: 'translate-y-0 opacity-100 sm:translate-x-0',
leaveActiveClass: 'transition ease-in duration-100',
leaveFromClass: 'opacity-100',
leaveToClass: 'opacity-0'
},
default: {
close: {
icon: 'i-heroicons-x-mark-20-solid',
color: 'gray',
variant: 'link',
padded: false
},
action: {
size: 'xs',
color: 'white'
}
}
}
const notifications = {
wrapper: 'fixed bottom-0 right-0 flex flex-col justify-end w-full z-[55] sm:w-96',
container: 'px-4 sm:px-6 py-6 space-y-3 overflow-y-auto'
}
export default {
ui: {
avatar,
avatarGroup,
badge,
button,
buttonGroup,
dropdown,
kbd,
input,
inputGroup,
textarea,
select,
selectMenu,
checkbox,
radio,
toggle,
card,
container,
verticalNavigation,
commandPalette,
modal,
slideover,
popover,
tooltip,
contextMenu,
notification,
notifications
}
}

View File

@@ -1,127 +1,135 @@
<template>
<span :class="wrapperClass">
<img v-if="url && !error" :class="avatarClass" :src="url" :alt="alt" :onerror="() => onError()">
<span v-else-if="text || placeholder" :class="placeholderClass">{{ text || placeholder }}</span>
<span v-else-if="text || placeholder" :class="ui.placeholder">{{ text || placeholder }}</span>
<span v-if="chip" :class="chipClass" />
<span v-if="chipColor" :class="chipClass" />
<slot />
</span>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { classNames } from '../../utils'
import $ui from '#build/ui'
const props = defineProps({
src: {
type: [String, Boolean],
default: null
},
alt: {
type: String,
default: null
},
text: {
type: String,
default: null
},
size: {
type: String,
default: 'md',
validator (value: string) {
return Object.keys($ui.avatar.size).includes(value)
}
},
rounded: {
type: Boolean,
default: true
},
chip: {
type: Boolean,
default: false
},
chipVariant: {
type: String,
default: 'primary',
validator (value: string) {
return Object.keys($ui.avatar.chip.variant).includes(value)
}
},
chipPosition: {
type: String,
default: 'top-right',
validator (value: string) {
return Object.keys($ui.avatar.chip.position).includes(value)
}
},
wrapperClass: {
type: String,
default: () => $ui.avatar.wrapper
},
backgroundClass: {
type: String,
default: () => $ui.avatar.background
},
placeholderClass: {
type: String,
default: () => $ui.avatar.placeholder
},
roundedClass: {
type: String,
default: () => $ui.avatar.rounded
}
})
const wrapperClass = computed(() => {
return classNames(
props.wrapperClass,
props.backgroundClass,
$ui.avatar.size[props.size],
props.rounded ? 'rounded-full' : props.roundedClass
)
})
const avatarClass = computed(() => {
return classNames(
$ui.avatar.size[props.size],
props.rounded ? 'rounded-full' : props.roundedClass
)
})
const chipClass = computed(() => {
return classNames(
$ui.avatar.chip.base,
$ui.avatar.chip.position[props.chipPosition],
$ui.avatar.chip.variant[props.chipVariant],
$ui.avatar.chip.size[props.size]
)
})
const url = computed(() => {
if (typeof props.src === 'boolean') {
return null
}
return props.src
})
const placeholder = computed(() => {
return (props.alt || '').split(' ').map(word => word.charAt(0)).join('').substring(0, 2)
})
const error = ref(false)
watch(() => props.src, () => {
if (error.value) {
error.value = false
}
})
function onError () {
error.value = true
}
</script>
<script lang="ts">
export default { name: 'UAvatar' }
import { defineComponent, ref, computed, watch } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
props: {
src: {
type: [String, Boolean],
default: null
},
alt: {
type: String,
default: null
},
text: {
type: String,
default: null
},
size: {
type: String,
default: () => appConfig.ui.avatar.default.size,
validator (value: string) {
return Object.keys(appConfig.ui.avatar.size).includes(value)
}
},
chipColor: {
type: String,
default: null,
validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value)
}
},
chipVariant: {
type: String,
default: () => appConfig.ui.avatar.default.chipVariant,
validator (value: string) {
return Object.keys(appConfig.ui.avatar.chip.variant).includes(value)
}
},
chipPosition: {
type: String,
default: () => appConfig.ui.avatar.default.chipPosition,
validator (value: string) {
return Object.keys(appConfig.ui.avatar.chip.position).includes(value)
}
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.avatar>>,
default: () => appConfig.ui.avatar
}
},
setup (props) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.avatar>>(() => defu({}, props.ui, appConfig.ui.avatar))
const wrapperClass = computed(() => {
return classNames(
ui.value.wrapper,
ui.value.background,
ui.value.rounded,
ui.value.size[props.size]
)
})
const avatarClass = computed(() => {
return classNames(
ui.value.rounded,
ui.value.size[props.size]
)
})
const chipClass = computed(() => {
return classNames(
ui.value.chip.base,
ui.value.chip.size[props.size],
ui.value.chip.position[props.chipPosition],
ui.value.chip.variant[props.chipVariant]?.replaceAll('{color}', props.chipColor)
)
})
const url = computed(() => {
if (typeof props.src === 'boolean') {
return null
}
return props.src
})
const placeholder = computed(() => {
return (props.alt || '').split(' ').map(word => word.charAt(0)).join('').substring(0, 2)
})
const error = ref(false)
watch(() => props.src, () => {
if (error.value) {
error.value = false
}
})
function onError () {
error.value = true
}
return {
wrapperClass,
avatarClass,
chipClass,
url,
placeholder,
error,
onError
}
}
})
</script>

View File

@@ -0,0 +1,80 @@
import { h, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { classNames } from '../../utils'
import Avatar from './Avatar.vue'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
props: {
size: {
type: String,
default: null,
validator (value: string) {
return Object.keys(appConfig.ui.avatar.size).includes(value)
}
},
max: {
type: Number,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.avatarGroup>>,
default: () => appConfig.ui.avatarGroup
}
},
setup (props, { slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defu({}, props.ui, appConfig.ui.avatarGroup))
const children = computed(() => {
let children = slots.default?.()
// @ts-ignore-next
if (children.length && children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
return children
})
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max)
const clones = computed(() => children.value.map((node, index) => {
if (!props.max || (max.value && index < max.value)) {
if (props.size) {
node.props.size = props.size
}
node.props.class = node.props.class || ''
node.props.class += ` ${classNames(
ui.value.ring,
ui.value.margin
)}`
return node
}
if (max.value !== undefined && index === max.value) {
return h(Avatar, {
size: props.size,
text: `+${children.value.length - max.value}`,
class: classNames(
ui.value.ring,
ui.value.margin
)
})
}
return null
}).filter(Boolean).reverse())
return () => h('div', { class: ui.value.wrapper }, clones.value)
}
})

View File

@@ -1,79 +0,0 @@
<template>
<div class="flex flex-row-reverse">
<Avatar
v-if="remainingGroupSize > 0"
:size="size"
:text="`+${remainingGroupSize}`"
:class="avatarClass"
/>
<Avatar
v-for="(avatar, index) of displayedGroup"
:key="index"
v-bind="avatar"
:size="size"
:class="avatarClass"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { classNames } from '../../utils'
import Avatar from './Avatar.vue'
import $ui from '#build/ui'
const props = defineProps({
group: {
type: Array,
default: () => []
},
size: {
type: String,
default: 'md',
validator (value: string) {
return Object.keys($ui.avatar.size).includes(value)
}
},
max: {
type: Number,
default: null
},
ringClass: {
type: String,
default: () => $ui.avatarGroup.ring
},
marginClass: {
type: String,
default: () => $ui.avatarGroup.margin
}
})
const avatars = computed(() => {
return props.group.map((avatar) => {
return typeof avatar === 'string' ? { src: avatar } : avatar
})
})
const displayedGroup = computed(() => {
if (!props.max) { return [...avatars.value].reverse() }
return avatars.value.slice(0, props.max).reverse()
})
const remainingGroupSize = computed(() => {
if (!props.max) { return 0 }
return avatars.value.length - props.max
})
const avatarClass = computed(() => {
return classNames(
props.ringClass,
props.marginClass
)
})
</script>
<script lang="ts">
export default { name: 'UAvatarGroup' }
</script>

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