Compare commits

...

97 Commits

Author SHA1 Message Date
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
Benjamin Canac
9719ea3858 chore(release): 1.1.2 2023-02-28 11:58:28 +01:00
Benjamin Canac
a4af6b3805 chore(deps): bump 2023-02-28 10:50:50 +01:00
Benjamin Canac
3493c138d9 chore: split props for Dropdown, ContextMenu and Popover 2023-02-24 17:33:57 +01:00
Benjamin Canac
d08e64d53f fix(Tooltip): truncate 2023-02-22 15:59:02 +01:00
renovate[bot]
6aecb082d2 chore(deps): update devdependency vue-tsc to ^1.1.7 (#132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-22 12:31:21 +01:00
Sylvain Marroufin
63e27f8b4b chore(docs): add ContextMenu component page (#128) 2023-02-22 12:31:08 +01:00
Benjamin Canac
7970aefcb0 fix(VerticalNavigation): links to type 2023-02-22 12:22:54 +01:00
renovate[bot]
a893d7fa2e chore(deps): update all non-major dependencies (#124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-21 19:26:03 +01:00
Benjamin Canac
8ace629ff8 chore(release): 1.1.1 2023-02-20 18:06:15 +01:00
Benjamin Canac
0d35b82ecb chore(CommandPalette): expose query 2023-02-20 18:05:52 +01:00
Benjamin Canac
5f37077835 types(CommandPalette): options no longer exists 2023-02-20 18:05:39 +01:00
Benjamin Canac
948f4b89b1 chore(release): 1.1.0 2023-02-17 19:08:09 +01:00
Benjamin Canac
e6d0dd5898 chore(CommandPalette): set debounce to 200 2023-02-17 19:07:38 +01:00
Benjamin Canac
4702a4f103 fix(CommandPalette): types 2023-02-17 18:14:11 +01:00
Benjamin Canac
efa9674815 feat(CommandPalette): handle async search for specific groups 2023-02-17 18:03:59 +01:00
Benjamin Canac
97d40395d3 chore(release): 1.0.0 2023-02-17 15:35:51 +01:00
Benjamin Canac
a2fb22d835 docs: ts-ignore module import 2023-02-17 14:53:35 +01:00
Benjamin Canac
d14a1a82c2 chore(deps): bump @egoist/tailwindcss-icons 2023-02-17 14:50:41 +01:00
Benjamin Canac
a566627a23 chore: bump deps 2023-02-17 14:26:20 +01:00
Benjamin Canac
ee3352278c feat: migrate to @egoist/tailwindcss-icons 2023-02-17 12:58:19 +01:00
Benjamin Canac
b1d9e01818 chore(release): 0.2.1 2023-02-16 17:02:23 +01:00
Benjamin Canac
ca171f3095 chore(ci): disable typecheck 2023-02-16 17:02:07 +01:00
Benjamin Canac
c0e493d96a chore(deps): bump nuxt-icon 2023-02-16 17:02:00 +01:00
Benjamin Canac
d0d3235860 chore(release): 0.2.0 2023-02-16 16:17:24 +01:00
Benjamin Canac
18915975be chore(release): 0.1.39 2023-02-16 16:16:57 +01:00
Benjamin Canac
f5d068be9d feat: use nuxt-icon 2023-02-16 16:16:37 +01:00
Benjamin Canac
6018f009a8 fix(SelectCustom): handle search on string arrays 2023-02-16 12:02:38 +01:00
Benjamin Canac
2b78b5d7dc chore(SelectCustom): improve options type 2023-02-16 11:53:19 +01:00
Benjamin Canac
87f3f0b4c0 chore(release): 0.1.38 2023-02-03 18:09:49 +01:00
Benjamin Canac
41bf56f2ae chore(tsconfig): remove override 2023-02-03 18:04:30 +01:00
Benjamin Canac
b7795f4ef6 chore(deps): add @types/node 2023-02-03 18:04:19 +01:00
Benjamin Canac
57f8145a8d chore(deps): bump 2023-02-03 17:51:06 +01:00
Benjamin Canac
70fbcb6b24 chore(release): 0.1.37 2023-02-03 16:02:21 +01:00
Kevin Marrec
bea47b5906 fix(CommandPalette): improve accessibility (#129) 2023-02-03 16:01:43 +01:00
Benjamin Canac
fc1b3b2f17 chore(release): 0.1.36 2023-02-02 15:10:25 +01:00
Benjamin Canac
5bf5a314c4 fix(CommandPalette): put back cursor on top only when query changes 2023-02-01 15:20:21 +01:00
Benjamin Canac
3558eb1a4f chore(release): 0.1.35 2023-02-01 14:38:15 +01:00
Benjamin Canac
1c4d46e056 fix(Dropdown): lint 2023-02-01 14:38:07 +01:00
Benjamin Canac
1b0ed9e732 docs: simplify popover panel template 2023-02-01 14:37:55 +01:00
Benjamin Canac
b72037a777 chore: handle disabled prop for Dropdown and Popover 2023-02-01 14:34:28 +01:00
Benjamin Canac
a7644860b8 fix(Dropdown): prevent panel display when no items 2023-01-31 12:33:27 +01:00
Benjamin Canac
c90cd9c4f3 fix(AvatarGroup): preset size prop 2023-01-28 01:49:24 +01:00
Benjamin Canac
7805168685 chore(release): 0.1.34 2023-01-27 14:10:46 +01:00
Benjamin Canac
27717a55b3 fix(CommandPalette): typecheck 2023-01-27 14:10:35 +01:00
Benjamin Canac
d651a22dce chore(release): 0.1.33 2023-01-27 13:02:18 +01:00
Benjamin Canac
c3ecbf4b20 chore(CommandPalette): start preset 2023-01-27 13:02:01 +01:00
Benjamin Canac
d8b10f3eef chore(release): 0.1.32 2023-01-23 14:24:36 +01:00
Benjamin Canac
1071b80b39 chore(deps): bump 2023-01-23 14:24:20 +01:00
Benjamin Canac
c5e9a1ef46 chore(release): 0.1.31 2023-01-17 15:35:38 +01:00
Benjamin Canac
afe69a570d chore(deps): bump 2023-01-17 15:35:16 +01:00
Benjamin Canac
e6ed834cea chore(release): 0.1.30 2023-01-17 15:00:54 +01:00
Benjamin Canac
30c5412a6b chore(CommandPalette): input close icon position 2023-01-17 15:00:21 +01:00
Benjamin Canac
01f56d9553 chore(Button): handle compact 2023-01-17 15:00:21 +01:00
Benjamin Canac
91f273c117 chore(release): 0.1.29 2023-01-17 15:00:19 +01:00
Benjamin Canac
cda8ce32a3 chore(release): 0.1.28 2023-01-13 18:33:18 +01:00
Benjamin Canac
2bc0eb05d1 chore(Slideover): emit event 2023-01-13 18:32:45 +01:00
Benjamin Canac
cfc4bdfbfe chore(release): 0.1.27 2023-01-12 15:33:35 +01:00
Sylvain Marroufin
370d05921d chore(lighthouse): improve CommandPalette 2023-01-12 14:28:29 +01:00
Sylvain Marroufin
b6455a151d chore(lighthouse): improve components accessibility (#127) 2023-01-12 12:32:42 +01:00
Benjamin Canac
8c0e0ec823 chore(release): 0.1.26 2023-01-09 18:43:45 +01:00
Sylvain Marroufin
4f56921096 fix(CommandPalette): select first item on search changes (#126) 2023-01-09 18:43:12 +01:00
Benjamin Canac
6a5ee32e05 chore(release): 0.1.25 2023-01-09 18:42:53 +01:00
Benjamin Canac
4ea07e1077 chore(release): 0.1.24 2023-01-04 12:17:13 +01:00
Benjamin Canac
5fd65d0917 chore(deps): bump 2023-01-04 11:39:24 +01:00
Benjamin Canac
2ec0cee1d9 chore(release): 0.1.23 2022-12-20 16:27:30 +01:00
Benjamin Canac
758e6f1400 Revert "chore: put back stop propagation on mode hover (#121)"
This reverts commit c015148f29.
2022-12-20 16:26:55 +01:00
Benjamin Canac
275fa1831d chore(release): 0.1.22 2022-12-19 18:27:36 +01:00
Benjamin Canac
8b5e08f6f2 chore(Tooltip): add prevent prop 2022-12-19 18:27:19 +01:00
Benjamin Canac
1635f57de6 chore(release): 0.1.21 2022-12-19 16:22:41 +01:00
Sylvain Marroufin
b3e0122001 chore(SelectCustom): add emit on open/close (#125) 2022-12-19 16:22:15 +01:00
Benjamin Canac
4f9d20e603 chore(release): 0.1.20 2022-12-19 11:37:46 +01:00
Benjamin Canac
f7add47cf2 chore(deps): pin @headlessui/vue version to 1.7.4 2022-12-19 11:37:28 +01:00
renovate[bot]
99c1c683eb Update all non-major dependencies (#122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-17 16:57:12 +01:00
Anthony Fu
ff9f6c251d fix: avoid referring to complex types in props (#123) 2022-12-17 16:56:49 +01:00
Benjamin Canac
e0c703ca6c chore(release): 0.1.19 2022-12-16 10:47:39 +01:00
Benjamin Canac
2210faa160 chore(deps): bump @headlessui/vue 2022-12-16 10:47:20 +01:00
Benjamin Canac
19589b5e05 chore(release): 0.1.18 2022-12-15 14:45:48 +01:00
Benjamin Canac
7051fa39a7 chore: add missing w-full on headlessui buttons 2022-12-15 14:45:14 +01:00
renovate[bot]
911278e95e chore(deps): update all non-major dependencies (#120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-13 13:55:28 +01:00
Sylvain Marroufin
c015148f29 chore: put back stop propagation on mode hover (#121) 2022-12-13 13:54:23 +01:00
Benjamin Canac
cb1fd55801 chore(release): 0.1.17 2022-12-06 16:56:31 +01:00
Benjamin Canac
16fd1c0ca3 fix: remove stop propagation on mode hover 2022-12-06 16:56:16 +01:00
35 changed files with 2784 additions and 1192 deletions

View File

@@ -2,6 +2,153 @@
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.
### [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)
### Bug Fixes
* **Tooltip:** truncate ([d08e64d](https://github.com/nuxtlabs/ui/commit/d08e64d53fa439f34d51909bcb6812f1bcd95d83))
* **VerticalNavigation:** links `to` type ([7970aef](https://github.com/nuxtlabs/ui/commit/7970aefcb032ce01fcb11e9285fa61ce87f59519))
### [1.1.1](https://github.com/nuxtlabs/ui/compare/v1.1.0...v1.1.1) (2023-02-20)
## [1.1.0](https://github.com/nuxtlabs/ui/compare/v1.0.0...v1.1.0) (2023-02-17)
### Features
* **CommandPalette:** handle async search for specific groups ([efa9674](https://github.com/nuxtlabs/ui/commit/efa9674815ab4de756079690da0a381c3703d564))
### Bug Fixes
* **CommandPalette:** types ([4702a4f](https://github.com/nuxtlabs/ui/commit/4702a4f10379201c167cc52099519778756a5780))
## [1.0.0](https://github.com/nuxtlabs/ui/compare/v0.2.1...v1.0.0) (2023-02-17)
### Features
* migrate to `@egoist/tailwindcss-icons` ([ee33522](https://github.com/nuxtlabs/ui/commit/ee3352278cf03fdd12f2a4663b403052de3f089a))
### [0.2.1](https://github.com/nuxtlabs/ui/compare/v0.2.0...v0.2.1) (2023-02-16)
## [0.2.0](https://github.com/nuxtlabs/ui/compare/v0.1.39...v0.2.0) (2023-02-16)
### [0.1.39](https://github.com/nuxtlabs/ui/compare/v0.1.38...v0.1.39) (2023-02-16)
### Features
* use `nuxt-icon` ([f5d068b](https://github.com/nuxtlabs/ui/commit/f5d068be9d5778b3d4fcdc11d06d9d765e62075d))
### Bug Fixes
* **SelectCustom:** handle search on string arrays ([6018f00](https://github.com/nuxtlabs/ui/commit/6018f009a86cca196d15e4e72dd5eb41aaeb4bad))
### [0.1.38](https://github.com/nuxtlabs/ui/compare/v0.1.37...v0.1.38) (2023-02-03)
### [0.1.37](https://github.com/nuxtlabs/ui/compare/v0.1.36...v0.1.37) (2023-02-03)
### Bug Fixes
* **CommandPalette:** improve accessibility ([#129](https://github.com/nuxtlabs/ui/issues/129)) ([bea47b5](https://github.com/nuxtlabs/ui/commit/bea47b5906d1bc665717830d6dc2f3ff2a0374f3))
### [0.1.36](https://github.com/nuxtlabs/ui/compare/v0.1.35...v0.1.36) (2023-02-02)
### Bug Fixes
* **CommandPalette:** put back cursor on top only when query changes ([5bf5a31](https://github.com/nuxtlabs/ui/commit/5bf5a314c414b96c656190719bd56acca10676f5))
### [0.1.35](https://github.com/nuxtlabs/ui/compare/v0.1.34...v0.1.35) (2023-02-01)
### Bug Fixes
* **AvatarGroup:** preset size prop ([c90cd9c](https://github.com/nuxtlabs/ui/commit/c90cd9c4f37bc3ce5f6e13f3279dc2c574c76524))
* **Dropdown:** lint ([1c4d46e](https://github.com/nuxtlabs/ui/commit/1c4d46e056adf84d69462a12af8ac29f93cbf87a))
* **Dropdown:** prevent panel display when no items ([a764486](https://github.com/nuxtlabs/ui/commit/a7644860b8c22a0163e01ca2c0eab2c48b09745a))
### [0.1.34](https://github.com/nuxtlabs/ui/compare/v0.1.33...v0.1.34) (2023-01-27)
### Bug Fixes
* **CommandPalette:** typecheck ([27717a5](https://github.com/nuxtlabs/ui/commit/27717a55b3e5120f32fba2bcea30f5a91262f1c5))
### [0.1.33](https://github.com/nuxtlabs/ui/compare/v0.1.32...v0.1.33) (2023-01-27)
### [0.1.32](https://github.com/nuxtlabs/ui/compare/v0.1.31...v0.1.32) (2023-01-23)
### [0.1.31](https://github.com/nuxtlabs/ui/compare/v0.1.30...v0.1.31) (2023-01-17)
### [0.1.30](https://github.com/nuxtlabs/ui/compare/v0.1.29...v0.1.30) (2023-01-17)
### [0.1.29](https://github.com/nuxtlabs/ui/compare/v0.1.28...v0.1.29) (2023-01-17)
### [0.1.28](https://github.com/nuxtlabs/ui/compare/v0.1.27...v0.1.28) (2023-01-13)
### [0.1.27](https://github.com/nuxtlabs/ui/compare/v0.1.26...v0.1.27) (2023-01-12)
### [0.1.26](https://github.com/nuxtlabs/ui/compare/v0.1.25...v0.1.26) (2023-01-09)
### Bug Fixes
* **CommandPalette:** select first item on search changes ([#126](https://github.com/nuxtlabs/ui/issues/126)) ([4f56921](https://github.com/nuxtlabs/ui/commit/4f56921096f5885cdab8b7cb5c5aa01304188e11))
### [0.1.25](https://github.com/nuxtlabs/ui/compare/v0.1.24...v0.1.25) (2023-01-09)
### [0.1.24](https://github.com/nuxtlabs/ui/compare/v0.1.23...v0.1.24) (2023-01-04)
### [0.1.23](https://github.com/nuxtlabs/ui/compare/v0.1.22...v0.1.23) (2022-12-20)
### [0.1.22](https://github.com/nuxtlabs/ui/compare/v0.1.21...v0.1.22) (2022-12-19)
### [0.1.21](https://github.com/nuxtlabs/ui/compare/v0.1.20...v0.1.21) (2022-12-19)
### [0.1.20](https://github.com/nuxtlabs/ui/compare/v0.1.19...v0.1.20) (2022-12-19)
### Bug Fixes
* avoid referring to complex types in props ([#123](https://github.com/nuxtlabs/ui/issues/123)) ([ff9f6c2](https://github.com/nuxtlabs/ui/commit/ff9f6c251df59641862d82587e5d963c8e6ea298))
### [0.1.19](https://github.com/nuxtlabs/ui/compare/v0.1.18...v0.1.19) (2022-12-16)
### [0.1.18](https://github.com/nuxtlabs/ui/compare/v0.1.17...v0.1.18) (2022-12-15)
### [0.1.17](https://github.com/nuxtlabs/ui/compare/v0.1.16...v0.1.17) (2022-12-06)
### Bug Fixes
* remove stop propagation on mode hover ([16fd1c0](https://github.com/nuxtlabs/ui/commit/16fd1c0ca38f1438e791c0d44399f590d9f20d02))
### [0.1.16](https://github.com/nuxtlabs/ui/compare/v0.1.15...v0.1.16) (2022-12-06)

View File

@@ -9,11 +9,11 @@
</NuxtLink>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center">
<ColorScheme placeholder="" tag="span">
<UButton variant="transparent" :icon="colorMode.value === 'dark' ? 'heroicons-outline:moon' : 'heroicons-outline:sun'" @click="toggleDark" />
<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="fa-brands:github" />
<UButton to="https://github.com/nuxtlabs/ui" target="_blank" variant="transparent" icon="i-mdi-github" />
</div>
</div>
</UContainer>
@@ -79,12 +79,12 @@ const toggleDark = () => {
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: 'Icon', to: '/components/Icon' }] },
{ 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: 'Modal', to: '/components/Modal' }, { label: 'Notification', to: '/components/Notification' }, { label: 'Popover', to: '/components/Popover' }, { label: 'Slideover', to: '/components/Slideover' }, { label: 'Tooltip', to: '/components/Tooltip' }] }
{ 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>

View File

@@ -3,7 +3,15 @@ import nuxtUI from '../src/module'
// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
app: {
head: {
htmlAttrs: {
lang: 'en'
}
}
},
modules: [
// @ts-ignore
nuxtUI
],
components: {
@@ -16,6 +24,7 @@ export default defineNuxtConfig({
},
preset: {
},
icons: ['heroicons', 'mdi'],
tailwindcss: {
theme: {
extend: {

View File

@@ -35,7 +35,7 @@
<UButton
class="absolute top-0 right-0"
:icon="copied ? 'heroicons-outline:clipboard-check' : 'heroicons-outline:clipboard-copy'"
:icon="copied ? 'i-heroicons-clipboard-document-check' : 'i-heroicons-clipboard-document'"
variant="transparent"
size="sm"
:custom-class="copied ? '!text-green-500' : ''"
@@ -56,7 +56,6 @@
v-if="prop.type === 'Boolean'"
v-model="prop.value"
:name="prop.key"
:label="prop.key"
/>
<USelect
v-else-if="prop.values"
@@ -121,6 +120,31 @@ 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'
@@ -132,7 +156,9 @@ const defaultProps = {
title: 'A new software update is available. See whats new in version 2.0.4.'
},
AlertDialog: {
title: 'Are you sure you want to close this modal?',
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: {
@@ -154,21 +180,21 @@ const defaultProps = {
items: [
[{
label: 'Edit',
icon: 'heroicons-solid:pencil'
icon: 'i-heroicons-pencil-square-20-solid'
}, {
label: 'Duplicate',
icon: 'heroicons-solid:duplicate'
icon: 'i-heroicons-document-duplicate-20-solid'
}],
[{
label: 'Archive',
icon: 'heroicons-solid:archive'
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'heroicons-solid:external-link'
icon: 'i-heroicons-arrow-right-circle-20-solid'
}],
[{
label: 'Delete',
icon: 'heroicons-solid:trash'
icon: 'i-heroicons-trash-20-solid'
}]
]
},
@@ -176,22 +202,22 @@ const defaultProps = {
links: [
{
label: 'Home',
icon: 'heroicons-outline:home',
icon: 'i-heroicons-home',
to: '/'
},
{
label: 'Examples',
icon: 'heroicons-outline:book-open',
icon: 'i-heroicons-book-open',
to: '/examples'
},
{
label: 'Migration',
icon: 'heroicons-outline:refresh',
icon: 'i-heroicons-arrow-path',
to: '/migration'
},
{
label: 'External link',
icon: 'heroicons-outline:external-link',
icon: 'i-heroicons-link',
to: 'https://google.fr',
target: '_blank'
}
@@ -207,7 +233,7 @@ const defaultProps = {
}]
},
Icon: {
name: 'heroicons-outline:bell'
name: 'i-heroicons-bell'
},
Input: {
name: 'input',
@@ -259,6 +285,31 @@ const defaultProps = {
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 },

View File

@@ -26,7 +26,7 @@
Button:
</div>
<UButton variant="primary" icon="heroicons-outline:mail">
<UButton variant="primary" icon="i-heroicons-envelope">
Button text
</UButton>
</div>
@@ -43,7 +43,7 @@
<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="heroicons-outline:exclamation" class="h-6 w-6 text-red-600" aria-hidden="true" />
<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">
@@ -73,7 +73,7 @@
</div>
<UDropdown v-slot="{ open }" :items="dropdownItems" placement="bottom-start">
<UButton variant="white" :icon="open ? 'heroicons-solid:chevron-up' : 'heroicons-solid:chevron-down'" trailing icon-class="transition">
<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>
@@ -96,54 +96,16 @@
Popover:
</div>
<UPopover mode="hover" wrapper-class="inline-block relative">
<UPopover wrapper-class="inline-block relative">
<template #default="{ open }">
<UButton variant="secondary" :icon="open ? 'heroicons-solid:chevron-up' : 'heroicons-solid:chevron-down'" trailing icon-class="transition">
<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="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"
>
<div class="relative grid gap-8 bg-white p-7">
<a
v-for="item in solutions"
:key="item.name"
:href="item.href"
class="flex items-center p-2 -m-3 transition duration-150 ease-in-out rounded-lg hover:bg-gray-50 focus:outline-none focus-visible:ring focus-visible:ring-orange-500 focus-visible:ring-opacity-50"
>
<div
class="flex items-center justify-center flex-shrink-0 w-10 h-10 text-white sm:h-12 sm:w-12"
>
<div v-html="item.icon" />
</div>
<div class="ml-4">
<p class="text-sm font-medium u-text-gray-900">
{{ item.name }}
</p>
<p class="text-sm u-text-gray-500">
{{ item.description }}
</p>
</div>
</a>
</div>
<div class="p-4 bg-gray-50">
<a
href="##"
class="flow-root px-2 py-2 transition duration-150 ease-in-out rounded-md hover:bg-gray-100 focus:outline-none focus-visible:ring focus-visible:ring-orange-500 focus-visible:ring-opacity-50"
>
<span class="flex items-center">
<span class="text-sm font-medium u-text-gray-900">
Documentation
</span>
</span>
<span class="block text-sm u-text-gray-500">
Start integrating products and tools
</span>
</a>
</div>
<div class="p-2">
Panel
</div>
</template>
</UPopover>
@@ -155,7 +117,7 @@
</div>
<UTooltip text="Hello tooltip!" :shortcuts="['⌘', 'G']">
<UIcon name="heroicons-outline:information-circle" class="w-6 h-6 u-text-gray-900 cursor-pointer" />
<UIcon name="i-heroicons-information-circle" class="w-6 h-6 u-text-gray-900 cursor-pointer" />
</UTooltip>
</div>
@@ -163,7 +125,17 @@
<div class="font-medium text-sm mb-1 u-text-gray-700">
Notifications:
</div>
<UButton icon="heroicons-outline:bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
<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">
Copy text to clipboard:
</div>
<div class="flex gap-2">
<UInput v-model="textToCopy" name="textToCopy" />
<UButton icon="i-heroicons-document-duplicate-solid" variant="primary" label="Copy text" @click="onCopyTextClick" />
</div>
</div>
<div>
@@ -171,11 +143,11 @@
Context menu:
</div>
<UCard ref="contextMenuRef" class="relative" body-class="h-64" @click="isContextMenuOpen = false" @contextmenu.prevent="openContextMenu">
<UCard class="relative" body-class="h-64" @click="isContextMenuOpen = false" @contextmenu.prevent="openContextMenu">
<UContextMenu v-model="isContextMenuOpen" :virtual-element="virtualElement" width-class="w-48">
<UCard @click.stop>
<div class="p-2">
Menu
</UCard>
</div>
</UContextMenu>
</UCard>
</div>
@@ -187,18 +159,13 @@
<UCard body-class="">
<UCommandPalette
v-model="form.persons"
multiple
:placeholder="false"
:options="{
fuseOptions: {
includeMatches: true
}
}"
:groups="[{
key: 'persons',
commands: people
}]"
:groups="commandPaletteGroups"
command-attribute="name"
/>
</UCard>
@@ -212,7 +179,7 @@
<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="heroicons-outline:mail" />
<UInput v-model="form.email" type="email" name="email" required icon="i-heroicons-mail" />
</UFormGroup>
<UFormGroup label="Description" name="description">
@@ -227,7 +194,7 @@
placeholder="Select a person"
text-attribute="name"
value-attribute="id"
icon="heroicons-outline:user"
icon="i-heroicons-user"
/>
</UFormGroup>
@@ -236,7 +203,7 @@
</UFormGroup>
<UFormGroup label="Toggle" name="toggle">
<UToggle v-model="form.toggle" name="toggle" icon-off="heroicons-solid:x" icon-on="heroicons-solid:check" />
<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?">
@@ -273,6 +240,7 @@
<script setup>
const isModalOpen = ref(false)
const textToCopy = ref('Copied text')
const people = ref([
{ id: 1, name: 'Durward Reynolds', disabled: false },
@@ -294,12 +262,25 @@ const form = reactive({
persons: ref([people.value[0]])
})
const { $toast } = useNuxtApp()
const toast = useToast()
const clipboard = useCopyToClipboard()
const x = ref(0)
const y = ref(0)
const isContextMenuOpen = ref(false)
const contextMenuRef = ref(null)
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 }) => {
@@ -308,8 +289,6 @@ onMounted(() => {
})
})
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
function openContextMenu () {
const top = unref(y)
const left = unref(x)
@@ -340,27 +319,27 @@ function onSubmit () {
const dropdownItems = [
[{
label: 'Edit',
icon: 'heroicons-solid:pencil',
icon: 'i-heroicons-pencil-square-20-solid',
click: (e) => {
e.preventDefault()
onClick()
}
}, {
label: 'Duplicate',
icon: 'heroicons-solid:duplicate'
icon: 'i-heroicons-document-duplicate-20-solid'
}],
[{
label: 'Archive',
icon: 'heroicons-solid:archive'
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'heroicons-solid:external-link',
icon: 'i-heroicons-arrow-right-circle-20-solid',
to: 'https://www.google.fr',
target: '_blank'
}],
[{
label: 'Delete',
icon: 'heroicons-solid:trash'
icon: 'i-heroicons-trash-20-solid'
}]
]
@@ -373,101 +352,16 @@ const customDropdownItems = [
}],
[{
label: 'About',
icon: 'heroicons-solid:plus',
icon: 'i-heroicons-plus-20-solid',
to: '/about'
}]
]
const solutions = [
{
name: 'Insights',
description: 'Measure actions your users take',
href: '##',
icon: `
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
aria-hidden='true'
xmlns="http://www.w3.org/2000/svg"
>
<rect width="48" height="48" rx="8" fill="#FFEDD5" />
<path
d="M24 11L35.2583 17.5V30.5L24 37L12.7417 30.5V17.5L24 11Z"
stroke="#FB923C"
stroke-width="2"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.7417 19.8094V28.1906L24 32.3812L31.2584 28.1906V19.8094L24 15.6188L16.7417 19.8094Z"
stroke="#FDBA74"
stroke-width="2"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20.7417 22.1196V25.882L24 27.7632L27.2584 25.882V22.1196L24 20.2384L20.7417 22.1196Z"
stroke="#FDBA74"
stroke-width="2"
/>
</svg>
`
},
{
name: 'Automations',
description: 'Create your own targeted content',
href: '##',
icon: `
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="48" height="48" rx="8" fill="#FFEDD5" />
<path
d="M28.0413 20L23.9998 13L19.9585 20M32.0828 27.0001L36.1242 34H28.0415M19.9585 34H11.8755L15.9171 27"
stroke="#FB923C"
stroke-width="2"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.804 30H29.1963L24.0001 21L18.804 30Z"
stroke="#FDBA74"
stroke-width="2"
/>
</svg>
`
},
{
name: 'Reports',
description: 'Keep track of your growth',
href: '##',
icon: `
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="48" height="48" rx="8" fill="#FFEDD5" />
<rect x="13" y="32" width="2" height="4" fill="#FDBA74" />
<rect x="17" y="28" width="2" height="8" fill="#FDBA74" />
<rect x="21" y="24" width="2" height="12" fill="#FDBA74" />
<rect x="25" y="20" width="2" height="16" fill="#FDBA74" />
<rect x="29" y="16" width="2" height="20" fill="#FB923C" />
<rect x="33" y="12" width="2" height="24" fill="#FB923C" />
</svg>
`
}
]
const onNotificationClick = () => {
$toast.error({ title: 'Error', description: 'This is an error message' })
toast.error({ title: 'Error', description: 'This is an error message' })
}
const onCopyTextClick = () => {
clipboard.copy(textToCopy.value, { title: 'Text copied successfully!' })
}
</script>

View File

@@ -105,13 +105,6 @@ const components = [
preset: true,
typescript: true
},
{
label: 'Icon',
to: '/components/Icon',
nuxt3: true,
capi: true,
typescript: true
},
{
label: 'Link',
to: '/components/Link',

View File

@@ -1,6 +1,6 @@
{
"name": "@nuxthq/ui",
"version": "0.1.16",
"version": "1.2.2",
"repository": "https://github.com/nuxtlabs/ui",
"license": "MIT",
"exports": {
@@ -25,37 +25,35 @@
"release": "yarn lint && standard-version && git push --follow-tags"
},
"dependencies": {
"@headlessui/vue": "^1.7.4",
"@iconify/vue": "^4.0.1",
"@nuxt/kit": "^3.0.0",
"@egoist/tailwindcss-icons": "^1.0.7",
"@headlessui/vue": "1.7.10",
"@iconify-json/heroicons": "^1.1.10",
"@nuxt/kit": "^3.3.1",
"@nuxtjs/color-mode": "^3.2.0",
"@nuxtjs/tailwindcss": "^6.1.3",
"@nuxtjs/tailwindcss": "^6.6.0",
"@popperjs/core": "^2.11.6",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.8",
"@vueuse/core": "^9.6.0",
"@vueuse/integrations": "^9.6.0",
"defu": "^6.1.1",
"@tailwindcss/typography": "^0.5.9",
"@vueuse/core": "^9.13.0",
"@vueuse/integrations": "^9.13.0",
"@vueuse/math": "^9.13.0",
"defu": "^6.1.2",
"fuse.js": "^6.6.2",
"lodash-es": "^4.17.21",
"tailwindcss": "^3.2.4"
"tailwindcss": "^3.2.7"
},
"devDependencies": {
"@iconify-json/mdi": "^1.1.50",
"@nuxt/module-builder": "^0.2.1",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
"@types/lodash-es": "^4.17.6",
"eslint": "^8.28.0",
"nuxt": "^3.0.0",
"@types/lodash-es": "^4.17.7",
"@types/node": "^18.15.3",
"eslint": "^8.36.0",
"nuxt": "^3.3.1",
"standard-version": "^9.5.0",
"unbuild": "^1.0.1",
"vue-tsc": "^1.0.10"
},
"build": {
"externals": [
"tailwindcss",
"tailwindcss/colors.js"
]
"unbuild": "^1.1.2",
"vue-tsc": "^1.2.0"
}
}

View File

@@ -1,7 +1,8 @@
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, addTemplate, addPlugin, createResolver } from '@nuxt/kit'
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, addTemplate, createResolver } from '@nuxt/kit'
import { defu } from 'defu'
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'
@@ -39,6 +40,8 @@ export interface ModuleOptions {
colors?: ColorsOptions
icons: string[]
tailwindcss?: Partial<Config>
}
@@ -49,6 +52,7 @@ const defaults = {
primary: 'indigo',
gray: 'gray'
},
icons: ['heroicons'],
tailwindcss: {
theme: {}
}
@@ -72,7 +76,7 @@ export default defineNuxtModule<ModuleOptions>({
// Transpile runtime
const runtimeDir = resolve('./runtime')
nuxt.options.build.transpile.push(runtimeDir)
nuxt.options.build.transpile.push('@popperjs/core', '@headlessui/vue', '@iconify/vue')
nuxt.options.build.transpile.push('@popperjs/core', '@headlessui/vue')
// @ts-ignore
nuxt.hook('tailwindcss:config', function (tailwindConfig: TailwindConfig) {
@@ -108,6 +112,9 @@ export default defineNuxtModule<ModuleOptions>({
variants: ['focus']
}])
tailwindConfig.plugins = tailwindConfig.plugins || []
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
const ui: object = defu(preset, defaultPreset(variantColors))
addTemplate({
@@ -148,9 +155,6 @@ export default defineNuxtModule<ModuleOptions>({
cssPath: resolve(runtimeDir, 'tailwind.css')
})
addPlugin(resolve(runtimeDir, 'plugins', 'toast.client'))
addPlugin(resolve(runtimeDir, 'plugins', 'clipboard.client'))
addComponentsDir({
path: resolve(runtimeDir, 'components', 'elements'),
prefix,

View File

@@ -31,7 +31,7 @@ const props = defineProps({
type: String,
default: 'md',
validator (value: string) {
return ['xxxs', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
return Object.keys($ui.avatar.size).includes(value)
}
},
max: {

View File

@@ -7,7 +7,12 @@
v-bind="buttonProps"
>
<Icon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
<slot><span :class="truncate ? 'text-left break-all line-clamp-1' : ''">{{ label }}</span></slot>
<slot>
<span :class="[truncate ? 'text-left break-all line-clamp-1' : '', compact ? 'hidden sm:block' : '']">
<span :class="[labelCompact && 'hidden sm:block']">{{ label }}</span>
<span v-if="labelCompact" class="sm:hidden">{{ labelCompact }}</span>
</span>
</slot>
<Icon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</component>
</template>
@@ -34,6 +39,10 @@ const props = defineProps({
type: String,
default: null
},
labelCompact: {
type: String,
default: null
},
loading: {
type: Boolean,
default: false
@@ -127,6 +136,10 @@ const props = defineProps({
truncate: {
type: Boolean,
default: false
},
compact: {
type: Boolean,
default: false
}
})
@@ -164,7 +177,7 @@ const buttonClass = computed(() => {
return classNames(
props.baseClass,
$ui.button.size[props.size],
$ui.button[isSquare.value ? 'square' : 'spacing'][props.size],
$ui.button[isSquare.value ? 'square' : (props.compact ? 'compact' : 'spacing')][props.size],
$ui.button.variant[props.variant],
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center',
props.rounded ? 'rounded-full' : props.roundedClass,
@@ -192,7 +205,7 @@ const leadingIconClass = computed(() => {
return classNames(
props.iconBaseClass,
$ui.button.icon.size[props.size],
(!!slots.default || !!props.label?.length) && $ui.button.icon.leading.spacing[props.size],
(!!slots.default || !!props.label?.length) && $ui.button.icon.leading[props.compact ? 'compactSpacing' : 'spacing'][props.size],
props.leadingIconClass,
props.loading && 'animate-spin'
)
@@ -202,7 +215,7 @@ const trailingIconClass = computed(() => {
return classNames(
props.iconBaseClass,
$ui.button.icon.size[props.size],
(!!slots.default || !!props.label?.length) && $ui.button.icon.trailing.spacing[props.size],
(!!slots.default || !!props.label?.length) && $ui.button.icon.trailing[props.compact ? 'compactSpacing' : 'spacing'][props.size],
props.trailingIconClass,
props.loading && !isLeading.value && 'animate-spin'
)

View File

@@ -1,20 +1,29 @@
<template>
<Menu v-slot="{ open }" as="div" :class="wrapperClass" @mouseleave="onMouseLeave">
<MenuButton ref="trigger" as="div" class="inline-flex" @mouseover="onMouseOver">
<MenuButton
ref="trigger"
as="div"
:disabled="disabled"
class="inline-flex w-full"
role="button"
@mouseover="onMouseOver"
>
<slot :open="open">
<button>Open</button>
<button :disabled="disabled">
Open
</button>
</slot>
</MenuButton>
<div v-if="open" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
<div v-if="open && items.length" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
<transition appear v-bind="transitionClass">
<MenuItems :class="baseClass" static>
<MenuItems :class="[baseClass, divideClass, ringClass, roundedClass, shadowClass, backgroundClass]" static>
<div v-for="(subItems, index) of items" :key="index" :class="groupClass">
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled }" :disabled="item.disabled">
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
<Component
v-bind="omit(item, ['click'])"
:is="(item.to && NuxtLink) || (item.click && 'button') || 'div'"
:class="resolveItemClass({ active, disabled })"
:class="resolveItemClass({ active, disabled: itemDisabled })"
@click="item.click"
>
<slot :name="item.slot" :item="item">
@@ -79,6 +88,10 @@ const props = defineProps({
return ['click', 'hover'].includes(value)
}
},
disabled: {
type: Boolean,
default: false
},
wrapperClass: {
type: String,
default: () => $ui.dropdown.wrapper
@@ -91,6 +104,26 @@ const props = defineProps({
type: String,
default: () => $ui.dropdown.width
},
backgroundClass: {
type: String,
default: () => $ui.dropdown.background
},
shadowClass: {
type: String,
default: () => $ui.dropdown.shadow
},
roundedClass: {
type: String,
default: () => $ui.dropdown.rounded
},
ringClass: {
type: String,
default: () => $ui.dropdown.ring
},
divideClass: {
type: String,
default: () => $ui.dropdown.divide
},
baseClass: {
type: String,
default: () => $ui.dropdown.base
@@ -172,12 +205,6 @@ onMounted(() => {
}
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
// stop trigger click propagation on hover
menuApi.value?.buttonRef?.addEventListener('click', (e: Event) => {
if (props.mode === 'hover') {
e.stopPropagation()
}
}, true)
}, 200)
})

View File

@@ -1,54 +1,14 @@
<template>
<span v-if="isFetching" />
<Iconify v-else-if="icon" :icon="icon" />
<Component :is="component" v-else-if="component" />
<span v-else>{{ name }}</span>
<span :class="name" />
</template>
<script setup lang="ts">
import type { IconifyIcon } from '@iconify/vue'
import { Icon as Iconify } from '@iconify/vue/dist/offline'
import { loadIcon } from '@iconify/vue'
import { useNuxtApp, useState, computed, watch, onMounted } from '#imports'
const props = defineProps({
defineProps({
name: {
type: String,
required: true
}
})
const nuxtApp = useNuxtApp()
const state = useState<Record<string, Required<IconifyIcon> | Promise<void> | null>>('u-icons', () => ({}))
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
const isFetching = computed(() => state.value?.[props.name] && ('then' in state.value?.[props.name]!))
const icon = computed<Required<IconifyIcon> | null>(() =>
!state.value?.[props.name] || 'then' in state.value[props.name]!
? null
: state.value[props.name] as Required<IconifyIcon>
)
const component = computed(() => nuxtApp.vueApp.component(props.name))
const loadIconComponent = (name: string) => {
state.value = state.value || {}
if (nuxtApp.vueApp.component(props.name) || state.value[name] || state.value[name] === null) { return state.value[name] }
state.value[name] = loadIcon(name)
.then((res) => { state.value[name] = res })
// We won't try to load this icon again if it fails
.catch(() => { state.value[name] = null })
// return an awaitable promise
return state.value[name]
}
onMounted(() => {
watch(() => props.name, loadIconComponent, { immediate: true })
})
if (process.server) {
await loadIconComponent(props.name)
}
</script>
<script lang="ts">

View File

@@ -1,7 +1,7 @@
<template>
<div class="rounded-md p-4" :class="variantClass">
<div class="flex">
<div class="flex-shrink-0">
<div class="inline-flex flex-shrink-0">
<Icon v-if="iconName" :name="iconName" :class="iconClass" class="h-5 w-5" />
</div>
<div class="ml-3 flex-1 md:flex md:justify-between">
@@ -57,10 +57,10 @@ const props = defineProps({
const iconName = computed(() => {
return ({
info: 'heroicons-solid:information-circle',
warning: 'heroicons-solid:exclamation',
error: 'heroicons-solid:x-circle',
success: 'heroicons-solid:check-circle'
info: 'i-heroicons-information-circle',
warning: 'i-heroicons-exclamation',
error: 'i-heroicons-x-circle',
success: 'i-heroicons-check-circle'
})[props.variant]
})

View File

@@ -23,9 +23,9 @@
<script setup lang="ts">
import { computed } from 'vue'
import { DialogTitle, DialogDescription } from '@headlessui/vue'
import Icon from '../elements/Icon.vue'
import Modal from '../overlays/Modal.vue'
import Button from '../elements/Button.vue'
import Icon from '../elements/Icon.vue'
import $ui from '#build/ui'
const props = defineProps({
@@ -55,7 +55,7 @@ const props = defineProps({
},
titleClass: {
type: String,
default: () => $ui.alertDialog.icon.title
default: () => $ui.alertDialog.title
},
description: {
type: String,

View File

@@ -45,8 +45,8 @@
<script setup lang="ts">
import { computed } from 'vue'
import { get } from 'lodash-es'
import Icon from '../elements/Icon.vue'
import { classNames } from '../../utils'
import Icon from '../elements/Icon.vue'
import $ui from '#build/ui'
const props = defineProps({

View File

@@ -10,9 +10,9 @@
:class="wrapperClass"
@update:model-value="onUpdate"
>
<input :value="modelValue" :required="required" class="absolute inset-0 w-px opacity-0 cursor-default" tabindex="-1">
<input :value="modelValue" :required="required" class="absolute inset-0 w-px opacity-0 cursor-default" tabindex="-1" aria-hidden="true">
<ComboboxButton ref="trigger" v-slot="{ disabled: buttonDisabled }" as="div" class="inline-flex w-full">
<ComboboxButton ref="trigger" v-slot="{ disabled: buttonDisabled }" as="div" role="button" class="inline-flex w-full">
<slot :open="open" :disabled="buttonDisabled">
<button :class="selectCustomClass" :disabled="disabled" type="button">
<slot name="label">
@@ -84,7 +84,7 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import type { PropType, ComponentPublicInstance } from 'vue'
import { defu } from 'defu'
import {
@@ -110,7 +110,7 @@ const props = defineProps({
default: undefined
},
options: {
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[]>,
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[] | string[]>,
default: () => []
},
required: {
@@ -261,7 +261,7 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'open', 'close'])
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.selectCustom.popperOptions))
@@ -292,11 +292,11 @@ const iconClass = computed(() => {
const filteredOptions = computed(() =>
query.value === ''
? props.options
: props.options.filter((option: any) => {
return (props.searchAttributes?.length ? props.searchAttributes : [props.textAttribute]).some((searchAttribute: any) => {
return option[searchAttribute] && option[searchAttribute].search(new RegExp(query.value, 'i')) !== -1
: (props.options as any[]).filter((option: any) => {
return (props.searchAttributes?.length ? props.searchAttributes : [props.textAttribute]).some((searchAttribute: any) => {
return typeof option === 'string' ? option.search(new RegExp(query.value, 'i')) !== -1 : (option[searchAttribute] && option[searchAttribute].search(new RegExp(query.value, 'i')) !== -1)
})
})
})
)
const queryOption = computed(() => {
@@ -307,6 +307,14 @@ const iconWrapperClass = classNames(
$ui.selectCustom.icon.trailing.wrapper
)
watch(container, (value) => {
if (value) {
emit('open')
} else {
emit('close')
}
})
function resolveOptionClass ({ active, selected, disabled }: { active: boolean, selected: boolean, disabled?: boolean }) {
return classNames(
props.listOptionBaseClass,

View File

@@ -7,23 +7,45 @@
:nullable="nullable"
@update:model-value="onSelect"
>
<div class="flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800">
<div :class="$ui.commandPalette.wrapper">
<div v-show="searchable" class="relative flex items-center">
<Icon :name="inputIcon" class="pointer-events-none absolute top-3.5 left-5 h-5 w-5 u-text-gray-400" aria-hidden="true" />
<Icon v-if="inputIcon" :name="inputIcon" :class="$ui.commandPalette.input.icon.base" aria-hidden="true" />
<ComboboxInput
ref="comboboxInput"
:value="query"
class="w-full h-12 pr-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 pl-[3.25rem] u-text-gray-900 focus:ring-0 sm:text-sm"
:class="$ui.commandPalette.input.base"
:placeholder="inputPlaceholder"
autocomplete="off"
@change="query = $event.target.value"
/>
<Button v-if="closeIcon" :icon="closeIcon" variant="transparent" class="absolute right-3" @click="onClear" />
<Button
v-if="inputCloseIcon"
:icon="inputCloseIcon"
:class="$ui.commandPalette.input.close.base"
:size="$ui.commandPalette.input.close.size"
:variant="$ui.commandPalette.input.close.variant"
aria-label="Close"
@click="onClear"
/>
</div>
<ComboboxOptions v-if="groups.length" static hold class="relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2">
<CommandPaletteGroup v-for="group of groups" :key="group.key" :group="group" :group-attribute="groupAttribute" :command-attribute="commandAttribute">
<ComboboxOptions
v-if="groups.length"
static
hold
as="div"
aria-label="Commands"
class="relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2"
>
<CommandPaletteGroup
v-for="group of groups"
:key="group.key"
:query="query"
:group="group"
:group-attribute="groupAttribute"
:command-attribute="commandAttribute"
>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
@@ -31,8 +53,8 @@
</ComboboxOptions>
<div v-else-if="placeholder" class="flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14">
<Icon :name="emptyIcon" class="w-6 h-6 mx-auto u-text-gray-400" aria-hidden="true" />
<p class="mt-4 text-sm text-center u-text-gray-900">
<Icon v-if="emptyIcon" :name="emptyIcon" class="w-6 h-6 mx-auto u-text-gray-400 mb-4" aria-hidden="true" />
<p class="text-sm text-center u-text-gray-900">
{{ query ? "We couldn't find any items with that term. Please try again." : "We couldn't find any items." }}
</p>
</div>
@@ -41,9 +63,10 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, watch, onMounted } from 'vue'
import { Combobox, ComboboxInput, ComboboxOptions } from '@headlessui/vue'
import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import { useFuse } from '@vueuse/integrations/useFuse'
import { groupBy, map } from 'lodash-es'
import { defu } from 'defu'
@@ -52,6 +75,7 @@ import type { Group, Command } from '../../types/command-palette'
import Icon from '../elements/Icon.vue'
import Button from '../elements/Button.vue'
import CommandPaletteGroup from './CommandPaletteGroup.vue'
import $ui from '#build/ui'
const props = defineProps({
modelValue: {
@@ -78,13 +102,13 @@ const props = defineProps({
type: Array as PropType<Group[]>,
default: () => []
},
closeIcon: {
type: String,
default: null
},
inputIcon: {
type: String,
default: 'heroicons-outline:search'
default: () => $ui.commandPalette.input.icon.name
},
inputCloseIcon: {
type: String,
default: () => $ui.commandPalette.input.close.icon.name
},
inputPlaceholder: {
type: String,
@@ -92,7 +116,7 @@ const props = defineProps({
},
emptyIcon: {
type: String,
default: 'heroicons-outline:search'
default: () => $ui.commandPalette.empty.icon.name
},
groupAttribute: {
type: String,
@@ -117,6 +141,10 @@ const props = defineProps({
placeholder: {
type: Boolean,
default: true
},
debounce: {
type: Number,
default: 200
}
})
@@ -152,33 +180,56 @@ const options: ComputedRef<Partial<UseFuseOptions<Command>>> = computed(() => de
matchAllWhenSearchEmpty: true
}))
const commands = computed(() => props.groups.reduce((acc, group) => {
const commands = computed(() => props.groups.filter(group => !group.search).reduce((acc, group) => {
return acc.concat(group.commands.map(command => ({ ...command, group: group.key })))
}, [] as Command[]))
const searchResults = ref<{ [key: string]: any }>({})
const { results } = useFuse(query, commands, options)
const groups = computed(() => map(groupBy(results.value, command => command.item.group), (results, key) => {
const commands = results.map((result) => {
const { item, ...data } = result
const groups = computed(() => ([
...map(groupBy(results.value, command => command.item.group), (results, key) => {
const commands = results.map((result) => {
const { item, ...data } = result
return {
...item,
...data
}
})
return {
...item,
...data
}
})
...props.groups.find(group => group.key === key),
commands: commands.slice(0, options.value.resultLimit)
} as Group
}),
...props.groups.filter(group => !!group.search).map(group => ({ ...group, commands: (searchResults.value[group.key] || []).slice(0, options.value.resultLimit) })).filter(group => group.commands.length)
]))
return {
...props.groups.find(group => group.key === key),
commands: commands.slice(0, options.value.resultLimit)
} as Group
}))
const debouncedSearch = useDebounceFn(async () => {
const searchableGroups = props.groups.filter(group => !!group.search)
await Promise.all(searchableGroups.map(async (group) => {
searchResults.value[group.key] = await group.search(query.value)
}))
}, props.debounce)
watch(query, () => {
debouncedSearch()
// Select first item on search changes
setTimeout(() => {
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L804
comboboxInput.value?.$el.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageUp' }))
}, 0)
})
// Methods
function activateFirstOption () {
// hack combobox by using keyboard event
// https://github.com/tailwindlabs/headlessui/blob/main/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L692
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L769
setTimeout(() => {
comboboxInput.value?.$el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
}, 0)
@@ -204,6 +255,7 @@ function onClear () {
}
defineExpose({
query,
updateQuery: (q: string) => {
query.value = q
},

View File

@@ -1,10 +1,10 @@
<template>
<li class="p-2">
<h2 v-if="group[groupAttribute]" class="px-3 my-2 text-xs font-semibold u-text-gray-900">
{{ group[groupAttribute] }}
<div class="p-2" role="option">
<h2 v-if="label" class="px-3 my-2 text-xs font-semibold u-text-gray-900">
{{ label }}
</h2>
<ul class="text-sm u-text-gray-700">
<div class="text-sm u-text-gray-700" role="listbox" :aria-label="group[groupAttribute]">
<ComboboxOption
v-for="(command, index) of group.commands"
:key="`${group.key}-${index}`"
@@ -13,15 +13,18 @@
:disabled="command.disabled"
as="template"
>
<li :class="['flex justify-between select-none items-center rounded-md px-3 py-2 gap-3 relative', active && 'bg-gray-100 dark:bg-gray-800 u-text-gray-900', command.disabled ? 'cursor-not-allowed' : 'cursor-pointer']">
<div :class="['flex justify-between select-none items-center rounded-md px-3 py-2 gap-3 relative', active && 'bg-gray-100 dark:bg-gray-800 u-text-gray-900', command.disabled ? 'cursor-not-allowed' : 'cursor-pointer']">
<div class="flex items-center gap-2 min-w-0">
<Icon v-if="command.icon" :name="command.icon" :class="['h-4 w-4 flex-shrink-0', active ? 'text-opacity-100 dark:text-opacity-100' : 'text-opacity-40 dark:text-opacity-40', command.iconClass || 'text-gray-900 dark:text-gray-50']" aria-hidden="true" />
<Avatar
v-else-if="command.avatar"
v-bind="{ size: 'xxxs', ...command.avatar }"
class="flex-shrink-0"
/>
<span v-else-if="command.chip" class="flex-shrink-0 w-2 h-2 mx-1 rounded-full" :style="{ background: `#${command.chip}` }" />
<slot :name="`${group.key}-icon`" :group="group" :command="command">
<Icon v-if="command.icon" :name="command.icon" :class="['h-4 w-4 flex-shrink-0', active ? 'text-opacity-100 dark:text-opacity-100' : 'text-opacity-40 dark:text-opacity-40', command.iconClass || 'text-gray-900 dark:text-gray-50']" aria-hidden="true" />
<Avatar
v-else-if="command.avatar"
v-bind="{ size: 'xxxs', ...command.avatar }"
class="flex-shrink-0"
aria-hidden="true"
/>
<span v-else-if="command.chip" class="flex-shrink-0 w-2 h-2 mx-1 rounded-full" :style="{ background: `#${command.chip}` }" />
</slot>
<div class="flex items-center gap-1.5 min-w-0" :class="{ 'opacity-50': command.disabled }">
<slot :name="`${group.key}-command`" :group="group" :command="command">
@@ -34,34 +37,40 @@
</div>
</div>
<Icon v-if="selected" name="heroicons-outline:check" class="h-5 w-5 u-text-gray-900 flex-shrink-0" aria-hidden="true" />
<Icon v-if="selected" :name="$ui.commandPalette.option.selected.icon.name" class="h-5 w-5 u-text-gray-900 flex-shrink-0" aria-hidden="true" />
<slot v-else-if="active && (group.active || $slots[`${group.key}-active`])" :name="`${group.key}-active`" :group="group" :command="command">
<span v-if="group.active" class="flex-shrink-0 u-text-gray-500">{{ group.active }}</span>
</slot>
<slot v-else :name="`${group.key}-inactive`" :group="group" :command="command">
<span v-if="command.shortcuts?.length" class="flex-shrink-0 text-xs font-semibold u-text-gray-500">
<span v-if="command.shortcuts?.length" :class="$ui.commandPalette.option.shortcuts">
<kbd v-for="shortcut of command.shortcuts" :key="shortcut" class="font-sans">{{ shortcut }}</kbd>
</span>
<span v-else-if="!command.disabled && group.inactive" class="flex-shrink-0 u-text-gray-500">{{ group.inactive }}</span>
</slot>
</li>
</div>
</ComboboxOption>
</ul>
</li>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ComboboxOption } from '@headlessui/vue'
import type { PropType } from 'vue'
import Icon from '../elements/Icon.vue'
import Avatar from '../elements/Avatar.vue'
import type { Group } from '../../types/command-palette'
import $ui from '#build/ui'
defineProps({
const props = defineProps({
group: {
type: Object as PropType<Group>,
required: true
},
query: {
type: String,
default: ''
},
groupAttribute: {
type: String,
required: true
@@ -72,6 +81,12 @@ defineProps({
}
})
const label = computed(() => {
const label = props.group[props.groupAttribute]
return typeof label === 'function' ? label(props.query) : label
})
function highlight ({ indices, value }: { indices: number[][], value:string }, i = 1): string {
const pair = indices[indices.length - i]
if (!pair) {

View File

@@ -49,7 +49,7 @@ import $ui from '#build/ui'
defineProps({
links: {
type: Array as PropType<{
to?: RouteLocationNormalized
to?: RouteLocationNormalized | string
exact?: boolean
label: string
icon?: string

View File

@@ -1,7 +1,7 @@
<template>
<div v-if="isOpen" ref="container" :class="[containerClass, widthClass]">
<transition appear v-bind="transitionClass">
<div :class="baseClass">
<div :class="[baseClass, ringClass, roundedClass, shadowClass, backgroundClass]">
<slot />
</div>
</transition>
@@ -9,7 +9,7 @@
</template>
<script setup lang="ts">
import type { PropType } from 'vue'
import type { PropType, Ref } from 'vue'
import { computed, toRef } from 'vue'
import { defu } from 'defu'
import type { VirtualElement } from '@popperjs/core'
@@ -23,7 +23,7 @@ const props = defineProps({
default: false
},
virtualElement: {
type: Object as PropType<VirtualElement>,
type: Object,
required: true
},
wrapperClass: {
@@ -38,6 +38,22 @@ const props = defineProps({
type: String,
default: () => $ui.contextMenu.width
},
backgroundClass: {
type: String,
default: () => $ui.contextMenu.background
},
shadowClass: {
type: String,
default: () => $ui.contextMenu.shadow
},
roundedClass: {
type: String,
default: () => $ui.contextMenu.rounded
},
ringClass: {
type: String,
default: () => $ui.contextMenu.ring
},
baseClass: {
type: String,
default: () => $ui.contextMenu.base
@@ -63,7 +79,7 @@ const isOpen = computed({
}
})
const virtualElement = toRef(props, 'virtualElement')
const virtualElement = toRef(props, 'virtualElement') as Ref<VirtualElement>
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.contextMenu.popperOptions))

View File

@@ -33,11 +33,11 @@
</div>
<button
class="transition duration-150 ease-in-out u-text-gray-400 focus:outline-none hover:u-text-gray-500 focus:u-text-gray-500"
class="inline-flex transition duration-150 ease-in-out u-text-gray-400 focus:outline-none hover:u-text-gray-500 focus:u-text-gray-500"
@click.stop="onClose"
>
<span class="sr-only">Close</span>
<Icon name="heroicons-solid:x" class="w-5 h-5" />
<Icon :name="$ui.notification.close.icon.name" class="w-5 h-5" />
</button>
</div>
</div>

View File

@@ -9,7 +9,7 @@
v-bind="notification"
:class="notification.click && 'cursor-pointer'"
@click="notification.click && notification.click(notification)"
@close="$toast.removeNotification(notification.id)"
@close="toast.removeNotification(notification.id)"
/>
</div>
</div>
@@ -18,10 +18,11 @@
<script setup lang="ts">
import type { ToastNotification } from '../../types'
import { useToast } from '../../composables/useToast'
import Notification from './Notification.vue'
import { useNuxtApp, useState } from '#imports'
import { useState } from '#imports'
const { $toast } = useNuxtApp()
const toast = useToast()
const notifications = useState<ToastNotification[]>('notifications', () => [])
</script>

View File

@@ -1,14 +1,23 @@
<template>
<Popover v-slot="{ open, close }" :class="wrapperClass" @mouseleave="onMouseLeave">
<PopoverButton ref="trigger" as="div" class="inline-flex" @mouseover="onMouseOver">
<PopoverButton
ref="trigger"
as="div"
:disabled="disabled"
class="inline-flex w-full"
role="button"
@mouseover="onMouseOver"
>
<slot :open="open" :close="close">
<button>Open</button>
<button :disabled="disabled">
Open
</button>
</slot>
</PopoverButton>
<div v-if="open" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
<transition appear v-bind="transitionClass">
<PopoverPanel :class="baseClass" static>
<PopoverPanel :class="[baseClass, ringClass, roundedClass, shadowClass, backgroundClass]" static>
<slot name="panel" :open="open" :close="close" />
</PopoverPanel>
</transition>
@@ -33,6 +42,10 @@ const props = defineProps({
return ['click', 'hover'].includes(value)
}
},
disabled: {
type: Boolean,
default: false
},
wrapperClass: {
type: String,
default: () => $ui.popover.wrapper
@@ -49,6 +62,22 @@ const props = defineProps({
type: String,
default: () => $ui.popover.base
},
backgroundClass: {
type: String,
default: () => $ui.popover.background
},
shadowClass: {
type: String,
default: () => $ui.popover.shadow
},
roundedClass: {
type: String,
default: () => $ui.popover.rounded
},
ringClass: {
type: String,
default: () => $ui.popover.ring
},
transitionClass: {
type: Object,
default: () => $ui.popover.transition
@@ -86,12 +115,6 @@ onMounted(() => {
}
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
// stop trigger click propagation on hover
popoverApi.value?.button?.addEventListener('click', (e: Event) => {
if (props.mode === 'hover') {
e.stopPropagation()
}
}, true)
}, 200)
})

View File

@@ -1,6 +1,6 @@
<template>
<TransitionRoot as="template" :appear="appear" :show="isOpen">
<Dialog :class="[wrapperClass, { 'justify-end': side === 'right' }]" @close="isOpen = false">
<Dialog :class="[wrapperClass, { 'justify-end': side === 'right' }]" @close="close">
<TransitionChild
v-if="overlay"
as="template"
@@ -89,7 +89,7 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'close'])
const isOpen: WritableComputedRef<boolean> = computed({
get () {
@@ -129,6 +129,11 @@ const slideoverTransition = computed(() => {
...props.transitionClass
}
})
function close (value: boolean) {
isOpen.value = value
emit('close')
}
</script>
<script lang="ts">

View File

@@ -4,14 +4,14 @@
Hover me
</slot>
<div v-if="open" ref="container" :class="[containerClass, widthClass]">
<div v-if="open && !prevent" ref="container" :class="[containerClass, widthClass]">
<transition appear v-bind="transitionClass">
<div :class="[baseClass, backgroundClass, roundedClass, shadowClass, ringClass]">
<slot name="text">
<span class="truncate">{{ text }}</span>
{{ text }}
</slot>
<span v-if="shortcuts?.length" class="inline-flex items-center justify-end flex-shrink-0 gap-0.5 ml-1">
<span v-if="shortcuts?.length" :class="shortcutsClass">
<span class="mr-1 u-text-gray-700">&middot;</span>
<kbd v-for="shortcut of shortcuts" :key="shortcut" class="flex items-center justify-center font-sans px-1 h-4 min-w-[16px] text-[10px] u-bg-gray-100 rounded u-text-gray-900">
{{ shortcut }}
@@ -36,6 +36,10 @@ const props = defineProps({
type: String,
default: null
},
prevent: {
type: Boolean,
default: false
},
shortcuts: {
type: Array as PropType<string[]>,
default: () => []
@@ -80,6 +84,10 @@ const props = defineProps({
type: Object as PropType<PopperOptions>,
default: () => ({})
},
shortcutsClass: {
type: String,
default: () => $ui.tooltip.shortcuts
},
openDelay: {
type: Number,
default: 0

View File

@@ -0,0 +1,109 @@
import type { Ref, ComputedRef } from 'vue'
import { logicAnd, logicNot } from '@vueuse/math'
import { computed, onMounted, onBeforeUnmount } from 'vue'
import { useShortcuts } from './useShortcuts'
export interface ShortcutConfig {
handler: Function
usingInput?: string | boolean
whenever?: Ref<Boolean>[]
}
export interface ShortcutsConfig {
[key: string]: ShortcutConfig | Function
}
interface Shortcut {
handler: Function
condition: ComputedRef<Boolean>
// KeyboardEvent attributes
key: string
ctrlKey: boolean
metaKey: boolean
shiftKey: boolean
altKey: boolean
// code?: string
// keyCode?: number
}
export const defineShortcuts = (config: ShortcutsConfig) => {
const { macOS, usingInput } = useShortcuts()
let shortcuts: Shortcut[] = []
const onKeyDown = (e: KeyboardEvent) => {
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key)
for (const shortcut of shortcuts) {
if (e.key.toLowerCase() !== shortcut.key) { continue }
if (e.metaKey !== shortcut.metaKey) { continue }
if (e.ctrlKey !== shortcut.ctrlKey) { continue }
// shift modifier is only checked in combination with alphabetical keys
// (shift with non-alphabetical keys would change the key)
if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) { continue }
// alt modifier changes the combined key anyways
// if (e.altKey !== shortcut.altKey) { continue }
if (shortcut.condition.value) {
e.preventDefault()
shortcut.handler()
}
return
}
}
onMounted(() => {
// Map config to full detailled shortcuts
shortcuts = Object.entries(config).map(([key, shortcutConfig]) => {
if (!shortcutConfig) {
return null
}
// Parse key and modifiers
const keySplit = key.toLowerCase().split('_').map(k => k)
let shortcut: Partial<Shortcut> = {
key: keySplit.filter(k => !['meta', 'ctrl', 'shift', 'alt'].includes(k)).join('_'),
metaKey: keySplit.includes('meta'),
ctrlKey: keySplit.includes('ctrl'),
shiftKey: keySplit.includes('shift'),
altKey: keySplit.includes('alt')
}
// Convert Meta to Ctrl for non-MacOS
if (!macOS.value && shortcut.metaKey && !shortcut.ctrlKey) {
shortcut.metaKey = false
shortcut.ctrlKey = true
}
// Retrieve handler function
if (typeof shortcutConfig === 'function') {
shortcut.handler = shortcutConfig
} else if (typeof shortcutConfig === 'object') {
shortcut = { ...shortcut, handler: shortcutConfig.handler }
}
if (!shortcut.handler) {
// eslint-disable-next-line no-console
console.trace('[Shortcut] Invalid value')
return null
}
// Create shortcut computed
const conditions = []
if (!(shortcutConfig as ShortcutConfig).usingInput) {
conditions.push(logicNot(usingInput))
} else if (typeof (shortcutConfig as ShortcutConfig).usingInput === 'string') {
conditions.push(computed(() => usingInput.value === (shortcutConfig as ShortcutConfig).usingInput))
}
shortcut.condition = logicAnd(...conditions, ...((shortcutConfig as ShortcutConfig).whenever || []))
return shortcut as Shortcut
}).filter(Boolean) as Shortcut[]
document.addEventListener('keydown', onKeyDown)
})
onBeforeUnmount(() => {
document.removeEventListener('keydown', onKeyDown)
})
}

View File

@@ -1,8 +1,9 @@
import { useClipboard } from '@vueuse/core'
import { defineNuxtPlugin } from '#app'
import { useToast } from './useToast'
export default defineNuxtPlugin((nuxtApp) => {
export function useCopyToClipboard () {
const { copy: copyToClipboard, isSupported } = useClipboard()
const toast = useToast()
function copy (text: string, success: { title?: string, description?: string } = {}, failure: { title?: string, description?: string } = {}) {
if (!isSupported) {
@@ -14,9 +15,9 @@ export default defineNuxtPlugin((nuxtApp) => {
return
}
nuxtApp.$toast.success(success)
toast.success(success)
}, function (e) {
nuxtApp.$toast.error({
toast.error({
...failure,
description: failure.description || e.message
})
@@ -24,10 +25,6 @@ export default defineNuxtPlugin((nuxtApp) => {
}
return {
provide: {
clipboard: {
copy
}
}
copy
}
})
}

View File

@@ -0,0 +1,32 @@
import { createSharedComposable, useActiveElement } from '@vueuse/core'
import { ref, computed, onMounted } from 'vue'
export const _useShortcuts = () => {
const macOS = computed(() => process.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/))
const metaSymbol = ref(' ')
const activeElement = useActiveElement()
const usingInput = computed(() => {
const usingInput = !!(activeElement.value?.tagName === 'INPUT' || activeElement.value?.tagName === 'TEXTAREA' || activeElement.value?.contentEditable === 'true')
if (usingInput) {
return ((activeElement.value as any)?.name as string) || true
}
return false
})
onMounted(() => {
metaSymbol.value = macOS.value ? '⌘' : 'Ctrl'
})
return {
macOS,
metaSymbol,
activeElement,
usingInput
}
}
export const useShortcuts = createSharedComposable(_useShortcuts)

View File

@@ -0,0 +1,41 @@
import type { ToastNotification } from '../types'
import { useState } from '#imports'
export function useToast () {
const notifications = useState<ToastNotification[]>('notifications', () => [])
function addNotification (notification: Partial<ToastNotification>) {
const body = {
id: new Date().getTime().toString(),
...notification
}
const index = notifications.value.findIndex((n: ToastNotification) => n.id === body.id)
if (index === -1) {
notifications.value.push(body as ToastNotification)
}
return body
}
function removeNotification (id: string) {
notifications.value = notifications.value.filter((n: ToastNotification) => n.id !== id)
}
const success = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'success', ...notification })
const info = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'info', ...notification })
const warning = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'warning', ...notification })
const error = (notification: Partial<ToastNotification>) => addNotification({ type: 'error', title: 'An error occurred!', ...notification })
return {
addNotification,
removeNotification,
success,
info,
warning,
error
}
}

View File

@@ -1,45 +0,0 @@
import { defineNuxtPlugin, useState } from '#app'
import type { ToastNotification } from '../types'
export default defineNuxtPlugin(() => {
const notifications = useState<ToastNotification[]>('notifications', () => [])
function addNotification (notification: Partial<ToastNotification>) {
const body = {
id: new Date().getTime().toString(),
...notification
}
const index = notifications.value.findIndex((n: ToastNotification) => n.id === body.id)
if (index === -1) {
notifications.value.push(body as ToastNotification)
}
return body
}
function removeNotification (id: string) {
notifications.value = notifications.value.filter((n: ToastNotification) => n.id !== id)
}
return {
provide: {
toast: {
addNotification,
removeNotification,
success (notification: Partial<ToastNotification> = {}) {
return addNotification({ type: 'success', ...notification })
},
info (notification: Partial<ToastNotification> = {}) {
return addNotification({ type: 'info', ...notification })
},
warning (notification: Partial<ToastNotification> = {}) {
return addNotification({ type: 'warning', ...notification })
},
error (notification: Partial<ToastNotification>) {
return addNotification({ type: 'error', title: 'An error occurred!', ...notification })
}
}
}
}
})

View File

@@ -26,6 +26,14 @@ export default function defaultPreset (variantColors: string[]) {
lg: 'p-2',
xl: 'p-3'
},
compact: {
xxs: 'p-1 sm:px-2',
xs: 'p-1.5 sm:px-2.5',
sm: 'p-2 sm:px-3',
md: 'p-2 sm:px-4',
lg: 'p-2 sm:px-4',
xl: 'p-3 sm:px-6'
},
variant: {
...variantColors.reduce((acc: any, color: string) => {
acc[color] = `shadow-sm border border-transparent text-white bg-${color}-600 hover:bg-${color}-700 disabled:bg-${color}-600 focus:ring-2 focus:ring-offset-2 focus:ring-${color}-500`
@@ -40,7 +48,7 @@ export default function defaultPreset (variantColors: string[]) {
},
icon: {
base: 'flex-shrink-0',
loading: 'heroicons-outline:refresh',
loading: 'i-heroicons-arrow-path',
size: {
xxs: 'h-3.5 w-3.5',
xs: 'h-4 w-4',
@@ -57,6 +65,14 @@ export default function defaultPreset (variantColors: string[]) {
md: '-ml-1 mr-2',
lg: '-ml-1 mr-3',
xl: '-ml-1 mr-3'
},
compactSpacing: {
xxs: 'sm:-ml-0.5 sm:mr-1',
xs: 'sm:-ml-0.5 sm:mr-1.5',
sm: 'sm:-ml-0.5 sm:mr-2',
md: 'sm:-ml-1 sm:mr-2',
lg: 'sm:-ml-1 sm:mr-3',
xl: 'sm:-ml-1 sm:mr-3'
}
},
trailing: {
@@ -67,6 +83,14 @@ export default function defaultPreset (variantColors: string[]) {
md: 'ml-2 -mr-1',
lg: 'ml-3 -mr-1',
xl: 'ml-3 -mr-1'
},
compactSpacing: {
xxs: 'sm:ml-1 sm:-mr-0.5',
xs: 'sm:ml-1.5 sm:-mr-0.5',
sm: 'sm:ml-2 sm:-mr-0.5',
md: 'sm:ml-2 sm:-mr-1',
lg: 'sm:ml-3 sm:-mr-1',
xl: 'sm:ml-3 sm:-mr-1'
}
}
}
@@ -144,7 +168,7 @@ export default function defaultPreset (variantColors: string[]) {
},
icon: {
base: 'u-text-gray-400',
loading: 'heroicons-outline:refresh',
loading: 'i-heroicons-arrow-path',
size: {
xxs: 'h-3 w-3',
xs: 'h-4 w-4',
@@ -191,7 +215,7 @@ export default function defaultPreset (variantColors: string[]) {
wrapper: 'relative',
base: `${select.base} text-left cursor-default`,
icon: {
name: 'heroicons-solid:selector',
name: 'i-heroicons-chevron-up-down-20-solid',
...select.icon
},
list: {
@@ -209,7 +233,7 @@ export default function defaultPreset (variantColors: string[]) {
disabled: 'cursor-not-allowed opacity-50',
empty: 'text-sm u-text-gray-400 px-4 py-2',
icon: {
name: 'heroicons-solid:check',
name: 'i-heroicons-check-20-solid',
base: 'absolute inset-y-0 right-0 flex items-center pr-4',
active: 'text-white',
inactive: 'text-primary-600',
@@ -331,18 +355,23 @@ export default function defaultPreset (variantColors: string[]) {
const alertDialog = {
icon: {
wrapper: 'h-12 w-12 sm:h-10 sm:w-10 bg-primary-100',
base: 'text-primary-600'
wrapper: 'mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-primary-100 sm:mx-0 sm:h-10 sm:w-10',
base: 'h-6 w-6 text-primary-600'
},
title: 'text-lg leading-6 font-medium text-gray-900',
description: 'text-sm text-gray-500'
title: 'text-lg leading-6 font-medium u-text-gray-900',
description: 'text-sm u-text-gray-500'
}
const dropdown = {
wrapper: 'relative inline-flex text-left',
container: 'z-20',
width: 'w-48',
base: 'u-bg-white divide-y u-divide-gray-100 rounded-md ring-1 u-ring-gray-200 shadow-lg focus:outline-none',
background: 'u-bg-white',
shadow: 'shadow-lg',
rounded: 'rounded-md',
ring: 'ring-1 u-ring-gray-200',
base: 'focus:outline-none',
divide: 'divide-y u-divide-gray-100',
group: 'py-1',
item: {
base: 'group flex items-center gap-3 px-4 py-2 text-sm w-full',
@@ -351,7 +380,7 @@ export default function defaultPreset (variantColors: string[]) {
disabled: 'cursor-not-allowed opacity-50',
icon: 'h-5 w-5 u-text-gray-400 group-hover:u-text-gray-500 flex-shrink-0',
avatar: '-m-0.5 group-hover:u-bg-gray-200 flex-shrink-0',
shortcuts: 'flex-shrink-0 text-xs font-semibold u-text-gray-500 ml-auto'
shortcuts: 'hidden md:inline-flex flex-shrink-0 text-xs font-semibold u-text-gray-500 ml-auto'
},
transition: {
enterActiveClass: 'transition duration-100 ease-out',
@@ -459,10 +488,10 @@ export default function defaultPreset (variantColors: string[]) {
rounded: 'rounded-lg',
ring: 'ring-1 u-ring-gray-200',
type: {
info: 'heroicons-outline:information-circle',
success: 'heroicons-outline:check-circle',
warning: 'heroicons-outline:exclamation-circle',
error: 'heroicons-outline:x-circle'
info: 'i-heroicons-information-circle',
success: 'i-heroicons-check-circle',
warning: 'i-heroicons-exclamation-circle',
error: 'i-heroicons-x-circle'
},
icon: {
base: 'w-6 h-6',
@@ -473,6 +502,11 @@ export default function defaultPreset (variantColors: string[]) {
error: 'text-red-400'
}
},
close: {
icon: {
name: 'i-heroicons-x-mark-20-solid'
}
},
transition: {
enterActiveClass: 'transform ease-out duration-300 transition',
enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2',
@@ -491,7 +525,8 @@ export default function defaultPreset (variantColors: string[]) {
shadow: 'shadow',
rounded: 'rounded',
ring: 'ring-1 u-ring-gray-200',
base: 'invisible lg:visible h-6 px-2 py-1 text-xs font-normal',
base: 'invisible lg:visible h-6 px-2 py-1 text-xs font-normal truncate',
shortcuts: 'hidden md:inline-flex items-center justify-end flex-shrink-0 gap-0.5 ml-1',
transition: {
enterActiveClass: 'transition ease-out duration-200',
enterFromClass: 'opacity-0 translate-y-1',
@@ -509,7 +544,11 @@ export default function defaultPreset (variantColors: string[]) {
wrapper: 'relative',
container: 'z-20',
width: '',
base: '',
background: 'u-bg-white',
shadow: 'shadow-lg',
rounded: 'rounded-md',
ring: 'ring-1 u-ring-gray-200',
base: 'overflow-hidden focus:outline-none',
transition: {
enterActiveClass: 'transition ease-out duration-200',
enterFromClass: 'opacity-0 translate-y-1',
@@ -527,7 +566,11 @@ export default function defaultPreset (variantColors: string[]) {
wrapper: 'relative',
container: 'z-20',
width: '',
base: '',
background: 'u-bg-white',
shadow: 'shadow-lg',
rounded: 'rounded-md',
ring: 'ring-1 u-ring-gray-200',
base: 'overflow-hidden focus:outline-none',
transition: {
enterActiveClass: 'transition ease-out duration-200',
enterFromClass: 'opacity-0 translate-y-1',
@@ -542,6 +585,38 @@ export default function defaultPreset (variantColors: string[]) {
}
}
const commandPalette = {
wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800',
input: {
base: 'w-full h-12 pr-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 pl-[3.25rem] u-text-gray-900 focus:ring-0 sm:text-sm',
icon: {
base: 'pointer-events-none absolute top-3.5 left-5 h-5 w-5 u-text-gray-400',
name: 'i-heroicons-magnifying-glass'
},
close: {
base: 'absolute right-2',
variant: 'transparent',
size: 'md',
icon: {
name: ''
}
}
},
empty: {
icon: {
name: 'i-heroicons-magnifying-glass'
}
},
option: {
selected: {
icon: {
name: 'i-heroicons-check-20-solid'
}
},
shortcuts: 'hidden md:inline-flex flex-shrink-0 text-xs font-semibold u-text-gray-500'
}
}
return {
card,
modal,
@@ -567,7 +642,8 @@ export default function defaultPreset (variantColors: string[]) {
notification,
tooltip,
popover,
contextMenu
contextMenu,
commandPalette
}
}

View File

@@ -1,6 +1,7 @@
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import type { FuseSortFunctionMatch, FuseSortFunctionMatchList } from 'fuse.js'
import type { Avatar } from './avatar'
export interface Command {
id: string | number
prefix?: string
@@ -22,6 +23,5 @@ export interface Group {
active?: string
inactive?: string
commands: Command[]
options?: Partial<UseFuseOptions<Command>>
[key: string]: any
}

View File

@@ -14,10 +14,3 @@ export interface ToastNotification {
click?: Function
callback?: Function
}
export interface ToastPlugin {
addNotification: (notification: Partial<Notification>) => Notification
removeNotification: (id: string) => void
success: (options: { title?: string, description?: string }) => void
error: (options: { title?: string, description?: string }) => void
}

View File

@@ -1,14 +1,3 @@
{
"extends": "./docs/.nuxt/tsconfig.json",
"compilerOptions": {
"module": "esnext"
},
"include": [
"./**/*",
"./docs/.nuxt/nuxt.d.ts"
],
"exclude": [
"node_modules",
"dist"
]
"extends": "./docs/.nuxt/tsconfig.json"
}

2660
yarn.lock

File diff suppressed because it is too large Load Diff