Compare commits

...

112 Commits

Author SHA1 Message Date
Benjamin Canac
4cbe983f61 chore(release): 0.1.2 2022-10-27 16:05:16 +02:00
Benjamin Canac
e42969f003 chore(Tooltip): improve design 2022-10-27 16:04:49 +02:00
Benjamin Canac
c93e37a0eb chore(Tooltip): handle shortcuts 2022-10-27 14:26:31 +02:00
Sylvain Marroufin
9e20f96b65 chore(Dropdown): don't show outline on keyboard navigation (#108) 2022-10-27 11:52:16 +02:00
Benjamin Canac
9f6f132a76 chore(release): 0.1.1 2022-10-26 13:13:13 +02:00
Sylvain Marroufin
5517cc2846 fix(Dropdown): close on click item with to (#103)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2022-10-26 13:05:41 +02:00
Benjamin Canac
8c24b5dc97 chore(module): add d.ts file 2022-10-26 13:04:36 +02:00
Benjamin Canac
ec8bd5cdc4 fix(SelectCustom): types and lint 2022-10-26 13:04:36 +02:00
Benjamin Canac
6fab68baa8 fix(CommandPalette): lint 2022-10-26 13:04:36 +02:00
Benjamin Canac
fc951e2980 chore(CommandPalette): improve types 2022-10-26 13:04:36 +02:00
Benjamin Canac
08ff6e6c09 docs: update 2022-10-26 13:04:36 +02:00
Sylvain Marroufin
9dcdaf474e chore(Dropdown|Popover|Tooltip): hover delay (#104) 2022-10-26 11:59:49 +02:00
Sylvain Marroufin
e9f0224b91 fix(Popover): avoid crash on mount if ref not loaded (#105) 2022-10-26 11:57:13 +02:00
Benjamin Canac
929192fd46 chore(deps): bump vueuse 2022-10-25 16:32:42 +02:00
Benjamin Canac
d9ff11d1a2 chore(release): 0.1.0 2022-10-25 14:52:45 +02:00
Benjamin Canac
80edb1bd4b chore(deps): bump 2022-10-25 14:11:27 +02:00
Sylvain Marroufin
fc658842bb chore: fix typescript errors (#102) 2022-10-25 14:09:54 +02:00
Benjamin Canac
32922def7d fix(CommandPalette): group items spacing 2022-10-24 16:46:19 +02:00
Benjamin Canac
a4bea1c508 chore(CommandPalette): reduce icon and avatar sizes 2022-10-24 16:19:27 +02:00
Benjamin Canac
f87252f05d fix(CommandPalette): prevent shortcuts to disappear on hover 2022-10-24 16:18:20 +02:00
Benjamin Canac
ec9f67094a fix(CommandPalette): reactivity issue + improve results
Resolves #95, resolves #96
2022-10-24 15:58:15 +02:00
Benjamin Canac
72dc0d0d0c fix(Modal): use object for innerStyle 2022-10-23 19:29:54 +02:00
Benjamin Canac
02b0a29d86 chore(Modal): add innerStyle prop 2022-10-23 19:26:30 +02:00
Benjamin Canac
cd2f1122be chore(Modal): add inner ref 2022-10-23 19:00:36 +02:00
Benjamin Canac
e3943123ce chore(deps): bump 2022-10-21 15:55:25 +02:00
Baptiste Leproux
d57647a77a fix(icon): hydratation warning when loading same icon twice (#99) 2022-10-20 11:34:58 +02:00
Benjamin Canac
aa97f26180 chore(deps): bump 2022-10-19 11:28:40 +02:00
Benjamin Canac
0b17fb02e4 chore(deps): bump 2022-10-18 11:24:11 +02:00
Baptiste Leproux
5039265097 chore: set popper options in preset (#97)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2022-10-18 11:13:28 +02:00
Benjamin Canac
c89c65bd44 chore(Dropdown): allow group preset change 2022-10-13 17:10:02 +02:00
Sylvain Marroufin
4515a1239b chore: expose all types in index.ts (#94) 2022-10-13 12:47:43 +02:00
Sylvain Marroufin
de9596bfc4 chore(Tooltip): update preset for truncate (#90) 2022-10-10 15:44:39 +02:00
Benjamin Canac
7da11ccec0 chore: improve Modal / Slideover preset 2022-10-09 21:35:53 +02:00
Benjamin Canac
d29377f614 chore(CommandPalette): support by prop and highlight 2022-10-09 13:31:11 +02:00
Benjamin Canac
f0e482cf01 chore(SelectCustom): support by prop 2022-10-09 13:30:24 +02:00
Benjamin Canac
056ab30474 fix(CommandPalette): prevent empty active slot 2022-10-09 12:54:45 +02:00
Benjamin Canac
95c14a4360 fix: default object options 2022-10-08 23:40:33 +02:00
Benjamin Canac
b25bb75783 chore(CommandPalette): show placeholder even when searchable 2022-10-08 23:31:35 +02:00
Benjamin Canac
98ae6c14aa docs: improve ContextMenu example 2022-10-08 23:22:23 +02:00
Benjamin Canac
8ee2a3cf5b chore(CommandPalette): expose to take control from textarea 2022-10-08 23:22:06 +02:00
Benjamin Canac
91f93c450e chore(ContextMenu): improve options 2022-10-08 22:24:57 +02:00
Benjamin Canac
237a673af3 chore(popper): add more options 2022-10-08 22:24:42 +02:00
Benjamin Canac
591e60dfc7 chore(deps): bump 2022-10-08 22:24:28 +02:00
Benjamin Canac
b9f0b3cb10 chore(ContextMenu): new component 2022-10-08 00:51:53 +02:00
Benjamin Canac
3ed64250cd chore(deps): bump 2022-10-08 00:37:26 +02:00
Benjamin Canac
f1b59fc59e chore: fix popper props merge 2022-10-07 13:40:23 +02:00
Benjamin Canac
005c18e4c0 chore: improve popper handling 2022-10-07 13:27:51 +02:00
Benjamin Canac
44b7199c4f chore(docs): improve 2022-10-07 13:27:42 +02:00
Sylvain Marroufin
4774adc0ae chore(SelectCustom): clear search on select (#88) 2022-10-06 15:56:20 +02:00
Sylvain Marroufin
03b0e07a53 chore(Tooltip): allow to disable popper flip (#87) 2022-10-06 15:54:28 +02:00
Sylvain Marroufin
2560088b2b chore(CommandPalette): allow to disable autoselect (#84) 2022-10-05 15:10:51 +02:00
Benjamin Canac
4ce991652b chore(Slideover): allow to disable transitions 2022-10-05 00:58:01 +02:00
Benjamin Canac
d96b4ea119 chore(preset): handle Modal / Slideover transitions 2022-10-05 00:24:44 +02:00
Benjamin Canac
b20c8c82ab chore(preset): handle transitions 2022-10-04 23:56:54 +02:00
Benjamin Canac
e5bf052c1f chore(deps): bump @nuxtjs/color-mode 2022-10-04 23:46:13 +02:00
Sylvain Marroufin
cf1b2cdd13 fix(SelectCustom): avoid submitting to form when closing (#83) 2022-10-04 16:48:31 +02:00
Benjamin Canac
8776929f63 chore(deps): bump 2022-10-03 17:50:27 +02:00
Daniel Roe
62361bfa8f fix: load icons on mount rather than within setup (#82) 2022-10-03 17:49:13 +02:00
Clément Ollivier
bf25ad15af chore: replace deprecated addAutoImportDir function (#81) 2022-10-03 10:26:12 +02:00
Benjamin Canac
03b18b1ff8 chore(deps): clean lock 2022-09-22 15:14:04 +02:00
Benjamin Canac
fdf149f0d0 chore(deps): bump 2022-09-22 15:11:09 +02:00
Benjamin Canac
e4c3a74e0e chore(Notification): add preset system 2022-09-20 11:47:52 +02:00
Benjamin Canac
38f248c24d chore(Modal): handle transition disable 2022-09-16 17:43:43 +02:00
Benjamin Canac
74de5b106b chore(Modal): improve preset 2022-09-16 16:57:44 +02:00
Benjamin Canac
70f8cd4889 chore(deps): remove nuxt-icon 2022-09-13 10:56:41 +02:00
Benjamin Canac
6a2159da94 Revert "chore: use nuxt-icon"
This reverts commit 70495e0a22.
2022-09-12 19:15:54 +02:00
Benjamin Canac
ae6b3c0ba6 chore(docs): set font-family like in tailwindui 2022-09-12 19:09:03 +02:00
Benjamin Canac
70495e0a22 chore: use nuxt-icon 2022-09-09 15:24:49 +02:00
Benjamin Canac
be99df3b30 chore(deps): bump 2022-09-09 15:24:49 +02:00
Sébastien Chopin
6870eff661 chore: update docs 2022-09-09 12:57:23 +02:00
Sébastien Chopin
062e468518 chore: remove not needed command 2022-09-09 12:19:16 +02:00
Benjamin Canac
3fc2a043fa chore(presets): handle radio and checkbox dark mode 2022-09-07 16:02:25 +02:00
Benjamin Canac
e99ec9b82b chore(VerticalNavigation): pass is-active to slots 2022-09-07 14:30:03 +02:00
Benjamin Canac
4556f24eb1 chore(deps): use nuxt rc.9 2022-09-05 11:03:48 +02:00
Benjamin Canac
f8fb66f177 chore(deps): upgrade @nuxt/kit 2022-09-05 10:58:35 +02:00
Benjamin Canac
5d760d9d8d chore(deps): bump 2022-09-05 10:54:09 +02:00
Benjamin Canac
cb5c9b3923 chore(package): try something for vercel deploy 2022-09-02 13:01:07 +02:00
Benjamin Canac
8e9418f94f chore(deps): bump @nuxt/kit 2022-09-02 13:00:56 +02:00
Benjamin Canac
4712726fbd docs: fix lint 2022-09-02 11:41:18 +02:00
Benjamin Canac
0cb38e2214 chore: prepare module for nuxt 3.0.0-rc9 (#77) 2022-09-02 11:37:59 +02:00
Sylvain Marroufin
3e9c502fbe chore(AvatarGroup): enhance avatars overlap (#75) 2022-08-29 18:57:11 +02:00
Benjamin Canac
541ed304a0 fix: error in Popover and Dropdown 2022-08-26 15:04:06 +02:00
Benjamin Canac
72919425b6 fix(Dropdown): increase timeout for hover mode 2022-08-17 14:14:13 +02:00
Benjamin Canac
8a66f5e9bf fix(Notification): prevent error without timeout 2022-08-17 14:10:34 +02:00
Benjamin Canac
a0d56d0f66 chore(Notifications): support actions instead of specific undo and dismiss 2022-08-17 13:53:53 +02:00
Benjamin Canac
c6706c76b2 chore(Dropdown): add item.iconClass 2022-08-12 17:19:52 +02:00
Benjamin Canac
1f7f28835d chore(Dropdown): pass event to item click 2022-08-12 17:16:00 +02:00
Benjamin Canac
593573a286 fix(Popover): inline-flex on trigger button 2022-08-12 10:27:13 +02:00
Benjamin Canac
3a6ecd6988 chore(SelectCustom): move icons in preset 2022-07-29 11:17:04 +02:00
Sylvain Marroufin
f9dbe28938 chore(typescript): fix typed imports (#72) 2022-07-25 18:45:37 +02:00
Benjamin Canac
2ae17ed919 chore(tailwind): override outline focus to primary color 2022-07-24 12:07:36 +02:00
Benjamin Canac
abb93b5fd3 fix(CommandPalette): command icons opacity in dark mode 2022-07-23 23:57:50 +02:00
Benjamin Canac
f75fc4f864 chore(Dropdown): support shortcuts 2022-07-23 23:53:13 +02:00
Benjamin Canac
5722a3ae62 fix(CommandPalette): icon inactive opacity on dark mode 2022-07-23 23:39:22 +02:00
Benjamin Canac
aa242aa87d fix(CommandPalette): truncate suffix 2022-07-22 18:06:11 +02:00
Benjamin Canac
945fec62c2 fix(Notification): improve placement with description 2022-07-22 17:07:37 +02:00
Sylvain Marroufin
9302b8d635 fix(CommandPalette): fix groups computed 2022-07-22 16:19:38 +02:00
Benjamin Canac
b43394ddc3 fix(CommandPalette): hack for reactivity 2022-07-22 15:47:32 +02:00
Benjamin Canac
be94fea84a fix: to prop type 2022-07-22 13:07:23 +02:00
Benjamin Canac
c08501db3f chore(VerticalNavigation): correct avatar size by default 2022-07-22 12:16:23 +02:00
Benjamin Canac
db2b73e741 chore(plugins): remove types imports 2022-07-21 23:37:05 +02:00
Benjamin Canac
6b813bd3b3 chore: improve types and imports 2022-07-21 23:33:06 +02:00
Benjamin Canac
7b3263a621 chore(deps): add vue-tsc 2022-07-21 23:32:46 +02:00
Benjamin Canac
002a8f6803 docs: refactor dropdown avatar 2022-07-21 23:32:33 +02:00
Benjamin Canac
5a596eb2b0 chore(CommandPalette): command avatar is now an object 2022-07-21 23:31:59 +02:00
Benjamin Canac
2f87cf86a1 chore(Dropdown): item avatar is now an object 2022-07-21 23:31:20 +02:00
Benjamin Canac
e4f148efa9 fix(CommandPalette): icon color on hover 2022-07-21 19:19:29 +02:00
Benjamin Canac
024a5914b9 chore(CommandPalette): align chip and avatar size with icon 2022-07-21 18:49:43 +02:00
Sylvain Marroufin
ce28b04187 chore(CommandPalette): improve customization options (#71)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2022-07-21 15:46:05 +02:00
Benjamin Canac
1ff9fd4f69 chore(toast): improve notifications 2022-07-21 00:39:06 +02:00
Benjamin Canac
487f253e14 chore(SelectCustom): handle disabled on button 2022-07-20 10:40:43 +02:00
Benjamin Canac
f54d4d788d chore(SelectCustom): add disabled prop 2022-07-20 10:15:54 +02:00
46 changed files with 3130 additions and 2251 deletions

View File

@@ -2,6 +2,45 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.1.2](https://github.com/nuxtlabs/ui/compare/v0.1.1...v0.1.2) (2022-10-27)
### [0.1.1](https://github.com/nuxtlabs/ui/compare/v0.1.0...v0.1.1) (2022-10-26)
### Bug Fixes
* **CommandPalette:** lint ([6fab68b](https://github.com/nuxtlabs/ui/commit/6fab68baa836c97680446e8cfdee7c5a64008539))
* **Dropdown:** close on click item with `to` ([#103](https://github.com/nuxtlabs/ui/issues/103)) ([5517cc2](https://github.com/nuxtlabs/ui/commit/5517cc28467f957956a20126c1ce48e64677cb82))
* **Popover:** avoid crash on mount if ref not loaded ([#105](https://github.com/nuxtlabs/ui/issues/105)) ([e9f0224](https://github.com/nuxtlabs/ui/commit/e9f0224b919445260d3b19511420a3fd448e4ea7))
* **SelectCustom:** types and lint ([ec8bd5c](https://github.com/nuxtlabs/ui/commit/ec8bd5cdc49feb924dfdff352d9f3d788653c583))
## [0.1.0](https://github.com/nuxtlabs/ui/compare/v0.0.3...v0.1.0) (2022-10-25)
### Bug Fixes
* `to` prop type ([be94fea](https://github.com/nuxtlabs/ui/commit/be94fea84acc69c0114099b5251ff34e3a239726))
* **CommandPalette:** command icons opacity in dark mode ([abb93b5](https://github.com/nuxtlabs/ui/commit/abb93b5fd3ddda8c91db3370c8e3109cff4a091d))
* **CommandPalette:** fix groups computed ([9302b8d](https://github.com/nuxtlabs/ui/commit/9302b8d635c3ffb489142601a17a9878072c89fe))
* **CommandPalette:** group items spacing ([32922de](https://github.com/nuxtlabs/ui/commit/32922def7deec5bee920a1fb1400449461315d0d))
* **CommandPalette:** hack for reactivity ([b43394d](https://github.com/nuxtlabs/ui/commit/b43394ddc3ee795b56679f7076e0c80a1c496b2e))
* **CommandPalette:** icon color on hover ([e4f148e](https://github.com/nuxtlabs/ui/commit/e4f148efa97adf52b1b5544ff6c349a4ac82a956))
* **CommandPalette:** icon inactive opacity on dark mode ([5722a3a](https://github.com/nuxtlabs/ui/commit/5722a3ae62706229179b75d9291babd1c2039244))
* **CommandPalette:** prevent empty active slot ([056ab30](https://github.com/nuxtlabs/ui/commit/056ab304745c3ba8dedbf9d6491839b9d620df88))
* **CommandPalette:** prevent shortcuts to disappear on hover ([f87252f](https://github.com/nuxtlabs/ui/commit/f87252f05debda7c98f5ab8a9453e57efafaad0f))
* **CommandPalette:** reactivity issue + improve results ([ec9f670](https://github.com/nuxtlabs/ui/commit/ec9f67094a51e3afde92f7924b8ee5d4e9053158)), closes [#95](https://github.com/nuxtlabs/ui/issues/95) [#96](https://github.com/nuxtlabs/ui/issues/96)
* **CommandPalette:** truncate suffix ([aa242aa](https://github.com/nuxtlabs/ui/commit/aa242aa87d5ae834d838518efd530003fdde5e24))
* default object options ([95c14a4](https://github.com/nuxtlabs/ui/commit/95c14a43600016bf405b557752fad289fb31154a))
* **Dropdown:** increase timeout for hover mode ([7291942](https://github.com/nuxtlabs/ui/commit/72919425b6e84581ba3b854aec3348977b335a3f))
* error in Popover and Dropdown ([541ed30](https://github.com/nuxtlabs/ui/commit/541ed304a0a4fa2646115547e03e44cf9e17c65e))
* **icon:** hydratation warning when loading same icon twice ([#99](https://github.com/nuxtlabs/ui/issues/99)) ([d57647a](https://github.com/nuxtlabs/ui/commit/d57647a77a145ff6e81d3a71550e98e3eaf3a842))
* load icons on mount rather than within setup ([#82](https://github.com/nuxtlabs/ui/issues/82)) ([62361bf](https://github.com/nuxtlabs/ui/commit/62361bfa8f77c2f3452af108f08434ba4c6ec4c5))
* **Modal:** use object for `innerStyle` ([72dc0d0](https://github.com/nuxtlabs/ui/commit/72dc0d0d0c270b2dfbf2ba8a8eb03a04eb5dea9a))
* **Notification:** improve placement with description ([945fec6](https://github.com/nuxtlabs/ui/commit/945fec62c2efa6baf7b32c8a85ba658dfd9311c9))
* **Notification:** prevent error without timeout ([8a66f5e](https://github.com/nuxtlabs/ui/commit/8a66f5e9bf65ab04b8878f0d597e439b45b46bb3))
* **Popover:** `inline-flex` on trigger button ([593573a](https://github.com/nuxtlabs/ui/commit/593573a286459b48fde8f49df2c2f1fc1dc98da6))
* **SelectCustom:** avoid submitting to form when closing ([#83](https://github.com/nuxtlabs/ui/issues/83)) ([cf1b2cd](https://github.com/nuxtlabs/ui/commit/cf1b2cdd133233481da6e1ec47b49b7f012aa204))
### [0.0.3](https://github.com/nuxtlabs/ui/compare/v0.0.2...v0.0.3) (2022-07-18) ### [0.0.3](https://github.com/nuxtlabs/ui/compare/v0.0.2...v0.0.3) (2022-07-18)

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<nav class="u-bg-white border-b u-border-gray-200 fixed top-0 inset-x-0 z-20"> <nav class="u-bg-white border-b u-border-gray-200 fixed top-0 inset-x-0 z-10">
<UContainer padded> <UContainer padded>
<div class="flex items-center justify-between h-16"> <div class="flex items-center justify-between h-16">
<div class="flex items-center"> <div class="flex items-center">
@@ -10,7 +10,9 @@
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<UButton variant="transparent" :icon="colorMode.value === 'dark' ? 'heroicons-outline:moon' : 'heroicons-outline:sun'" @click="toggleDark" /> <ColorScheme placeholder="" tag="span">
<UButton variant="transparent" :icon="colorMode.value === 'dark' ? 'heroicons-outline:moon' : 'heroicons-outline: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="fa-brands:github" />
</div> </div>
</div> </div>
@@ -19,7 +21,7 @@
<UContainer class="mt-16"> <UContainer class="mt-16">
<div class="lg:grid lg:grid-cols-10 lg:gap-10 lg:relative"> <div class="lg:grid lg:grid-cols-10 lg:gap-10 lg:relative">
<aside class="lg:flex lg:flex-col lg:relative 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:sticky lg:h-screen"> <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"> <nav class="overflow-y-auto h-auto pt-8 lg:py-12">
<ul class="space-y-6"> <ul class="space-y-6">
<li v-for="section of sections" :key="section"> <li v-for="section of sections" :key="section">

View File

@@ -1,5 +1,5 @@
import { defineNuxtConfig } from 'nuxt' import defaultTheme from 'tailwindcss/defaultTheme'
import module from '../src/module' import nuxtUI from '../src/module'
// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config // https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({ export default defineNuxtConfig({
@@ -12,8 +12,8 @@ export default defineNuxtConfig({
{ rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' } { rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' }
] ]
}, },
buildModules: [ modules: [
module nuxtUI
], ],
components: { components: {
global: true global: true
@@ -29,7 +29,7 @@ export default defineNuxtConfig({
theme: { theme: {
extend: { extend: {
fontFamily: { fontFamily: {
sans: ['"Inter var", sans-serif'] sans: ['Inter var', ...defaultTheme.fontFamily.sans]
} }
} }
} }

View File

@@ -1,5 +1,8 @@
<!-- eslint-disable vue/no-template-shadow -->
<!-- eslint-disable vue/no-v-html -->
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<template> <template>
<UCard v-if="component" class="relative flex flex-col lg:h-[calc(100vh-10rem)]" body-class="px-4 py-5 sm:p-6 relative" footer-class="flex flex-col flex-1 overflow-hidden"> <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"> <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="`U${defaultProps[params.component].component.name}`" v-if="defaultProps[params.component] && defaultProps[params.component].component" v-bind="defaultProps[params.component].component.props" />
@@ -26,7 +29,22 @@
</div> </div>
<template v-if="props.length" #footer> <template v-if="props.length" #footer>
<div class="flex-1 px-4 py-5 sm:p-6 space-y-3 lg:overflow-y-auto"> <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 ? 'heroicons-outline:clipboard-check' : 'heroicons-outline:clipboard-copy'"
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 <UFormGroup
v-for="prop of props" v-for="prop of props"
:key="prop.key" :key="prop.key"
@@ -73,21 +91,6 @@
/> />
</UFormGroup> </UFormGroup>
</div> </div>
<div class="border-t u-border-gray-200">
<pre class="text-sm leading-6 u-text-gray-900 flex-1 relative flex ligatures-none lg:overflow-y-auto 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 ? 'heroicons-outline:clipboard-check' : 'heroicons-outline:clipboard-copy'"
variant="transparent"
size="sm"
:custom-class="copied ? '!text-green-500' : ''"
@click="onCopy"
/>
</pre>
</div>
</template> </template>
</UCard> </UCard>
</template> </template>

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<div class="pb-10 border-b u-border-gray-200 mb-10"> <div class="pb-10 border-b u-border-gray-200 mb-10">
@@ -83,15 +84,10 @@
Dropdown with avatar: Dropdown with avatar:
</div> </div>
<UDropdown :items="customDropdownItems" placement="bottom-end" @hover="onHover"> <UDropdown :items="customDropdownItems" placement="bottom-end">
<button class="flex"> <button class="flex">
<UAvatar src="https://picsum.photos/200/300" /> <UAvatar src="https://picsum.photos/200/300" />
</button> </button>
<template #item-with-avatar="{ item }">
<UAvatar v-if="item.avatar" :src="item.avatar" size="xxs" />
{{ item.label }}
</template>
</UDropdown> </UDropdown>
</div> </div>
@@ -158,8 +154,8 @@
Tooltip: Tooltip:
</div> </div>
<UTooltip text="Hello tooltip!"> <UTooltip text="Hello tooltip!" :shortcuts="['⌘', 'G']">
<UIcon name="heroicons-outline:information-circle" class="w-6 h-6 text-black cursor-pointer" /> <UIcon name="heroicons-outline:information-circle" class="w-6 h-6 u-text-gray-900 cursor-pointer" />
</UTooltip> </UTooltip>
</div> </div>
@@ -170,13 +166,41 @@
<UButton icon="heroicons-outline:bell" variant="red" label="Trigger an error" @click="onNotificationClick" /> <UButton icon="heroicons-outline:bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
</div> </div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Context menu:
</div>
<UCard ref="contextMenuRef" 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>
Menu
</UCard>
</UContextMenu>
</UCard>
</div>
<div> <div>
<div class="font-medium text-sm mb-1 u-text-gray-700"> <div class="font-medium text-sm mb-1 u-text-gray-700">
Command palette: Command palette:
</div> </div>
<UCard body-class=""> <UCard body-class="">
<UCommandPalette v-model="form.persons" multiple :groups="[{ key: 'persons', commands: people }]" /> <UCommandPalette
v-model="form.persons"
multiple
:placeholder="false"
:options="{
fuseOptions: {
includeMatches: true
}
}"
:groups="[{
key: 'persons',
commands: people
}]"
command-attribute="name"
/>
</UCard> </UCard>
</div> </div>
@@ -208,7 +232,7 @@
</UFormGroup> </UFormGroup>
<UFormGroup label="People" name="people" required> <UFormGroup label="People" name="people" required>
<USelectCustom v-model="form.person" name="people" :options="people" text-attribute="label" searchable /> <USelectCustom v-model="form.person" name="people" :options="people" text-attribute="name" searchable />
</UFormGroup> </UFormGroup>
<UFormGroup label="Toggle" name="toggle"> <UFormGroup label="Toggle" name="toggle">
@@ -250,13 +274,14 @@
<script setup> <script setup>
const isModalOpen = ref(false) const isModalOpen = ref(false)
const people = [ const people = ref([
{ id: 1, label: 'Durward Reynolds', disabled: false }, { id: 1, name: 'Durward Reynolds', disabled: false },
{ id: 2, label: 'Kenton Towne', disabled: false }, { id: 2, name: 'Kenton Towne', disabled: false },
{ id: 3, label: 'Therese Wunsch', disabled: false }, { id: 3, name: 'Therese Wunsch', disabled: false },
{ id: 4, label: 'Benedict Kessler', disabled: true }, { id: 4, name: 'Benedict Kessler', disabled: true },
{ id: 5, label: 'Katelyn Rohan', disabled: false } { id: 5, name: 'Katelyn Rohan', disabled: false }
] ])
const form = reactive({ const form = reactive({
email: '', email: '',
description: '', description: '',
@@ -265,12 +290,39 @@ const form = reactive({
notifications: [], notifications: [],
terms: false, terms: false,
personId: null, personId: null,
person: ref(people[0]), person: ref(people.value[0]),
persons: ref([people[0]]) persons: ref([people.value[0]])
}) })
const { $toast } = useNuxtApp() const { $toast } = useNuxtApp()
const x = ref(0)
const y = ref(0)
const isContextMenuOpen = ref(false)
const contextMenuRef = ref(null)
onMounted(() => {
document.addEventListener('mousemove', ({ clientX, clientY }) => {
x.value = clientX
y.value = clientY
})
})
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
function openContextMenu () {
const top = unref(y)
const left = unref(x)
virtualElement.value.getBoundingClientRect = () => ({
width: 0,
height: 0,
top,
left
})
isContextMenuOpen.value = true
}
function toggleModalIsOpen () { function toggleModalIsOpen () {
isModalOpen.value = !isModalOpen.value isModalOpen.value = !isModalOpen.value
} }
@@ -285,15 +337,14 @@ function onSubmit () {
console.warn('submit') console.warn('submit')
} }
function onHover (item) {
console.log(item)
}
const dropdownItems = [ const dropdownItems = [
[{ [{
label: 'Edit', label: 'Edit',
icon: 'heroicons-solid:pencil', icon: 'heroicons-solid:pencil',
click: () => onClick() click: (e) => {
e.preventDefault()
onClick()
}
}, { }, {
label: 'Duplicate', label: 'Duplicate',
icon: 'heroicons-solid:duplicate' icon: 'heroicons-solid:duplicate'
@@ -316,10 +367,9 @@ const dropdownItems = [
const customDropdownItems = [ const customDropdownItems = [
[{ [{
label: 'benjamincanac', label: 'benjamincanac',
avatar: 'https://picsum.photos/200/300', avatar: { src: 'https://picsum.photos/200/300' },
href: 'https://google.fr', href: 'https://google.fr',
target: '_blank', target: '_blank'
slot: 'item-with-avatar'
}], }],
[{ [{
label: 'About', label: 'About',

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<div class="pb-10 border-b u-border-gray-200 mb-10"> <div class="pb-10 border-b u-border-gray-200 mb-10">

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nuxthq/ui", "name": "@nuxthq/ui",
"version": "0.0.3", "version": "0.1.2",
"repository": "https://github.com/nuxtlabs/ui", "repository": "https://github.com/nuxtlabs/ui",
"license": "MIT", "license": "MIT",
"exports": { "exports": {
@@ -18,37 +18,37 @@
"build": "nuxt-module-build", "build": "nuxt-module-build",
"prepack": "yarn build", "prepack": "yarn build",
"dev": "nuxi dev docs", "dev": "nuxi dev docs",
"build:docs": "nuxi build docs", "build:docs": "nuxi generate docs",
"postbuild:docs": "mv ./docs/.vercel_build_output .vercel_build_output",
"lint": "eslint --ext .ts,.js,.vue .", "lint": "eslint --ext .ts,.js,.vue .",
"prepare": "nuxi prepare docs", "prepare": "nuxi prepare docs",
"release": "yarn lint && standard-version && git push --follow-tags" "release": "yarn lint && standard-version && git push --follow-tags"
}, },
"dependencies": { "dependencies": {
"@headlessui/vue": "^1.6.7", "@headlessui/vue": "^1.7.3",
"@iconify/vue": "^3.2.1", "@iconify/vue": "^4.0.0",
"@nuxt/kit": "npm:@nuxt/kit-edge@latest", "@nuxt/kit": "^3.0.0-rc.12",
"@nuxtjs/color-mode": "^3.1.4", "@nuxtjs/color-mode": "^3.1.8",
"@nuxtjs/tailwindcss": "^5.3.0", "@nuxtjs/tailwindcss": "^6.1.1",
"@popperjs/core": "^2.11.5", "@popperjs/core": "^2.11.6",
"@tailwindcss/aspect-ratio": "^0.4.0", "@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.0", "@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.7",
"@vueuse/core": "^8.9.4", "@vueuse/core": "^9.4.0",
"@vueuse/integrations": "^8.9.4", "@vueuse/integrations": "^9.4.0",
"defu": "^6.0.0", "defu": "^6.1.0",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"tailwindcss": "^3.1.6" "tailwindcss": "^3.2.1"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/module-builder": "^0.1.7", "@nuxt/module-builder": "^0.2.0",
"@nuxtjs/eslint-config-typescript": "^10.0.0", "@nuxtjs/eslint-config-typescript": "^11.0.0",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.6",
"eslint": "^8.20.0", "eslint": "^8.26.0",
"nuxt": "npm:nuxt3@latest", "nuxt": "^3.0.0-rc.9",
"standard-version": "^9.5.0" "standard-version": "^9.5.0",
"vue-tsc": "^1.0.9"
}, },
"build": { "build": {
"externals": [ "externals": [

View File

@@ -1,4 +1,4 @@
import { defineNuxtModule, installModule, addComponentsDir, addTemplate, addPlugin, createResolver } from '@nuxt/kit' import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, addTemplate, addPlugin, createResolver } from '@nuxt/kit'
import { defu } from 'defu' import { defu } from 'defu'
import colors from 'tailwindcss/colors.js' import colors from 'tailwindcss/colors.js'
import type { Config } from 'tailwindcss' import type { Config } from 'tailwindcss'
@@ -60,8 +60,7 @@ export default defineNuxtModule<ModuleOptions>({
version, version,
configKey: 'ui', configKey: 'ui',
compatibility: { compatibility: {
nuxt: '^3.0.0', nuxt: '^3.0.0-rc.8'
bridge: true
} }
}, },
defaults, defaults,
@@ -115,6 +114,11 @@ export default defineNuxtModule<ModuleOptions>({
filename: 'ui.mjs', filename: 'ui.mjs',
getContents: () => `export default ${JSON.stringify(ui)}` 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/color-mode', { classSuffix: '' })
@@ -178,9 +182,6 @@ export default defineNuxtModule<ModuleOptions>({
watch: false watch: false
}) })
// Add composables addImportsDir(resolve(runtimeDir, 'composables'))
nuxt.hook('autoImports:dirs', (dirs) => {
dirs.push(resolve(runtimeDir, 'composables'))
})
} }
}) })

View File

@@ -1,5 +1,11 @@
<template> <template>
<div class="flex"> <div class="flex flex-row-reverse">
<Avatar
v-if="remainingGroupSize > 0"
:size="size"
:text="`+${remainingGroupSize}`"
:class="avatarClass"
/>
<Avatar <Avatar
v-for="(avatar, index) of displayedGroup" v-for="(avatar, index) of displayedGroup"
:key="index" :key="index"
@@ -7,19 +13,13 @@
:size="size" :size="size"
:class="avatarClass" :class="avatarClass"
/> />
<Avatar
v-if="remainingGroupSize > 0"
:size="size"
:text="`+${remainingGroupSize}`"
:class="avatarClass"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { classNames } from '../../utils/' import { classNames } from '../../utils'
import Avatar from './Avatar' import Avatar from './Avatar.vue'
import $ui from '#build/ui' import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({
@@ -55,9 +55,9 @@ const avatars = computed(() => {
}) })
const displayedGroup = computed(() => { const displayedGroup = computed(() => {
if (!props.max) { return avatars.value } if (!props.max) { return [...avatars.value].reverse() }
return avatars.value.slice(0, props.max) return avatars.value.slice(0, props.max).reverse()
}) })
const remainingGroupSize = computed(() => { const remainingGroupSize = computed(() => {

View File

@@ -14,8 +14,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, useSlots } from 'vue' import { ref, computed, useSlots } from 'vue'
import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router'
import NuxtLink from '#app/components/nuxt-link' import NuxtLink from '#app/components/nuxt-link'
import Icon from '../elements/Icon' import Icon from '../elements/Icon.vue'
import { classNames } from '../../utils' import { classNames } from '../../utils'
import $ui from '#build/ui' import $ui from '#build/ui'
@@ -79,7 +81,7 @@ const props = defineProps({
default: false default: false
}, },
to: { to: {
type: [String, Object], type: [String, Object] as PropType<string | RouteLocationNormalized>,
default: null default: null
}, },
target: { target: {

View File

@@ -6,25 +6,21 @@
</slot> </slot>
</MenuButton> </MenuButton>
<div v-if="open" ref="container" :class="containerClass" @mouseover="onMouseOver"> <div v-if="open" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
<transition <transition appear v-bind="transitionClass">
appear
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<MenuItems :class="baseClass" static> <MenuItems :class="baseClass" static>
<div v-for="(subItems, index) of items" :key="index" class="py-1"> <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" as="div"> <MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled }" :disabled="item.disabled" @click="e => item.click?.(e)">
<Component v-bind="item" :is="(item.to && NuxtLink) || (item.click && 'button') || 'div'" :class="resolveItemClass({ active, disabled })" @click="onItemClick(item)" @mouseover="$emit('hover', item)"> <Component v-bind="item" :is="(item.to && NuxtLink) || (item.click && 'button') || 'div'" :class="resolveItemClass({ active, disabled })">
<slot :name="item.slot" :item="item"> <slot :name="item.slot" :item="item">
<Icon v-if="item.icon" :name="item.icon" :class="itemIconClass" /> <Icon v-if="item.icon" :name="item.icon" :class="[itemIconClass, item.iconClass]" />
<Avatar v-if="item.avatar" :src="item.avatar" :alt="item.label" :class="itemAvatarClass" size="xs" /> <Avatar v-if="item.avatar" v-bind="{ size: 'xxs', ...item.avatar }" :class="itemAvatarClass" />
<span class="truncate">{{ item.label }}</span> <span class="truncate">{{ item.label }}</span>
<span v-if="item.shortcuts?.length" :class="itemShortcutsClass">
<kbd v-for="shortcut of item.shortcuts" :key="shortcut" class="font-sans">{{ shortcut }}</kbd>
</span>
</slot> </slot>
</Component> </Component>
</MenuItem> </MenuItem>
@@ -42,34 +38,35 @@ import {
MenuItems, MenuItems,
MenuItem MenuItem
} from '@headlessui/vue' } from '@headlessui/vue'
import type { Ref, PropType } from 'vue'
import type { Ref } from 'vue' import type { RouteLocationNormalized } from 'vue-router'
import { ref, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { defu } from 'defu'
import NuxtLink from '#app/components/nuxt-link' import NuxtLink from '#app/components/nuxt-link'
import Icon from '../elements/Icon' import Icon from '../elements/Icon.vue'
import Avatar from '../elements/Avatar' import Avatar from '../elements/Avatar.vue'
import { classNames, usePopper } from '../../utils' import { classNames } from '../../utils'
import { usePopper } from '../../composables/usePopper'
import type { Avatar as AvatarType } from '../../types/avatar'
import type { PopperOptions } from '../../types'
import $ui from '#build/ui' import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({
items: { items: {
type: Array, type: Array as PropType<{
to: RouteLocationNormalized
exact: boolean
label: string
disabled?: boolean
slot?: string
icon?: string
iconClass?: string
avatar?: Partial<AvatarType>
click?: Function
shortcuts?: string[]
}[][]>,
default: () => [] default: () => []
}, },
placement: {
type: String,
default: 'bottom-end',
validator: (value: string) => {
return ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'].includes(value)
}
},
strategy: {
type: String,
default: 'fixed',
validator: (value: string) => {
return ['absolute', 'fixed'].includes(value)
}
},
mode: { mode: {
type: String, type: String,
default: 'click', default: 'click',
@@ -85,10 +82,22 @@ const props = defineProps({
type: String, type: String,
default: () => $ui.dropdown.container default: () => $ui.dropdown.container
}, },
widthClass: {
type: String,
default: () => $ui.dropdown.width
},
baseClass: { baseClass: {
type: String, type: String,
default: () => $ui.dropdown.base default: () => $ui.dropdown.base
}, },
transitionClass: {
type: Object,
default: () => $ui.dropdown.transition
},
groupClass: {
type: String,
default: () => $ui.dropdown.group
},
itemBaseClass: { itemBaseClass: {
type: String, type: String,
default: () => $ui.dropdown.item.base default: () => $ui.dropdown.item.base
@@ -112,34 +121,28 @@ const props = defineProps({
itemAvatarClass: { itemAvatarClass: {
type: String, type: String,
default: () => $ui.dropdown.item.avatar default: () => $ui.dropdown.item.avatar
},
itemShortcutsClass: {
type: String,
default: () => $ui.dropdown.item.shortcuts
},
popperOptions: {
type: Object as PropType<PopperOptions>,
default: () => {}
},
openDelay: {
type: Number,
default: 50
},
closeDelay: {
type: Number,
default: 0
} }
}) })
defineEmits(['hover']) const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.dropdown.popperOptions))
const [trigger, container] = usePopper({ const [trigger, container] = usePopper(popperOptions.value)
placement: props.placement,
strategy: props.strategy,
modifiers: [{
name: 'offset',
options: {
offset: 0
}
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
},
{
name: 'preventOverflow',
options: {
padding: 8
}
}]
})
function resolveItemClass ({ active, disabled }: { active: boolean, disabled: boolean }) { function resolveItemClass ({ active, disabled }: { active: boolean, disabled: boolean }) {
return classNames( return classNames(
@@ -149,31 +152,27 @@ function resolveItemClass ({ active, disabled }: { active: boolean, disabled: bo
) )
} }
function onItemClick (item: any) { // https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/menu/menu.ts#L131
if (item.disabled) {
return
}
if (item.click) {
item.click()
}
}
const menuApi: Ref<any> = ref(null) const menuApi: Ref<any> = ref(null)
let openTimeout: NodeJS.Timeout | null = null let openTimeout: NodeJS.Timeout | null = null
let closeTimeout: NodeJS.Timeout | null = null let closeTimeout: NodeJS.Timeout | null = null
onMounted(() => { onMounted(() => {
setTimeout(() => { setTimeout(() => {
const menuProvides = trigger.value?.$.provides const menuProvides = trigger.value?.$.provides
if (!menuProvides) {
return
}
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides) const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]] menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
// stop trigger click propagation on hover // stop trigger click propagation on hover
menuApi.value?.buttonRef.addEventListener('click', (e: Event) => { menuApi.value?.buttonRef?.addEventListener('click', (e: Event) => {
if (props.mode === 'hover') { if (props.mode === 'hover') {
e.stopPropagation() e.stopPropagation()
} }
}, true) }, true)
}, 0) }, 200)
}) })
function onMouseOver () { function onMouseOver () {
@@ -193,7 +192,7 @@ function onMouseOver () {
openTimeout = openTimeout || setTimeout(() => { openTimeout = openTimeout || setTimeout(() => {
menuApi.value.openMenu && menuApi.value.openMenu() menuApi.value.openMenu && menuApi.value.openMenu()
openTimeout = null openTimeout = null
}, 50) }, props.openDelay)
} }
function onMouseLeave () { function onMouseLeave () {
@@ -213,7 +212,7 @@ function onMouseLeave () {
closeTimeout = closeTimeout || setTimeout(() => { closeTimeout = closeTimeout || setTimeout(() => {
menuApi.value.closeMenu && menuApi.value.closeMenu() menuApi.value.closeMenu && menuApi.value.closeMenu()
closeTimeout = null closeTimeout = null
}, 0) }, props.closeDelay)
} }
</script> </script>

View File

@@ -7,10 +7,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { IconifyIcon } from '@iconify/vue' import type { IconifyIcon } from '@iconify/vue'
import { computed, ref, watch } from 'vue'
import { Icon as Iconify } from '@iconify/vue/dist/offline' import { Icon as Iconify } from '@iconify/vue/dist/offline'
import { loadIcon } from '@iconify/vue' import { loadIcon } from '@iconify/vue'
import { useNuxtApp, useState } from '#imports' import { useNuxtApp, useState, computed, watch, onMounted } from '#imports'
const props = defineProps({ const props = defineProps({
name: { name: {
@@ -20,19 +19,31 @@ const props = defineProps({
}) })
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const state = useState('u-icons', () => ({})) const state = useState('u-icons', () => ({}))
const isFetching = ref(false)
const icon = computed<IconifyIcon | null>(() => state.value?.[props.name]) const isFetching = computed(() => !!state.value?.[props.name]?.then)
const icon = computed<IconifyIcon | null>(() => !state.value?.[props.name] || state.value[props.name].then ? null : state.value[props.name])
const component = computed(() => nuxtApp.vueApp.component(props.name)) const component = computed(() => nuxtApp.vueApp.component(props.name))
const loadIconComponent = async () => {
if (component.value) { return } const loadIconComponent = (name: string) => {
if (!state.value?.[props.name]) { state.value = state.value || {}
isFetching.value = true if (nuxtApp.vueApp.component(props.name) || state.value[name] || state.value[name] === null) { return state.value[name] }
state.value[props.name] = await loadIcon(props.name).catch(() => null)
isFetching.value = false 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)
} }
watch(() => props.name, loadIconComponent)
!component.value && (await loadIconComponent())
</script> </script>
<script lang="ts"> <script lang="ts">

View File

@@ -24,12 +24,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
const props = defineProps({ const props = defineProps({
...RouterLink.props, ...RouterLink.props,
to: { to: {
type: [String, Object], type: [String, Object] as PropType<string | RouteLocationNormalized>,
default: null default: null
}, },
inactiveClass: { inactiveClass: {

View File

@@ -25,7 +25,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import Icon from '../elements/Icon' import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router'
import Icon from '../elements/Icon.vue'
const props = defineProps({ const props = defineProps({
variant: { variant: {
@@ -36,7 +38,7 @@ const props = defineProps({
} }
}, },
to: { to: {
type: [String, Object], type: [String, Object] as PropType<string | RouteLocationNormalized>,
default: null default: null
}, },
click: { click: {

View File

@@ -23,9 +23,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { DialogTitle, DialogDescription } from '@headlessui/vue' import { DialogTitle, DialogDescription } from '@headlessui/vue'
import Modal from '../overlays/Modal' import Modal from '../overlays/Modal.vue'
import Button from '../elements/Button' import Button from '../elements/Button.vue'
import Icon from '../elements/Icon' import Icon from '../elements/Icon.vue'
import $ui from '#build/ui' import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({

View File

@@ -30,7 +30,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import Icon from '../elements/Icon' import Icon from '../elements/Icon.vue'
import { classNames } from '../../utils' import { classNames } from '../../utils'
import $ui from '#build/ui' import $ui from '#build/ui'
@@ -72,7 +72,7 @@ const props = defineProps({
default: null default: null
}, },
spellcheck: { spellcheck: {
type: String, type: Boolean,
default: null default: null
}, },
icon: { icon: {

View File

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

View File

@@ -1,18 +1,20 @@
<template> <template>
<Combobox <Combobox
v-slot="{ open }" v-slot="{ open }"
:by="by"
:model-value="modelValue" :model-value="modelValue"
:multiple="multiple" :multiple="multiple"
:nullable="nullable" :nullable="nullable"
:disabled="disabled"
as="div" as="div"
:class="wrapperClass" :class="wrapperClass"
@update:model-value="$emit('update:modelValue', $event)" @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">
<ComboboxButton ref="trigger" as="div"> <ComboboxButton ref="trigger" v-slot="{ disabled: buttonDisabled }" as="div">
<slot :open="open"> <slot :open="open" :disabled="buttonDisabled">
<button :class="selectCustomClass"> <button :class="selectCustomClass" :disabled="disabled" type="button">
<slot name="label"> <slot name="label">
<span v-if="modelValue" class="block truncate">{{ modelValue[textAttribute] }}</span> <span v-if="modelValue" class="block truncate">{{ modelValue[textAttribute] }}</span>
<span v-else class="block truncate u-text-gray-400">{{ placeholder }}</span> <span v-else class="block truncate u-text-gray-400">{{ placeholder }}</span>
@@ -26,11 +28,12 @@
</slot> </slot>
</ComboboxButton> </ComboboxButton>
<div v-if="open" ref="container" :class="listContainerClass"> <div v-if="open" ref="container" :class="[listContainerClass, listWidthClass]">
<transition appear leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100" leave-to-class="opacity-0"> <transition appear v-bind="listTransitionClass">
<ComboboxOptions static :class="listBaseClass"> <ComboboxOptions static :class="listBaseClass">
<ComboboxInput <ComboboxInput
v-if="searchable" v-if="searchable"
ref="searchInput"
:display-value="() => query" :display-value="() => query"
name="q" name="q"
placeholder="Search..." placeholder="Search..."
@@ -41,13 +44,13 @@
/> />
<ComboboxOption <ComboboxOption
v-for="(option, index) in filteredOptions" v-for="(option, index) in filteredOptions"
v-slot="{ active, selected, disabled }" v-slot="{ active, selected, disabled: optionDisabled }"
:key="index" :key="index"
as="template" as="template"
:value="option" :value="option"
:disabled="option.disabled" :disabled="option.disabled"
> >
<li :class="resolveOptionClass({ active, selected, disabled })"> <li :class="resolveOptionClass({ active, selected, disabled: optionDisabled })">
<div :class="listOptionContainerClass"> <div :class="listOptionContainerClass">
<slot name="option" :option="option" :active="active" :selected="selected"> <slot name="option" :option="option" :active="active" :selected="selected">
<span class="block truncate">{{ option[textAttribute] }}</span> <span class="block truncate">{{ option[textAttribute] }}</span>
@@ -82,6 +85,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import type { PropType, ComponentPublicInstance } from 'vue'
import { defu } from 'defu'
import { import {
Combobox, Combobox,
ComboboxButton, ComboboxButton,
@@ -89,8 +94,10 @@ import {
ComboboxOption, ComboboxOption,
ComboboxInput ComboboxInput
} from '@headlessui/vue' } from '@headlessui/vue'
import Icon from '../elements/Icon' import Icon from '../elements/Icon.vue'
import { classNames, usePopper } from '../../utils' import { classNames } from '../../utils'
import { usePopper } from '../../composables/usePopper'
import type { PopperOptions } from '../../types'
import $ui from '#build/ui' import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({
@@ -98,28 +105,22 @@ const props = defineProps({
type: [String, Number, Object, Array], type: [String, Number, Object, Array],
default: '' default: ''
}, },
by: {
type: String,
default: undefined
},
options: { options: {
type: Array, type: Array as PropType<{ [key: string]: any, disabled?: boolean }[]>,
default: () => [] default: () => []
}, },
placement: {
type: String,
default: 'bottom-end',
validator: (value: string) => {
return ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'].includes(value)
}
},
strategy: {
type: String,
default: 'absolute',
validator: (value: string) => {
return ['absolute', 'fixed'].includes(value)
}
},
required: { required: {
type: Boolean, type: Boolean,
default: false default: false
}, },
disabled: {
type: Boolean,
default: false
},
multiple: { multiple: {
type: Boolean, type: Boolean,
default: false default: false
@@ -157,7 +158,7 @@ const props = defineProps({
}, },
icon: { icon: {
type: String, type: String,
default: () => 'heroicons-solid:selector' default: () => $ui.selectCustom.icon.name
}, },
iconBaseClass: { iconBaseClass: {
type: String, type: String,
@@ -182,10 +183,18 @@ const props = defineProps({
type: String, type: String,
default: () => $ui.selectCustom.list.container default: () => $ui.selectCustom.list.container
}, },
listWidthClass: {
type: String,
default: () => $ui.selectCustom.list.width
},
listInputClass: { listInputClass: {
type: String, type: String,
default: () => $ui.selectCustom.list.input default: () => $ui.selectCustom.list.input
}, },
listTransitionClass: {
type: Object,
default: () => $ui.selectCustom.list.transition
},
listOptionBaseClass: { listOptionBaseClass: {
type: String, type: String,
default: () => $ui.selectCustom.list.option.base default: () => $ui.selectCustom.list.option.base
@@ -220,7 +229,7 @@ const props = defineProps({
}, },
listOptionIcon: { listOptionIcon: {
type: String, type: String,
default: () => 'heroicons-solid:check' default: () => $ui.selectCustom.list.option.icon.name
}, },
listOptionIconBaseClass: { listOptionIconBaseClass: {
type: String, type: String,
@@ -245,36 +254,21 @@ const props = defineProps({
searchAttributes: { searchAttributes: {
type: Array, type: Array,
default: null default: null
},
popperOptions: {
type: Object as PropType<PopperOptions>,
default: () => {}
} }
}) })
defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const [trigger, container] = usePopper({ const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.selectCustom.popperOptions))
placement: props.placement,
strategy: props.strategy, const [trigger, container] = usePopper(popperOptions.value)
modifiers: [{
name: 'offset',
options: {
offset: 0
}
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
},
{
name: 'preventOverflow',
options: {
padding: 8
}
}]
})
const query = ref('') const query = ref('')
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
const selectCustomClass = computed(() => { const selectCustomClass = computed(() => {
return classNames( return classNames(
@@ -328,6 +322,15 @@ function resolveOptionIconClass ({ active }: { active: boolean }) {
active ? props.listOptionIconActiveClass : props.listOptionIconInactiveClass active ? props.listOptionIconActiveClass : props.listOptionIconInactiveClass
) )
} }
function onUpdate (event) {
if (query.value && searchInput.value?.$el) {
query.value = ''
// explicitly set input text because `ComboboxInput` `displayValue` is not reactive
searchInput.value.$el.value = ''
}
emit('update:modelValue', event)
}
</script> </script>
<script lang="ts"> <script lang="ts">

View File

@@ -17,7 +17,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { Switch } from '@headlessui/vue' import { Switch } from '@headlessui/vue'
import Icon from '../elements/Icon' import Icon from '../elements/Icon.vue'
import $ui from '#build/ui' import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({

View File

@@ -24,7 +24,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { classNames } from '../../utils/' import { classNames } from '../../utils'
import $ui from '#build/ui' import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({

View File

@@ -6,7 +6,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { classNames } from '../../utils/' import { classNames } from '../../utils'
import $ui from '#build/ui' import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({

View File

@@ -1,13 +1,15 @@
<template> <template>
<Combobox <Combobox
ref="comboboxRef"
:by="by"
:model-value="modelValue" :model-value="modelValue"
:multiple="multiple" :multiple="multiple"
:nullable="nullable" :nullable="nullable"
@update:model-value="onSelect" @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="flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800">
<div class="relative flex items-center"> <div v-show="searchable" class="relative flex items-center">
<UIcon :name="inputIcon" class="pointer-events-none absolute top-3.5 left-5 h-5 w-5 u-text-gray-400" aria-hidden="true" /> <Icon :name="inputIcon" class="pointer-events-none absolute top-3.5 left-5 h-5 w-5 u-text-gray-400" aria-hidden="true" />
<ComboboxInput <ComboboxInput
ref="comboboxInput" ref="comboboxInput"
:value="query" :value="query"
@@ -17,16 +19,20 @@
@change="query = $event.target.value" @change="query = $event.target.value"
/> />
<UButton v-if="closeIcon" :icon="closeIcon" variant="transparent" class="absolute right-3" @click="onClear" /> <Button v-if="closeIcon" :icon="closeIcon" variant="transparent" class="absolute right-3" @click="onClear" />
</div> </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"> <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" /> <CommandPaletteGroup v-for="group of groups" :key="group.key" :group="group" :group-attribute="groupAttribute" :command-attribute="commandAttribute">
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</CommandPaletteGroup>
</ComboboxOptions> </ComboboxOptions>
<div v-else class="flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14"> <div v-else-if="placeholder" class="flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14">
<UIcon :name="emptyIcon" class="w-6 h-6 mx-auto u-text-gray-400" aria-hidden="true" /> <Icon :name="emptyIcon" class="w-6 h-6 mx-auto u-text-gray-400" aria-hidden="true" />
<p class="mt-4 text-sm u-text-gray-900"> <p class="mt-4 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." }} {{ query ? "We couldn't find any items with that term. Please try again." : "We couldn't find any items." }}
</p> </p>
</div> </div>
@@ -39,9 +45,12 @@ import { ref, computed, onMounted } from 'vue'
import { Combobox, ComboboxInput, ComboboxOptions } from '@headlessui/vue' import { Combobox, ComboboxInput, ComboboxOptions } from '@headlessui/vue'
import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue' import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue'
import { useFuse } from '@vueuse/integrations/useFuse' import { useFuse } from '@vueuse/integrations/useFuse'
import { groupBy, map } from 'lodash-es'
import { defu } from 'defu' import { defu } from 'defu'
import type { UseFuseOptions } from '@vueuse/integrations/useFuse' import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import type { Group, Command } from '../../types/command-palette' 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 CommandPaletteGroup from './CommandPaletteGroup.vue'
const props = defineProps({ const props = defineProps({
@@ -49,6 +58,10 @@ const props = defineProps({
type: [String, Number, Object, Array], type: [String, Number, Object, Array],
default: null default: null
}, },
by: {
type: String,
default: 'id'
},
multiple: { multiple: {
type: Boolean, type: Boolean,
default: false default: false
@@ -57,6 +70,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false default: false
}, },
searchable: {
type: Boolean,
default: true
},
groups: { groups: {
type: Array as PropType<Group[]>, type: Array as PropType<Group[]>,
default: () => [] default: () => []
@@ -77,9 +94,25 @@ const props = defineProps({
type: String, type: String,
default: 'heroicons-outline:search' default: 'heroicons-outline:search'
}, },
groupAttribute: {
type: String,
default: 'label'
},
commandAttribute: {
type: String,
default: 'label'
},
options: { options: {
type: Object as PropType<Partial<UseFuseOptions<Command>>>, type: Object as PropType<Partial<UseFuseOptions<Command>>>,
default: () => ({}) default: () => {}
},
autoselect: {
type: Boolean,
default: true
},
placeholder: {
type: Boolean,
default: true
} }
}) })
@@ -87,31 +120,54 @@ const emit = defineEmits(['update:modelValue', 'close'])
const query = ref('') const query = ref('')
const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>() const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>()
const comboboxApi = ref(null)
onMounted(() => { onMounted(() => {
activateFirstOption() if (props.autoselect) {
activateFirstOption()
}
})
onMounted(() => {
setTimeout(() => {
const popoverProvides = comboboxInput.value?.$.provides
if (!popoverProvides) {
return
}
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
comboboxApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
}, 200)
}) })
const options: ComputedRef<Partial<UseFuseOptions<Command>>> = computed(() => defu({}, props.options, { const options: ComputedRef<Partial<UseFuseOptions<Command>>> = computed(() => defu({}, props.options, {
fuseOptions: { fuseOptions: {
keys: ['label'] keys: [props.commandAttribute]
}, },
resultLimit: 12, resultLimit: 12,
matchAllWhenSearchEmpty: true matchAllWhenSearchEmpty: true
})) }))
const fuse = props.groups.reduce((acc, group) => { const commands = computed(() => props.groups.reduce((acc, group) => {
const fuse = useFuse(query, group.commands, defu({}, group.options || {}, options.value)) return acc.concat(group.commands.map(command => ({ ...command, group: group.key })))
acc[group.key] = fuse }, [] as Command[]))
return acc
}, {}) 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
return {
...item,
...data
}
})
const groups = computed(() => props.groups.map((group) => {
return { return {
...group, ...props.groups.find(group => group.key === key),
commands: fuse[group.key].results.value.map(result => result.item).slice(0, group.options?.resultLimit || options.value.resultLimit) commands: commands.slice(0, options.value.resultLimit)
} }
}).filter(group => group.commands.length)) }))
// Methods // Methods
@@ -124,7 +180,7 @@ function activateFirstOption () {
} }
function onSelect (option: Command | Command[]) { function onSelect (option: Command | Command[]) {
emit('update:modelValue', option) emit('update:modelValue', option, { query: query.value })
// Clear input after selection // Clear input after selection
if (!props.multiple) { if (!props.multiple) {
@@ -141,6 +197,14 @@ function onClear () {
emit('close') emit('close')
} }
} }
defineExpose({
updateQuery: (q) => {
query.value = q
},
comboboxApi,
results
})
</script> </script>
<script lang="ts"> <script lang="ts">

View File

@@ -1,7 +1,7 @@
<template> <template>
<li class="p-2"> <li class="p-2">
<h2 v-if="group.label" class="px-3 my-2 text-xs font-semibold u-text-gray-900"> <h2 v-if="group[groupAttribute]" class="px-3 my-2 text-xs font-semibold u-text-gray-900">
{{ group.label }} {{ group[groupAttribute] }}
</h2> </h2>
<ul class="text-sm u-text-gray-700"> <ul class="text-sm u-text-gray-700">
@@ -13,32 +13,37 @@
:disabled="command.disabled" :disabled="command.disabled"
as="template" as="template"
> >
<li :class="['flex justify-between select-none items-center rounded-md px-3 py-2 u-text-gray-400 gap-3 relative', active && 'bg-gray-100 dark:bg-gray-800 u-text-gray-900', command.disabled ? 'cursor-not-allowed' : 'cursor-pointer']"> <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 items-center flex-1 gap-3 min-w-0"> <div class="flex items-center gap-2 min-w-0">
<UIcon v-if="command.icon" :name="command.icon" :class="['h-4 w-4', command.iconClass]" class="flex-shrink-0" aria-hidden="true" /> <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" />
<UAvatar <Avatar
v-else-if="command.avatar" v-else-if="command.avatar"
:src="command.avatar" v-bind="{ size: 'xxxs', ...command.avatar }"
:alt="command.label"
:rounded="false"
size="xxxs"
class="flex-shrink-0" class="flex-shrink-0"
/> />
<span v-else-if="command.chip" class="flex-shrink-0 w-2 h-2 rounded-full" :style="{ background: `#${command.chip}` }" /> <span v-else-if="command.chip" class="flex-shrink-0 w-2 h-2 mx-1 rounded-full" :style="{ background: `#${command.chip}` }" />
<div class="flex items-center flex-1 min-w-0 u-text-gray-400 gap-1.5" :class="{ 'opacity-50': command.disabled }"> <div class="flex items-center gap-1.5 min-w-0" :class="{ 'opacity-50': command.disabled }">
<span v-if="command.prefix">{{ command.prefix }}</span> <slot :name="`${group.key}-command`" :group="group" :command="command">
<span class="u-text-gray-700 truncate">{{ command.label }}</span> <span v-if="command.prefix" class="u-text-gray-400">{{ command.prefix }}</span>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="command.matches?.length" class="truncate" :class="{ 'flex-none': command.suffix }" v-html="highlight(command.matches[0])" />
<span v-else class="truncate" :class="{ 'flex-none': command.suffix }">{{ command[commandAttribute] }}</span>
<span v-if="command.suffix" class="u-text-gray-400 truncate">{{ command.suffix }}</span>
</slot>
</div> </div>
</div> </div>
<span v-if="selected" class="absolute inset-y-0 right-0 flex items-center pr-2 u-text-gray-900"> <Icon v-if="selected" name="heroicons-outline:check" class="h-5 w-5 u-text-gray-900 flex-shrink-0" aria-hidden="true" />
<UIcon name="heroicons-outline:check" class="h-5 w-5" aria-hidden="true" /> <slot v-else-if="active && (group.active || $slots[`${group.key}-active`])" :name="`${group.key}-active`" :group="group" :command="command">
</span> <span v-if="group.active" class="flex-shrink-0 u-text-gray-500">{{ group.active }}</span>
<span v-else-if="active && command.suffix" class="flex-none u-text-gray-500">{{ command.suffix }}</span> </slot>
<span v-else-if="command.shortcuts?.length" class="flex-none text-xs font-semibold u-text-gray-500"> <slot v-else :name="`${group.key}-inactive`" :group="group" :command="command">
<kbd v-for="shortcut of command.shortcuts" :key="shortcut" class="font-sans">{{ shortcut }}</kbd> <span v-if="command.shortcuts?.length" class="flex-shrink-0 text-xs font-semibold u-text-gray-500">
</span> <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> </li>
</ComboboxOption> </ComboboxOption>
</ul> </ul>
@@ -48,14 +53,33 @@
<script setup lang="ts"> <script setup lang="ts">
import { ComboboxOption } from '@headlessui/vue' import { ComboboxOption } from '@headlessui/vue'
import type { PropType } from '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 type { Group } from '../../types/command-palette'
defineProps({ defineProps({
group: { group: {
type: Object as PropType<Group>, type: Object as PropType<Group>,
required: true required: true
},
groupAttribute: {
type: String,
required: true
},
commandAttribute: {
type: String,
required: true
} }
}) })
function highlight ({ indices, value }, i = 1) {
const pair = indices[indices.length - i]
if (!pair) {
return value
}
return `${highlight({ indices, value: value.substring(0, pair[0]) }, i + 1)}<mark>${value.substring(pair[0], pair[1] + 1)}</mark>${value.substring(pair[1] + 1)}`
}
</script> </script>
<script lang="ts"> <script lang="ts">

View File

@@ -15,12 +15,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Link from '../elements/Link' import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router'
import Link from '../elements/Link.vue'
import $ui from '#build/ui' import $ui from '#build/ui'
defineProps({ defineProps({
links: { links: {
type: Array, type: Array as PropType<{ to: RouteLocationNormalized, exact: boolean, label: string }[]>,
required: true required: true
}, },
wrapperClass: { wrapperClass: {

View File

@@ -15,12 +15,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Link from '../elements/Link' import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router'
import Link from '../elements/Link.vue'
import $ui from '#build/ui' import $ui from '#build/ui'
defineProps({ defineProps({
links: { links: {
type: Array, type: Array as PropType<{ to: RouteLocationNormalized, exact: boolean, label: string }[]>,
required: true required: true
}, },
wrapperClass: { wrapperClass: {

View File

@@ -14,11 +14,11 @@
<slot name="avatar" :link="link"> <slot name="avatar" :link="link">
<Avatar <Avatar
v-if="link.avatar" v-if="link.avatar"
v-bind="link.avatar" v-bind="{ size: 'xs', ...link.avatar }"
:class="[avatarBaseClass, link.label && avatarSpacingClass]" :class="[avatarBaseClass, link.label && avatarSpacingClass]"
/> />
</slot> </slot>
<slot name="icon" :link="link"> <slot name="icon" :link="link" :is-active="isActive">
<Icon <Icon
v-if="link.icon" v-if="link.icon"
:name="link.icon" :name="link.icon"
@@ -28,7 +28,7 @@
<slot :link="link"> <slot :link="link">
<span v-if="link.label" class="truncate">{{ link.label }}</span> <span v-if="link.label" class="truncate">{{ link.label }}</span>
</slot> </slot>
<slot name="badge" :link="link"> <slot name="badge" :link="link" :is-active="isActive">
<span v-if="link.badge" :class="[badgeBaseClass, isActive ? badgeActiveClass : badgeInactiveClass]"> <span v-if="link.badge" :class="[badgeBaseClass, isActive ? badgeActiveClass : badgeInactiveClass]">
{{ link.badge }} {{ link.badge }}
</span> </span>
@@ -38,14 +38,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Icon from '../elements/Icon' import type { PropType } from 'vue'
import Link from '../elements/Link' import Icon from '../elements/Icon.vue'
import Avatar from '../elements/Avatar' import Link from '../elements/Link.vue'
import Avatar from '../elements/Avatar.vue'
import type { Avatar as AvatarType } from '../../types/avatar'
import $ui from '#build/ui' import $ui from '#build/ui'
defineProps({ defineProps({
links: { links: {
type: Array, type: Array as PropType<{
label: string
icon?: string
iconClass?: string
avatar?: Partial<AvatarType>
click?: Function
badge?: string
}[]>,
required: true required: true
}, },
wrapperClass: { wrapperClass: {

View File

@@ -0,0 +1,73 @@
<template>
<div v-if="isOpen" ref="container" :class="[containerClass, widthClass]">
<transition appear v-bind="transitionClass">
<div :class="baseClass">
<slot />
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { PropType, computed, toRef } from 'vue'
import { defu } from 'defu'
import { usePopper } from '../../composables/usePopper'
import type { PopperOptions } from '../../types'
import $ui from '#build/ui'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
virtualElement: {
type: Object,
required: true
},
wrapperClass: {
type: String,
default: () => $ui.contextMenu.wrapper
},
containerClass: {
type: String,
default: () => $ui.contextMenu.container
},
widthClass: {
type: String,
default: () => $ui.contextMenu.width
},
baseClass: {
type: String,
default: () => $ui.contextMenu.base
},
transitionClass: {
type: Object,
default: () => $ui.contextMenu.transition
},
popperOptions: {
type: Object as PropType<PopperOptions>,
default: () => {}
}
})
const emit = defineEmits(['update:modelValue', 'close'])
const isOpen = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const virtualElement = toRef(props, 'virtualElement')
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.contextMenu.popperOptions))
const [, container] = usePopper(popperOptions.value, virtualElement)
</script>
<script lang="ts">
export default { name: 'UContextMenu' }
</script>

View File

@@ -1,52 +1,41 @@
<template> <template>
<TransitionRoot :appear="appear" :show="isOpen" as="template"> <TransitionRoot :appear="appear" :show="isOpen" as="template">
<Dialog class="relative z-50" @close="close"> <Dialog :class="wrapperClass" @close="close">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <TransitionChild
<TransitionChild v-if="overlay"
as="template" as="template"
:appear="appear" :appear="appear"
enter="ease-out duration-300" v-bind="overlayTransition"
enter-from="opacity-0" >
enter-to="opacity-100" <div class="fixed inset-0 transition-opacity" :class="overlayBackgroundClass" />
leave="ease-in duration-200" </TransitionChild>
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 transition-opacity" :class="overlayClass" />
</TransitionChild>
<div class="fixed inset-0 overflow-y-auto"> <div :class="innerClass" :style="innerStyle">
<div class="flex min-h-full items-end sm:items-center justify-center p-4 text-center"> <div :class="containerClass">
<TransitionChild <TransitionChild
as="template" as="template"
:appear="appear" :appear="appear"
enter="ease-out duration-300" v-bind="modalTransition"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" >
enter-to="opacity-100 translate-y-0 sm:scale-100" <DialogPanel :class="modalClass">
leave="ease-in duration-200" <Card
leave-from="opacity-100 translate-y-0 sm:scale-100" base-class=""
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" background-class=""
> shadow-class=""
<DialogPanel :class="modalClass"> ring-class=""
<Card rounded-class=""
base-class v-bind="$attrs"
background-class >
shadow-class <template v-if="$slots.header" #header>
ring-class <slot name="header" />
rounded-class </template>
v-bind="$attrs" <slot />
> <template v-if="$slots.footer" #footer>
<template v-if="$slots.header" #header> <slot name="footer" />
<slot name="header" /> </template>
</template> </Card>
<slot /> </DialogPanel>
<template v-if="$slots.footer" #footer> </TransitionChild>
<slot name="footer" />
</template>
</Card>
</DialogPanel>
</TransitionChild>
</div>
</div> </div>
</div> </div>
</Dialog> </Dialog>
@@ -56,8 +45,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue' import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { classNames } from '../../utils/' import { classNames } from '../../utils'
import Card from '../layout/Card' import Card from '../layout/Card.vue'
import $ui from '#build/ui' import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({
@@ -69,6 +58,22 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false default: false
}, },
wrapperClass: {
type: String,
default: () => $ui.modal.wrapper
},
innerClass: {
type: String,
default: () => $ui.modal.inner
},
innerStyle: {
type: Object,
default: () => ({})
},
containerClass: {
type: String,
default: () => $ui.modal.container
},
baseClass: { baseClass: {
type: String, type: String,
default: () => $ui.modal.base default: () => $ui.modal.base
@@ -77,9 +82,17 @@ const props = defineProps({
type: String, type: String,
default: () => $ui.modal.background default: () => $ui.modal.background
}, },
overlayClass: { overlay: {
type: Boolean,
default: true
},
overlayBackgroundClass: {
type: String, type: String,
default: () => $ui.modal.overlay default: () => $ui.modal.overlay.background
},
overlayTransitionClass: {
type: Object,
default: () => $ui.modal.overlay.transition
}, },
shadowClass: { shadowClass: {
type: String, type: String,
@@ -96,6 +109,14 @@ const props = defineProps({
widthClass: { widthClass: {
type: String, type: String,
default: () => $ui.modal.width default: () => $ui.modal.width
},
transition: {
type: Boolean,
default: true
},
transitionClass: {
type: Object,
default: () => $ui.modal.transition
} }
}) })
@@ -121,6 +142,22 @@ const modalClass = computed(() => {
) )
}) })
const overlayTransition = computed(() => {
if (!props.transition) {
return {}
}
return props.overlayTransitionClass
})
const modalTransition = computed(() => {
if (!props.transition) {
return {}
}
return props.transitionClass
})
function close (value: boolean) { function close (value: boolean) {
isOpen.value = value isOpen.value = value
emit('close') emit('close')

View File

@@ -1,42 +1,37 @@
<template> <template>
<transition <transition appear v-bind="transitionClass">
appear
enter-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enter-active-class="transition duration-300 ease-out transform"
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-class="opacity-100"
leave-active-class="transition duration-100 ease-in"
leave-to-class="opacity-0"
>
<div <div
class="z-50 w-full bg-white rounded-lg shadow-lg pointer-events-auto dark:bg-gray-800" :class="['z-50 w-full pointer-events-auto', backgroundClass, roundedClass, shadowClass]"
@mouseover="onMouseover" @mouseover="onMouseover"
@mouseleave="onMouseleave" @mouseleave="onMouseleave"
> >
<div class="relative overflow-hidden rounded-lg ring-1 u-ring-gray-200"> <div :class="['relative overflow-hidden', roundedClass, ringClass]">
<div class="p-4"> <div class="p-4">
<div class="flex"> <div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }">
<div class="flex-shrink-0"> <div v-if="iconName" class="flex-shrink-0">
<Icon :name="iconName" class="w-6 h-6" :class="iconClass" /> <Icon :name="iconName" :class="iconClass" />
</div> </div>
<div class="ml-3 w-0 flex-1 pt-0.5"> <div class="w-0 flex-1">
<p class="text-sm font-medium leading-5 u-text-gray-900"> <p class="text-sm font-medium u-text-gray-900">
{{ title }} {{ title }}
</p> </p>
<p v-if="description" class="mt-1 text-sm leading-5 u-text-gray-500"> <p v-if="description" class="mt-1 text-sm leading-5 u-text-gray-500">
{{ description }} {{ description }}
</p> </p>
<Button
v-if="undo" <div v-if="description && actions.length" class="mt-3 flex items-center gap-6">
variant="white" <button v-for="(action, index) of actions" :key="index" type="button" class="text-sm font-medium focus:outline-none text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" @click.stop="onAction(action)">
size="xs" {{ action.label }}
class="mt-2" </button>
@click.stop="onUndo" </div>
>
Undo
</Button>
</div> </div>
<div class="flex-shrink-0 ml-4"> <div class="flex-shrink-0 flex items-center gap-3">
<div v-if="!description && actions.length" class="flex items-center gap-2">
<button v-for="(action, index) of actions" :key="index" type="button" class="text-sm font-medium focus:outline-none text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" @click.stop="onAction(action)">
{{ action.label }}
</button>
</div>
<button <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="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" @click.stop="onClose"
@@ -57,10 +52,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watchEffect } from 'vue' import { ref, computed, onMounted, onUnmounted, watchEffect } from 'vue'
import type { PropType } from 'vue'
import Icon from '../elements/Icon' import Icon from '../elements/Icon.vue'
import Button from '../elements/Button'
import { useTimer } from '../../composables/useTimer' import { useTimer } from '../../composables/useTimer'
import { classNames } from '../../utils'
import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({
id: { id: {
@@ -69,10 +65,9 @@ const props = defineProps({
}, },
type: { type: {
type: String, type: String,
required: true, default: null,
default: 'info',
validator (value: string) { validator (value: string) {
return ['info', 'success', 'error', 'warning'].includes(value) return Object.keys($ui.notification.type).includes(value)
} }
}, },
title: { title: {
@@ -83,17 +78,48 @@ const props = defineProps({
type: String, type: String,
default: null default: null
}, },
backgroundClass: {
type: String,
default: () => $ui.notification.background
},
shadowClass: {
type: String,
default: () => $ui.notification.shadow
},
ringClass: {
type: String,
default: () => $ui.notification.ring
},
roundedClass: {
type: String,
default: () => $ui.notification.rounded
},
transitionClass: {
type: Object,
default: () => $ui.notification.transition
},
customClass: {
type: String,
default: null
},
icon: { icon: {
type: String, type: String,
default: null default: null
}, },
iconBaseClass: {
type: String,
default: () => $ui.notification.icon.base
},
timeout: { timeout: {
type: Number, type: Number,
default: 5000 default: 5000
}, },
undo: { actions: {
type: Function, type: Array as PropType<{
default: null label: string,
click: Function
}[]>,
default: () => []
}, },
callback: { callback: {
type: Function, type: Function,
@@ -107,21 +133,14 @@ let timer: any = null
const remaining = ref(props.timeout) const remaining = ref(props.timeout)
const iconName = computed(() => { const iconName = computed(() => {
return props.icon || ({ return props.icon || $ui.notification.type[props.type]
warning: 'heroicons-outline:exclamation-circle',
info: 'heroicons-outline:information-circle',
success: 'heroicons-outline:check-circle',
error: 'heroicons-outline:x-circle'
})[props.type]
}) })
const iconClass = computed(() => { const iconClass = computed(() => {
return ({ return classNames(
warning: 'text-orange-400', props.iconBaseClass,
info: 'text-blue-400', $ui.notification.icon.color[props.type] || 'u-text-gray-400'
success: 'text-green-400', )
error: 'text-red-400'
})[props.type] || 'u-text-gray-400'
}) })
const progressBarStyle = computed(() => { const progressBarStyle = computed(() => {
@@ -153,13 +172,13 @@ function onClose () {
emit('close') emit('close')
} }
function onUndo () { function onAction (action) {
if (timer) { if (timer) {
timer.stop() timer.stop()
} }
if (props.undo) { if (action.click) {
props.undo() action.click()
} }
emit('close') emit('close')
@@ -180,7 +199,9 @@ onMounted(() => {
}) })
onUnmounted(() => { onUnmounted(() => {
timer.stop() if (timer) {
timer.stop()
}
}) })
</script> </script>

View File

@@ -2,8 +2,7 @@
<div class="fixed bottom-0 right-0 flex flex-col justify-end w-full z-[55] sm:w-96"> <div class="fixed bottom-0 right-0 flex flex-col justify-end w-full z-[55] sm:w-96">
<div v-if="notifications.length" class="px-4 py-6 space-y-3 overflow-y-auto sm:px-6"> <div v-if="notifications.length" class="px-4 py-6 space-y-3 overflow-y-auto sm:px-6">
<div <div
v-for="(notification, index) of notifications" v-for="notification of notifications"
v-show="index === notifications.length - 1"
:key="notification.id" :key="notification.id"
> >
<Notification <Notification
@@ -18,7 +17,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Notification from './Notification' import Notification from './Notification.vue'
import { useNuxtApp, useState } from '#imports' import { useNuxtApp, useState } from '#imports'
const { $toast } = useNuxtApp() const { $toast } = useNuxtApp()

View File

@@ -1,22 +1,14 @@
<template> <template>
<Popover v-slot="{ open, close }" :class="wrapperClass" @mouseleave="onMouseLeave"> <Popover v-slot="{ open, close }" :class="wrapperClass" @mouseleave="onMouseLeave">
<PopoverButton ref="trigger" as="div" @mouseover="onMouseOver"> <PopoverButton ref="trigger" as="div" class="inline-flex" @mouseover="onMouseOver">
<slot :open="open" :close="close"> <slot :open="open" :close="close">
<button>Open</button> <button>Open</button>
</slot> </slot>
</PopoverButton> </PopoverButton>
<div v-if="open" ref="container" :class="containerClass" @mouseover="onMouseOver"> <div v-if="open" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
<transition <transition appear v-bind="transitionClass">
appear <PopoverPanel :class="baseClass" static>
enter-active-class="transition ease-out duration-200"
enter-from-class="opacity-0 translate-y-1"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition ease-in duration-150"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 translate-y-1"
>
<PopoverPanel :class="panelClass" static>
<slot name="panel" :open="open" :close="close" /> <slot name="panel" :open="open" :close="close" />
</PopoverPanel> </PopoverPanel>
</transition> </transition>
@@ -25,21 +17,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Ref } from 'vue' import type { Ref, PropType } from 'vue'
import { ref, onMounted } from 'vue' import { computed, ref, onMounted } from 'vue'
import { defu } from 'defu'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue' import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
import { usePopper } from '../../composables/usePopper'
import { usePopper } from '../../utils' import type { PopperOptions } from '../../types'
import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({
placement: {
type: String,
default: 'bottom'
},
strategy: {
type: String,
default: 'fixed'
},
mode: { mode: {
type: String, type: String,
default: 'click', default: 'click',
@@ -49,42 +35,43 @@ const props = defineProps({
}, },
wrapperClass: { wrapperClass: {
type: String, type: String,
default: 'relative' default: () => $ui.popover.wrapper
}, },
containerClass: { containerClass: {
type: String, type: String,
default: 'z-10 py-2' default: () => $ui.popover.container
}, },
panelClass: { widthClass: {
type: String, type: String,
default: '' default: () => $ui.tooltip.width
},
baseClass: {
type: String,
default: () => $ui.popover.base
},
transitionClass: {
type: Object,
default: () => $ui.popover.transition
},
popperOptions: {
type: Object as PropType<PopperOptions>,
default: () => {}
},
openDelay: {
type: Number,
default: 50
},
closeDelay: {
type: Number,
default: 0
} }
}) })
const [trigger, container] = usePopper({ const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.popover.popperOptions))
placement: props.placement,
strategy: props.strategy,
modifiers: [{
name: 'offset',
options: {
offset: 0
}
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
},
{
name: 'preventOverflow',
options: {
padding: 8
}
}]
})
const [trigger, container] = usePopper(popperOptions.value)
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/popover/popover.ts#L151
const popoverApi: Ref<any> = ref(null) const popoverApi: Ref<any> = ref(null)
let openTimeout: NodeJS.Timeout | null = null let openTimeout: NodeJS.Timeout | null = null
@@ -93,21 +80,25 @@ let closeTimeout: NodeJS.Timeout | null = null
onMounted(() => { onMounted(() => {
setTimeout(() => { setTimeout(() => {
const popoverProvides = trigger.value?.$.provides const popoverProvides = trigger.value?.$.provides
if (!popoverProvides) {
return
}
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides) const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]] popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
// stop trigger click propagation on hover // stop trigger click propagation on hover
popoverApi.value.button.addEventListener('click', (e: Event) => { popoverApi.value?.button?.addEventListener('click', (e: Event) => {
if (props.mode === 'hover') { if (props.mode === 'hover') {
e.stopPropagation() e.stopPropagation()
} }
}, true) }, true)
}, 0) }, 200)
}) })
function onMouseOver () { function onMouseOver () {
if (props.mode !== 'hover' || !popoverApi.value) { if (props.mode !== 'hover' || !popoverApi.value) {
return return
} }
// cancel programmed closing // cancel programmed closing
if (closeTimeout) { if (closeTimeout) {
clearTimeout(closeTimeout) clearTimeout(closeTimeout)
@@ -120,13 +111,14 @@ function onMouseOver () {
openTimeout = openTimeout || setTimeout(() => { openTimeout = openTimeout || setTimeout(() => {
popoverApi.value.togglePopover && popoverApi.value.togglePopover() popoverApi.value.togglePopover && popoverApi.value.togglePopover()
openTimeout = null openTimeout = null
}, 50) }, props.openDelay)
} }
function onMouseLeave () { function onMouseLeave () {
if (props.mode !== 'hover' || !popoverApi.value) { if (props.mode !== 'hover' || !popoverApi.value) {
return return
} }
// cancel programmed opening // cancel programmed opening
if (openTimeout) { if (openTimeout) {
clearTimeout(openTimeout) clearTimeout(openTimeout)
@@ -139,7 +131,7 @@ function onMouseLeave () {
closeTimeout = closeTimeout || setTimeout(() => { closeTimeout = closeTimeout || setTimeout(() => {
popoverApi.value.closePopover && popoverApi.value.closePopover() popoverApi.value.closePopover && popoverApi.value.closePopover()
closeTimeout = null closeTimeout = null
}, 0) }, props.closeDelay)
} }
</script> </script>

View File

@@ -1,32 +1,19 @@
<template> <template>
<TransitionRoot as="template" :show="isOpen"> <TransitionRoot as="template" :appear="appear" :show="isOpen">
<Dialog <Dialog :class="[wrapperClass, { 'justify-end': side === 'right' }]" @close="isOpen = false">
class="fixed inset-0 flex z-40"
:class="{
'justify-end': side === 'right'
}"
@close="isOpen = false"
>
<TransitionChild <TransitionChild
v-if="overlay"
as="template" as="template"
enter="transition-opacity ease-linear duration-300" :appear="appear"
enter-from="opacity-0" v-bind="overlayTransition"
enter-to="opacity-100"
leave="transition-opacity ease-linear duration-300"
leave-from="opacity-100"
leave-to="opacity-0"
> >
<div class="fixed inset-0 transition-opacity" :class="overlayClass" /> <div class="fixed inset-0 transition-opacity" :class="overlayBackgroundClass" />
</TransitionChild> </TransitionChild>
<TransitionChild <TransitionChild
as="template" as="template"
enter="transition ease-in-out duration-300 transform" :appear="appear"
:enter-from="side === 'left' ? '-translate-x-full' : 'translate-x-full'" v-bind="slideoverTransition"
enter-to="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leave-from="translate-x-0"
:leave-to="side === 'left' ? '-translate-x-full' : 'translate-x-full'"
> >
<DialogPanel :class="slideoverClass"> <DialogPanel :class="slideoverClass">
<div v-if="$slots.header" :class="headerClass"> <div v-if="$slots.header" :class="headerClass">
@@ -43,7 +30,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import type { WritableComputedRef, PropType } from 'vue' import type { WritableComputedRef, PropType } from 'vue'
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue' import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { classNames } from '../../utils/' import { classNames } from '../../utils'
import $ui from '#build/ui' import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({
@@ -51,11 +38,19 @@ const props = defineProps({
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: false default: false
}, },
appear: {
type: Boolean,
default: false
},
side: { side: {
type: String, type: String,
default: 'left', default: 'left',
validator: (value: string) => ['left', 'right'].includes(value) validator: (value: string) => ['left', 'right'].includes(value)
}, },
wrapperClass: {
type: String,
default: () => $ui.slideover.wrapper
},
baseClass: { baseClass: {
type: String, type: String,
default: () => $ui.slideover.base default: () => $ui.slideover.base
@@ -64,9 +59,17 @@ const props = defineProps({
type: String, type: String,
default: () => $ui.slideover.background default: () => $ui.slideover.background
}, },
overlayClass: { overlay: {
type: Boolean,
default: true
},
overlayBackgroundClass: {
type: String, type: String,
default: () => $ui.slideover.overlay default: () => $ui.slideover.overlay.background
},
overlayTransitionClass: {
type: Object,
default: () => $ui.slideover.overlay.transition
}, },
widthClass: { widthClass: {
type: String, type: String,
@@ -75,8 +78,17 @@ const props = defineProps({
headerClass: { headerClass: {
type: String, type: String,
default: () => $ui.slideover.header default: () => $ui.slideover.header
},
transition: {
type: Boolean,
default: true
},
transitionClass: {
type: Object,
default: () => $ui.slideover.transition
} }
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const isOpen: WritableComputedRef<boolean> = computed({ const isOpen: WritableComputedRef<boolean> = computed({
@@ -95,6 +107,28 @@ const slideoverClass = computed(() => {
props.backgroundClass props.backgroundClass
) )
}) })
const overlayTransition = computed(() => {
if (!props.transition) {
return {}
}
return props.overlayTransitionClass
})
const slideoverTransition = computed(() => {
if (!props.transition) {
return {}
}
return {
enterFrom: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
enterTo: 'translate-x-0',
leaveFrom: 'translate-x-0',
leaveTo: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
...props.transitionClass
}
})
</script> </script>
<script lang="ts"> <script lang="ts">

View File

@@ -1,23 +1,22 @@
<template> <template>
<div ref="trigger" :class="wrapperClass" @mouseover="open = true" @mouseleave="open = false"> <div ref="trigger" :class="wrapperClass" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
<slot :open="open"> <slot :open="open">
Hover me Hover me
</slot> </slot>
<div v-if="open" ref="container" :class="containerClass"> <div v-if="open" ref="container" :class="[containerClass, widthClass]">
<transition <transition appear v-bind="transitionClass">
appear <div :class="[baseClass, backgroundClass, roundedClass, shadowClass, ringClass]">
enter-active-class="transition ease-out duration-200"
enter-from-class="opacity-0 translate-y-1"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition ease-in duration-150"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 translate-y-1"
>
<div :class="tooltipClass">
<slot name="text"> <slot name="text">
{{ text }} <span class="truncate">{{ text }}</span>
</slot> </slot>
<span v-if="shortcuts?.length" class="inline-flex items-center justify-end flex-shrink-0 gap-0.5 ml-1">
<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 }}
</kbd>
</span>
</div> </div>
</transition> </transition>
</div> </div>
@@ -25,66 +24,114 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import type { PropType } from 'vue'
import { usePopper } from '../../utils' import { computed, ref } from 'vue'
import { defu } from 'defu'
import { usePopper } from '../../composables/usePopper'
import type { PopperOptions } from '../../types'
import $ui from '#build/ui'
const props = defineProps({ const props = defineProps({
text: { text: {
type: String, type: String,
default: null default: null
}, },
placement: { shortcuts: {
type: String, type: Array as PropType<string[]>,
default: 'bottom', default: () => []
validator: (value: string) => {
return ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'].includes(value)
}
},
strategy: {
type: String,
default: 'fixed',
validator: (value: string) => {
return ['absolute', 'fixed'].includes(value)
}
}, },
wrapperClass: { wrapperClass: {
type: String, type: String,
default: 'relative inline-flex' default: () => $ui.tooltip.wrapper
}, },
containerClass: { containerClass: {
type: String, type: String,
default: 'z-10 py-2' default: () => $ui.tooltip.container
}, },
tooltipClass: { widthClass: {
type: String, type: String,
default: 'flex items-center justify-center invisible w-auto h-6 max-w-xs px-2 space-x-1 truncate rounded shadow lg:visible u-bg-gray-800 truncate u-text-gray-50 text-xs' default: () => $ui.tooltip.width
},
backgroundClass: {
type: String,
default: () => $ui.tooltip.background
},
shadowClass: {
type: String,
default: () => $ui.tooltip.shadow
},
ringClass: {
type: String,
default: () => $ui.tooltip.ring
},
roundedClass: {
type: String,
default: () => $ui.tooltip.rounded
},
baseClass: {
type: String,
default: () => $ui.tooltip.base
},
transitionClass: {
type: Object,
default: () => $ui.tooltip.transition
},
popperOptions: {
type: Object as PropType<PopperOptions>,
default: () => {}
},
openDelay: {
type: Number,
default: 0
},
closeDelay: {
type: Number,
default: 0
} }
}) })
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.tooltip.popperOptions))
const [trigger, container] = usePopper(popperOptions.value)
const open = ref(false) const open = ref(false)
const [trigger, container] = usePopper({
placement: props.placement, let openTimeout: NodeJS.Timeout | null = null
strategy: props.strategy, let closeTimeout: NodeJS.Timeout | null = null
modifiers: [{
name: 'offset', // Methods
options: {
offset: 0 function onMouseOver () {
} // cancel programmed closing
}, if (closeTimeout) {
{ clearTimeout(closeTimeout)
name: 'computeStyles', closeTimeout = null
options: { }
gpuAcceleration: false, // dropdown already open
adaptive: false if (open.value) {
} return
}, }
{ openTimeout = openTimeout || setTimeout(() => {
name: 'preventOverflow', open.value = true
options: { openTimeout = null
padding: 8 }, props.openDelay)
} }
}]
}) function onMouseLeave () {
// cancel programmed opening
if (openTimeout) {
clearTimeout(openTimeout)
openTimeout = null
}
// dropdown already closed
if (!open.value) {
return
}
closeTimeout = closeTimeout || setTimeout(() => {
open.value = false
closeTimeout = null
}, props.closeDelay)
}
</script> </script>
<script lang="ts"> <script lang="ts">

View File

@@ -0,0 +1,80 @@
import { ref, onMounted, watchEffect } from 'vue'
import type { Ref } from 'vue'
import { popperGenerator, defaultModifiers } from '@popperjs/core/lib/popper-lite'
import type { Instance } from '@popperjs/core'
import { omitBy, isUndefined } from 'lodash-es'
import flip from '@popperjs/core/lib/modifiers/flip'
import offset from '@popperjs/core/lib/modifiers/offset'
import preventOverflow from '@popperjs/core/lib/modifiers/preventOverflow'
import computeStyles from '@popperjs/core/lib/modifiers/computeStyles'
import eventListeners from '@popperjs/core/lib/modifiers/eventListeners'
import type { PopperOptions } from '../types'
export const createPopper = popperGenerator({
defaultModifiers: [...defaultModifiers, offset, flip, preventOverflow, computeStyles, eventListeners]
})
export function usePopper ({
locked = false,
overflowPadding = 8,
offsetDistance = 8,
offsetSkid = 0,
gpuAcceleration = true,
adaptive = true,
scroll = true,
resize = true,
placement,
strategy
}: PopperOptions, virtualReference: Ref<Object> = null) {
const reference: Ref<HTMLElement> = ref(null)
const popper: Ref<HTMLElement> = ref(null)
const instance: Ref<Instance> = ref(null)
onMounted(() => {
watchEffect((onInvalidate) => {
if (!popper.value) { return }
if (!reference.value && !virtualReference.value) { return }
const popperEl = popper.value.$el || popper.value
const referenceEl = virtualReference?.value || reference.value.$el || reference.value
// if (!(referenceEl instanceof HTMLElement)) { return }
if (!(popperEl instanceof HTMLElement)) { return }
instance.value = createPopper(referenceEl, popperEl, omitBy({
placement,
strategy,
modifiers: [{
name: 'flip',
enabled: !locked
}, {
name: 'preventOverflow',
options: {
padding: overflowPadding
}
}, {
name: 'offset',
options: {
offset: [offsetSkid, offsetDistance]
}
}, {
name: 'computeStyles',
options: {
adaptive,
gpuAcceleration
}
}, {
name: 'eventListeners',
options: {
scroll,
resize
}
}]
}, isUndefined))
onInvalidate(instance.value.destroy)
})
})
return [reference, popper, instance]
}

View File

@@ -1,6 +1,5 @@
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import { defineNuxtPlugin } from '#app' import { defineNuxtPlugin } from '#app'
import { ClipboardPlugin } from '../types'
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
const { copy: copyToClipboard, isSupported } = useClipboard() const { copy: copyToClipboard, isSupported } = useClipboard()
@@ -32,9 +31,3 @@ export default defineNuxtPlugin((nuxtApp) => {
} }
} }
}) })
declare module '#app' {
interface NuxtApp {
$clipboard: ClipboardPlugin
}
}

View File

@@ -1,6 +1,6 @@
import { Ref } from 'vue' import type { Ref } from 'vue'
import { defineNuxtPlugin, useState } from '#app' import { defineNuxtPlugin, useState } from '#app'
import { ToastNotification, ToastPlugin } from '../types' import type { ToastNotification } from '../types'
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const notifications: Ref<ToastNotification[]> = useState('notifications', () => []) const notifications: Ref<ToastNotification[]> = useState('notifications', () => [])
@@ -28,45 +28,19 @@ export default defineNuxtPlugin(() => {
toast: { toast: {
addNotification, addNotification,
removeNotification, removeNotification,
success ({ title, description, timeout }: { title?: string, description?: string, timeout?: number } = {}) { success (notification: Partial<ToastNotification> = {}) {
addNotification({ return addNotification({ type: 'success', ...notification })
type: 'success',
title,
description,
timeout
})
}, },
info ({ title, description, timeout }: { title?: string, description?: string, timeout?: number } = {}) { info (notification: Partial<ToastNotification> = {}) {
addNotification({ return addNotification({ type: 'info', ...notification })
type: 'info',
title,
description,
timeout
})
}, },
warning ({ title, description, timeout }: { title?: string, description?: string, timeout?: number } = {}) { warning (notification: Partial<ToastNotification> = {}) {
addNotification({ return addNotification({ type: 'warning', ...notification })
type: 'warning',
title,
description,
timeout
})
}, },
error ({ title = 'An error occurred!', description, timeout }: { title?: string, description?: string, timeout?: number } = {}) { error (notification: Partial<ToastNotification>) {
addNotification({ return addNotification({ type: 'error', title: 'An error occurred!', ...notification })
type: 'error',
title,
description,
timeout
})
} }
} }
} }
} }
}) })
declare module '#app' {
interface NuxtApp {
$toast: ToastPlugin
}
}

View File

@@ -190,8 +190,13 @@ export default (variantColors: string[]) => {
...select, ...select,
wrapper: 'relative', wrapper: 'relative',
base: `${select.base} text-left cursor-default`, base: `${select.base} text-left cursor-default`,
icon: {
name: 'heroicons-solid:selector',
...select.icon
},
list: { list: {
container: 'z-10 w-full py-1', container: 'z-20',
width: 'w-full',
base: 'u-bg-white shadow-lg rounded-md ring-1 u-ring-gray-200 focus:outline-none overflow-y-auto py-1 max-h-60', base: 'u-bg-white shadow-lg rounded-md ring-1 u-ring-gray-200 focus:outline-none overflow-y-auto py-1 max-h-60',
input: 'relative block w-full focus:ring-transparent text-sm px-4 py-2 u-text-gray-700 border-l-0 u-bg-white border-t-0 border-r-0 u-border-gray-200 focus:u-border-gray-200', input: 'relative block w-full focus:ring-transparent text-sm px-4 py-2 u-text-gray-700 border-l-0 u-bg-white border-t-0 border-r-0 u-border-gray-200 focus:u-border-gray-200',
option: { option: {
@@ -204,18 +209,27 @@ export default (variantColors: string[]) => {
disabled: 'cursor-not-allowed opacity-50', disabled: 'cursor-not-allowed opacity-50',
empty: 'text-sm u-text-gray-400 px-4 py-2', empty: 'text-sm u-text-gray-400 px-4 py-2',
icon: { icon: {
name: 'heroicons-solid:check',
base: 'absolute inset-y-0 right-0 flex items-center pr-4', base: 'absolute inset-y-0 right-0 flex items-center pr-4',
active: 'text-white', active: 'text-white',
inactive: 'text-primary-600', inactive: 'text-primary-600',
size: 'h-5 w-5' size: 'h-5 w-5'
} }
},
transition: {
leaveActiveClass: 'transition ease-in duration-100',
leaveFromClass: 'opacity-100',
leaveToClass: 'opacity-0'
} }
},
popperOptions: {
placement: 'bottom-end'
} }
} }
const radio = { const radio = {
wrapper: 'relative flex items-start', wrapper: 'relative flex items-start',
base: 'h-4 w-4 text-primary-600 focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 focus:ring-offset-white dark:focus:ring-offset-black u-border-gray-300 dark:checked:border-primary-600 disabled:opacity-50 disabled:cursor-not-allowed', base: 'h-4 w-4 text-primary-600 focus:ring-2 focus:ring-offset-2 u-bg-white dark:checked:bg-current dark:checked:border-transparent focus:ring-primary-500 focus:ring-offset-white dark:focus:ring-offset-black u-border-gray-300 disabled:opacity-50 disabled:cursor-not-allowed',
label: 'font-medium u-text-gray-700', label: 'font-medium u-text-gray-700',
required: 'text-red-400', required: 'text-red-400',
help: 'u-text-gray-500' help: 'u-text-gray-500'
@@ -239,14 +253,35 @@ export default (variantColors: string[]) => {
} }
const modal = { 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 p-4 sm:p-0 text-center',
base: 'relative inline-block align-bottom text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle w-full', base: 'relative inline-block align-bottom text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle w-full',
background: 'u-bg-white', background: 'u-bg-white',
overlay: 'bg-gray-500/75 dark:bg-gray-600/75', overlay: {
background: 'bg-gray-500/75 dark:bg-gray-600/75',
transition: {
enter: 'ease-out duration-300',
enterFrom: 'opacity-0',
enterTo: 'opacity-100',
leave: 'ease-in duration-200',
leaveFrom: 'opacity-100',
leaveTo: 'opacity-0'
}
},
border: '', border: '',
ring: '', ring: '',
rounded: 'rounded-lg', rounded: 'rounded-lg',
shadow: 'shadow-xl', shadow: 'shadow-xl',
width: 'sm:max-w-lg' width: 'sm:max-w-lg',
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 container = { const container = {
@@ -305,15 +340,30 @@ export default (variantColors: string[]) => {
const dropdown = { const dropdown = {
wrapper: 'relative inline-flex text-left', wrapper: 'relative inline-flex text-left',
container: 'w-48 z-20 py-2', container: 'z-20',
base: 'u-bg-white divide-y u-divide-gray-100 rounded-md ring-1 u-ring-gray-200 shadow-lg', 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',
group: 'py-1',
item: { item: {
base: 'group flex items-center gap-3 px-4 py-2 text-sm w-full', base: 'group flex items-center gap-3 px-4 py-2 text-sm w-full',
active: 'u-bg-gray-100 u-text-gray-900', active: 'u-bg-gray-100 u-text-gray-900',
inactive: 'u-text-gray-700', inactive: 'u-text-gray-700',
disabled: 'cursor-not-allowed opacity-50', disabled: 'cursor-not-allowed opacity-50',
icon: 'h-5 w-5 u-text-gray-400 group-hover:u-text-gray-500 flex-shrink-0', 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' 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'
},
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'
},
popperOptions: {
placement: 'bottom-end',
strategy: 'fixed'
} }
} }
@@ -377,15 +427,119 @@ export default (variantColors: string[]) => {
const avatarGroup = { const avatarGroup = {
ring: 'ring-2 u-ring-white', ring: 'ring-2 u-ring-white',
margin: '-ml-1.5 first:ml-0' margin: '-mr-1.5 first:mr-0'
} }
const slideover = { const slideover = {
overlay: 'bg-gray-500/75 dark:bg-gray-600/75', wrapper: 'fixed inset-0 flex z-40',
overlay: {
background: 'bg-gray-500/75 dark:bg-gray-600/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', base: 'relative flex-1 flex flex-col w-full focus:outline-none',
background: 'u-bg-white', background: 'u-bg-white',
width: 'max-w-md', width: 'max-w-md',
header: 'flex items-center justify-between flex-shrink-0 px-4 sm:px-6 h-16 border-b u-border-gray-200' header: 'flex items-center justify-between flex-shrink-0 px-4 sm:px-6 h-16 border-b u-border-gray-200',
transition: {
enter: 'transform transition ease-in-out duration-500 sm:duration-700',
leave: 'transform transition ease-in-out duration-500 sm:duration-700'
}
}
const notification = {
background: 'u-bg-white',
shadow: 'shadow-lg',
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'
},
icon: {
base: 'w-6 h-6',
color: {
warning: 'text-orange-400',
info: 'text-blue-400',
success: 'text-green-400',
error: 'text-red-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'
}
}
const tooltip = {
wrapper: 'relative inline-flex',
container: 'z-20',
width: 'max-w-xs',
background: 'u-bg-white',
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',
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'
},
popperOptions: {
strategy: 'fixed'
}
}
const popover = {
wrapper: 'relative',
container: 'z-20',
width: '',
base: '',
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'
},
popperOptions: {
strategy: 'fixed'
}
}
const contextMenu = {
wrapper: 'relative',
container: 'z-20',
width: '',
base: '',
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'
},
popperOptions: {
placement: 'bottom-start',
scroll: false
}
} }
return { return {
@@ -409,6 +563,10 @@ export default (variantColors: string[]) => {
pills, pills,
avatar, avatar,
avatarGroup, avatarGroup,
slideover slideover,
notification,
tooltip,
popover,
contextMenu
} }
} }

View File

@@ -6,6 +6,10 @@
color-scheme: dark; color-scheme: dark;
} }
a:focus {
@apply outline-primary-500;
}
@layer utilities { @layer utilities {
.u-bg-white { @apply bg-white dark:bg-black; } .u-bg-white { @apply bg-white dark:bg-black; }
.u-bg-gray-50 { @apply bg-gray-50 dark:bg-gray-900; } .u-bg-gray-50 { @apply bg-gray-50 dark:bg-gray-900; }

10
src/runtime/types/avatar.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
export interface Avatar {
src: string
alt: string
text: string
size: string
rounded: boolean
chip: boolean
chipVariant: string
chipPosition: string
}

View File

@@ -1,20 +1,25 @@
import type { UseFuseOptions } from '@vueuse/integrations/useFuse' import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import type { FuseSortFunctionMatch, FuseSortFunctionMatchList } from 'fuse.js'
import type { Avatar } from './avatar'
export interface Command { export interface Command {
id: string | number
prefix?: string prefix?: string
suffix?: string suffix?: string
label: string
icon?: string icon?: string
iconClass?: string iconClass?: string
avatar?: string avatar?: Partial<Avatar>
chip?: string chip?: string
disabled?: boolean disabled?: boolean
shortcuts?: string[] shortcuts?: string[]
group?: string
score?: number
matches?: (FuseSortFunctionMatch | FuseSortFunctionMatchList)[]
} }
export interface Group { export interface Group {
key: string key: string
label: string active?: string
inactive?: string
commands: Command[] commands: Command[]
options?: Partial<UseFuseOptions<Command>> options?: Partial<UseFuseOptions<Command>>
} }

View File

@@ -1,2 +1,5 @@
export * from './avatar'
export * from './clipboard' export * from './clipboard'
export * from './command-palette'
export * from './popper'
export * from './toast' export * from './toast'

14
src/runtime/types/popper.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
import type { Placement, PositioningStrategy } from '@popperjs/core'
export interface PopperOptions {
locked?: boolean
overflowPadding?: number
offsetDistance?: number
offsetSkid?: number
placement?: Placement
strategy?: PositioningStrategy
gpuAcceleration?: boolean
adaptive?: boolean
scroll?: boolean
resize?: boolean
}

View File

@@ -5,7 +5,10 @@ export interface ToastNotification {
type: string type: string
icon?: string icon?: string
timeout: number timeout: number
undo?: Function actions?: {
label: string,
click: Function
}[]
callback?: Function callback?: Function
} }

View File

@@ -1,5 +1,3 @@
export * from './popper'
export function classNames (...classes: any[string]) { export function classNames (...classes: any[string]) {
return classes.filter(Boolean).join(' ') return classes.filter(Boolean).join(' ')
} }

View File

@@ -1,26 +0,0 @@
import { ref, onMounted, watchEffect } from 'vue'
import { createPopper } from '@popperjs/core'
export function usePopper (options: object) {
const reference = ref(null)
const popper = ref(null)
onMounted(() => {
watchEffect((onInvalidate) => {
if (!popper.value) { return }
if (!reference.value) { return }
const popperEl = popper.value.$el || popper.value
const referenceEl = reference.value.$el || reference.value
if (!(referenceEl instanceof HTMLElement)) { return }
if (!(popperEl instanceof HTMLElement)) { return }
const { destroy } = createPopper(referenceEl, popperEl, options)
onInvalidate(destroy)
})
})
return [reference, popper]
}

3504
yarn.lock

File diff suppressed because it is too large Load Diff