mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-20 23:11:43 +01:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d84dfd05b | ||
|
|
7dc59a05ec | ||
|
|
4bd994985d | ||
|
|
c83d3b7147 | ||
|
|
f022665351 | ||
|
|
f29c325dc7 | ||
|
|
876f9578c2 | ||
|
|
f69f584188 | ||
|
|
377b4189ca | ||
|
|
f76a9f0ab0 | ||
|
|
37b2271bf0 | ||
|
|
fa49d52f17 | ||
|
|
fd4b608150 | ||
|
|
fef93f3198 | ||
|
|
0826ef8d59 | ||
|
|
0e3066d865 | ||
|
|
fb9d6cb544 | ||
|
|
531a89cdb8 | ||
|
|
6970c2d665 | ||
|
|
9719ea3858 | ||
|
|
a4af6b3805 | ||
|
|
3493c138d9 | ||
|
|
d08e64d53f | ||
|
|
6aecb082d2 | ||
|
|
63e27f8b4b | ||
|
|
7970aefcb0 | ||
|
|
a893d7fa2e | ||
|
|
8ace629ff8 | ||
|
|
0d35b82ecb | ||
|
|
5f37077835 | ||
|
|
948f4b89b1 | ||
|
|
e6d0dd5898 | ||
|
|
4702a4f103 | ||
|
|
efa9674815 | ||
|
|
97d40395d3 | ||
|
|
a2fb22d835 | ||
|
|
d14a1a82c2 | ||
|
|
a566627a23 | ||
|
|
ee3352278c |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -41,8 +41,8 @@ jobs:
|
|||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
|
|
||||||
# - name: Typecheck
|
- name: Typecheck
|
||||||
# run: yarn typecheck
|
run: yarn typecheck
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build
|
run: yarn build
|
||||||
|
|||||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -2,6 +2,60 @@
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
### [1.2.4](https://github.com/nuxtlabs/ui/compare/v1.2.3...v1.2.4) (2023-04-04)
|
||||||
|
|
||||||
|
### [1.2.3](https://github.com/nuxtlabs/ui/compare/v1.2.2...v1.2.3) (2023-03-22)
|
||||||
|
|
||||||
|
### [1.2.2](https://github.com/nuxtlabs/ui/compare/v1.2.1...v1.2.2) (2023-03-20)
|
||||||
|
|
||||||
|
### [1.2.1](https://github.com/nuxtlabs/ui/compare/v1.2.0...v1.2.1) (2023-03-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **defineShortcuts:** shift + alphabetic character handling ([#140](https://github.com/nuxtlabs/ui/issues/140)) ([377b418](https://github.com/nuxtlabs/ui/commit/377b4189ca85603db0b7f040949260ba7494c46f))
|
||||||
|
|
||||||
|
## [1.2.0](https://github.com/nuxtlabs/ui/compare/v1.1.4...v1.2.0) (2023-03-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **defineShortcuts:** add missing import ([37b2271](https://github.com/nuxtlabs/ui/commit/37b2271bf04adfe6bee4d984fa12452b2168318c))
|
||||||
|
* **Tooltip:** `shortcutsClass` prop type ([fa49d52](https://github.com/nuxtlabs/ui/commit/fa49d52f17752eaa06f997a9b6e8df8adcab983f))
|
||||||
|
|
||||||
|
### [1.1.4](https://github.com/nuxtlabs/ui/compare/v1.1.3...v1.1.4) (2023-03-02)
|
||||||
|
|
||||||
|
### [1.1.3](https://github.com/nuxtlabs/ui/compare/v1.1.2...v1.1.3) (2023-03-02)
|
||||||
|
|
||||||
|
### [1.1.2](https://github.com/nuxtlabs/ui/compare/v1.1.1...v1.1.2) (2023-02-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Tooltip:** truncate ([d08e64d](https://github.com/nuxtlabs/ui/commit/d08e64d53fa439f34d51909bcb6812f1bcd95d83))
|
||||||
|
* **VerticalNavigation:** links `to` type ([7970aef](https://github.com/nuxtlabs/ui/commit/7970aefcb032ce01fcb11e9285fa61ce87f59519))
|
||||||
|
|
||||||
|
### [1.1.1](https://github.com/nuxtlabs/ui/compare/v1.1.0...v1.1.1) (2023-02-20)
|
||||||
|
|
||||||
|
## [1.1.0](https://github.com/nuxtlabs/ui/compare/v1.0.0...v1.1.0) (2023-02-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **CommandPalette:** handle async search for specific groups ([efa9674](https://github.com/nuxtlabs/ui/commit/efa9674815ab4de756079690da0a381c3703d564))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **CommandPalette:** types ([4702a4f](https://github.com/nuxtlabs/ui/commit/4702a4f10379201c167cc52099519778756a5780))
|
||||||
|
|
||||||
|
## [1.0.0](https://github.com/nuxtlabs/ui/compare/v0.2.1...v1.0.0) (2023-02-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* migrate to `@egoist/tailwindcss-icons` ([ee33522](https://github.com/nuxtlabs/ui/commit/ee3352278cf03fdd12f2a4663b403052de3f089a))
|
||||||
|
|
||||||
### [0.2.1](https://github.com/nuxtlabs/ui/compare/v0.2.0...v0.2.1) (2023-02-16)
|
### [0.2.1](https://github.com/nuxtlabs/ui/compare/v0.2.0...v0.2.1) (2023-02-16)
|
||||||
|
|
||||||
## [0.2.0](https://github.com/nuxtlabs/ui/compare/v0.1.39...v0.2.0) (2023-02-16)
|
## [0.2.0](https://github.com/nuxtlabs/ui/compare/v0.1.39...v0.2.0) (2023-02-16)
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center">
|
||||||
<ColorScheme placeholder="" tag="span">
|
<ColorScheme placeholder="" tag="span">
|
||||||
<UButton variant="transparent" :icon="colorMode.value === 'dark' ? 'heroicons-outline:moon' : 'heroicons-outline:sun'" @click="toggleDark" />
|
<UButton variant="transparent" :icon="colorMode.value === 'dark' ? 'i-heroicons-moon' : 'i-heroicons-sun'" @click="toggleDark" />
|
||||||
</ColorScheme>
|
</ColorScheme>
|
||||||
<UButton to="https://github.com/nuxtlabs/ui" target="_blank" variant="transparent" icon="fa-brands:github" />
|
<UButton to="https://github.com/nuxtlabs/ui" target="_blank" variant="transparent" icon="i-mdi-github" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UContainer>
|
</UContainer>
|
||||||
@@ -84,7 +84,7 @@ const sections = [
|
|||||||
{ label: 'Forms', links: [{ label: 'Checkbox', to: '/components/Checkbox' }, { label: 'Input', to: '/components/Input' }, { label: 'FormGroup', to: '/components/FormGroup' }, { label: 'Radio', to: '/components/Radio' }, { label: 'Select', to: '/components/Select' }, { label: 'SelectCustom', to: '/components/SelectCustom' }, { label: 'Textarea', to: '/components/Textarea' }, { label: 'Toggle', to: '/components/Toggle' }] },
|
{ label: 'Forms', links: [{ label: 'Checkbox', to: '/components/Checkbox' }, { label: 'Input', to: '/components/Input' }, { label: 'FormGroup', to: '/components/FormGroup' }, { label: 'Radio', to: '/components/Radio' }, { label: 'Select', to: '/components/Select' }, { label: 'SelectCustom', to: '/components/SelectCustom' }, { label: 'Textarea', to: '/components/Textarea' }, { label: 'Toggle', to: '/components/Toggle' }] },
|
||||||
{ label: 'Layout', links: [{ label: 'Card', to: '/components/Card' }, { label: 'Container', to: '/components/Container' }] },
|
{ label: 'Layout', links: [{ label: 'Card', to: '/components/Card' }, { label: 'Container', to: '/components/Container' }] },
|
||||||
{ label: 'Navigation', links: [{ label: 'Pills', to: '/components/Pills' }, { label: 'Tabs', to: '/components/Tabs' }, { label: 'VerticalNavigation', to: '/components/VerticalNavigation' }, { label: 'CommandPalette', to: '/components/CommandPalette' }] },
|
{ label: 'Navigation', links: [{ label: 'Pills', to: '/components/Pills' }, { label: 'Tabs', to: '/components/Tabs' }, { label: 'VerticalNavigation', to: '/components/VerticalNavigation' }, { label: 'CommandPalette', to: '/components/CommandPalette' }] },
|
||||||
{ label: 'Overlays', links: [{ label: 'Modal', to: '/components/Modal' }, { label: 'Notification', to: '/components/Notification' }, { label: 'Popover', to: '/components/Popover' }, { label: 'Slideover', to: '/components/Slideover' }, { label: 'Tooltip', to: '/components/Tooltip' }] }
|
{ label: 'Overlays', links: [{ label: 'ContextMenu', to: '/components/ContextMenu' }, { label: 'Modal', to: '/components/Modal' }, { label: 'Notification', to: '/components/Notification' }, { label: 'Popover', to: '/components/Popover' }, { label: 'Slideover', to: '/components/Slideover' }, { label: 'Tooltip', to: '/components/Tooltip' }] }
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ 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({
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: 'en'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
modules: [
|
modules: [
|
||||||
|
// @ts-ignore
|
||||||
nuxtUI
|
nuxtUI
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
@@ -16,6 +24,7 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
preset: {
|
preset: {
|
||||||
},
|
},
|
||||||
|
icons: ['heroicons', 'mdi'],
|
||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
<UButton
|
<UButton
|
||||||
class="absolute top-0 right-0"
|
class="absolute top-0 right-0"
|
||||||
:icon="copied ? 'heroicons-outline:clipboard-check' : 'heroicons-outline:clipboard-copy'"
|
:icon="copied ? 'i-heroicons-clipboard-document-check' : 'i-heroicons-clipboard-document'"
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
size="sm"
|
size="sm"
|
||||||
:custom-class="copied ? '!text-green-500' : ''"
|
:custom-class="copied ? '!text-green-500' : ''"
|
||||||
@@ -56,7 +56,6 @@
|
|||||||
v-if="prop.type === 'Boolean'"
|
v-if="prop.type === 'Boolean'"
|
||||||
v-model="prop.value"
|
v-model="prop.value"
|
||||||
:name="prop.key"
|
:name="prop.key"
|
||||||
:label="prop.key"
|
|
||||||
/>
|
/>
|
||||||
<USelect
|
<USelect
|
||||||
v-else-if="prop.values"
|
v-else-if="prop.values"
|
||||||
@@ -121,6 +120,31 @@ const toggle = ref(false)
|
|||||||
const modal = ref(false)
|
const modal = ref(false)
|
||||||
const slideover = ref(false)
|
const slideover = ref(false)
|
||||||
|
|
||||||
|
const x = ref(0)
|
||||||
|
const y = ref(0)
|
||||||
|
const isContextMenuOpen = ref(false)
|
||||||
|
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('mousemove', ({ clientX, clientY }) => {
|
||||||
|
x.value = clientX
|
||||||
|
y.value = clientY
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function openContextMenu () {
|
||||||
|
const top = unref(y)
|
||||||
|
const left = unref(x)
|
||||||
|
|
||||||
|
virtualElement.value.getBoundingClientRect = () => ({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
top,
|
||||||
|
left
|
||||||
|
})
|
||||||
|
isContextMenuOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
Button: {
|
Button: {
|
||||||
label: 'Button text'
|
label: 'Button text'
|
||||||
@@ -132,7 +156,9 @@ const defaultProps = {
|
|||||||
title: 'A new software update is available. See what’s new in version 2.0.4.'
|
title: 'A new software update is available. See what’s new in version 2.0.4.'
|
||||||
},
|
},
|
||||||
AlertDialog: {
|
AlertDialog: {
|
||||||
title: 'Are you sure you want to close this modal?',
|
icon: 'i-heroicons-exclamation-triangle',
|
||||||
|
title: 'Deactivate account',
|
||||||
|
description: 'Are you sure you want to deactivate your account? All of your data will be permanently removed from our servers forever. This action cannot be undone.',
|
||||||
modelValue: alertDialog,
|
modelValue: alertDialog,
|
||||||
'onUpdate:modelValue': (v) => { alertDialog.value = v },
|
'onUpdate:modelValue': (v) => { alertDialog.value = v },
|
||||||
component: {
|
component: {
|
||||||
@@ -154,21 +180,21 @@ const defaultProps = {
|
|||||||
items: [
|
items: [
|
||||||
[{
|
[{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: 'heroicons-solid:pencil'
|
icon: 'i-heroicons-pencil-square-20-solid'
|
||||||
}, {
|
}, {
|
||||||
label: 'Duplicate',
|
label: 'Duplicate',
|
||||||
icon: 'heroicons-solid:duplicate'
|
icon: 'i-heroicons-document-duplicate-20-solid'
|
||||||
}],
|
}],
|
||||||
[{
|
[{
|
||||||
label: 'Archive',
|
label: 'Archive',
|
||||||
icon: 'heroicons-solid:archive'
|
icon: 'i-heroicons-archive-box-20-solid'
|
||||||
}, {
|
}, {
|
||||||
label: 'Move',
|
label: 'Move',
|
||||||
icon: 'heroicons-solid:external-link'
|
icon: 'i-heroicons-arrow-right-circle-20-solid'
|
||||||
}],
|
}],
|
||||||
[{
|
[{
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
icon: 'heroicons-solid:trash'
|
icon: 'i-heroicons-trash-20-solid'
|
||||||
}]
|
}]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -176,22 +202,22 @@ const defaultProps = {
|
|||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
label: 'Home',
|
label: 'Home',
|
||||||
icon: 'heroicons-outline:home',
|
icon: 'i-heroicons-home',
|
||||||
to: '/'
|
to: '/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Examples',
|
label: 'Examples',
|
||||||
icon: 'heroicons-outline:book-open',
|
icon: 'i-heroicons-book-open',
|
||||||
to: '/examples'
|
to: '/examples'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Migration',
|
label: 'Migration',
|
||||||
icon: 'heroicons-outline:refresh',
|
icon: 'i-heroicons-arrow-path',
|
||||||
to: '/migration'
|
to: '/migration'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'External link',
|
label: 'External link',
|
||||||
icon: 'heroicons-outline:external-link',
|
icon: 'i-heroicons-link',
|
||||||
to: 'https://google.fr',
|
to: 'https://google.fr',
|
||||||
target: '_blank'
|
target: '_blank'
|
||||||
}
|
}
|
||||||
@@ -207,7 +233,7 @@ const defaultProps = {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
Icon: {
|
Icon: {
|
||||||
name: 'heroicons-outline:bell'
|
name: 'i-heroicons-bell'
|
||||||
},
|
},
|
||||||
Input: {
|
Input: {
|
||||||
name: 'input',
|
name: 'input',
|
||||||
@@ -259,6 +285,31 @@ const defaultProps = {
|
|||||||
title: 'Notification title',
|
title: 'Notification title',
|
||||||
callback: 'console.log(\'Timer expired\')'
|
callback: 'console.log(\'Timer expired\')'
|
||||||
},
|
},
|
||||||
|
ContextMenu: {
|
||||||
|
modelValue: isContextMenuOpen,
|
||||||
|
'onUpdate:modelValue': (v) => { isContextMenuOpen.value = v },
|
||||||
|
virtualElement,
|
||||||
|
component: {
|
||||||
|
name: 'Card',
|
||||||
|
props: {
|
||||||
|
variant: 'secondary',
|
||||||
|
label: 'Open context menu',
|
||||||
|
onClick: () => { isContextMenuOpen.value = false },
|
||||||
|
onContextmenu: (e) => {
|
||||||
|
e?.preventDefault()
|
||||||
|
openContextMenu()
|
||||||
|
},
|
||||||
|
class: 'relative w-[300px] h-[100px]'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: {
|
||||||
|
tag: 'div',
|
||||||
|
html: 'Context menu content',
|
||||||
|
class: 'rounded border u-border-gray-200 p-2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
Modal: {
|
Modal: {
|
||||||
modelValue: modal,
|
modelValue: modal,
|
||||||
'onUpdate:modelValue': (v) => { modal.value = v },
|
'onUpdate:modelValue': (v) => { modal.value = v },
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
Button:
|
Button:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UButton variant="primary" icon="heroicons-outline:mail">
|
<UButton variant="primary" icon="i-heroicons-envelope">
|
||||||
Button text
|
Button text
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<UModal v-model="isModalOpen" @submit.prevent="onSubmit">
|
<UModal v-model="isModalOpen" @submit.prevent="onSubmit">
|
||||||
<div class="sm:flex sm:items-start">
|
<div class="sm:flex sm:items-start">
|
||||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<IconCSS name="heroicons-outline:exclamation" class="h-6 w-6 text-red-600" aria-hidden="true" />
|
<UIcon name="i-heroicons-exclamation" class="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<h3 class="text-lg leading-6 font-medium u-text-gray-900">
|
<h3 class="text-lg leading-6 font-medium u-text-gray-900">
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UDropdown v-slot="{ open }" :items="dropdownItems" placement="bottom-start">
|
<UDropdown v-slot="{ open }" :items="dropdownItems" placement="bottom-start">
|
||||||
<UButton variant="white" :icon="open ? 'heroicons-solid:chevron-up' : 'heroicons-solid:chevron-down'" trailing icon-class="transition">
|
<UButton variant="white" :icon="open ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'" trailing icon-class="transition">
|
||||||
Open menu!
|
Open menu!
|
||||||
</UButton>
|
</UButton>
|
||||||
</UDropdown>
|
</UDropdown>
|
||||||
@@ -98,13 +98,13 @@
|
|||||||
|
|
||||||
<UPopover wrapper-class="inline-block relative">
|
<UPopover wrapper-class="inline-block relative">
|
||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
<UButton variant="secondary" :icon="open ? 'heroicons-solid:chevron-up' : 'heroicons-solid:chevron-down'" trailing icon-class="transition">
|
<UButton variant="secondary" :icon="open ? 'i-heroicons-chevron-up-20-solid-20' : 'i-heroicons-chevron-down-20-solid'" trailing icon-class="transition">
|
||||||
Open popover!
|
Open popover!
|
||||||
</UButton>
|
</UButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #panel>
|
<template #panel>
|
||||||
<div class="p-4 u-bg-white border u-border-gray-200 rounded-md">
|
<div class="p-2">
|
||||||
Panel
|
Panel
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UTooltip text="Hello tooltip!" :shortcuts="['⌘', 'G']">
|
<UTooltip text="Hello tooltip!" :shortcuts="['⌘', 'G']">
|
||||||
<IconCSS name="heroicons-outline:information-circle" class="w-6 h-6 u-text-gray-900 cursor-pointer" />
|
<UIcon name="i-heroicons-information-circle" class="w-6 h-6 u-text-gray-900 cursor-pointer" />
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -125,7 +125,17 @@
|
|||||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||||
Notifications:
|
Notifications:
|
||||||
</div>
|
</div>
|
||||||
<UButton icon="heroicons-outline:bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
|
<UButton icon="i-heroicons-bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||||
|
Copy text to clipboard:
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UInput v-model="textToCopy" name="textToCopy" />
|
||||||
|
<UButton icon="i-heroicons-document-duplicate-solid" variant="primary" label="Copy text" @click="onCopyTextClick" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -133,11 +143,11 @@
|
|||||||
Context menu:
|
Context menu:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UCard ref="contextMenuRef" class="relative" body-class="h-64" @click="isContextMenuOpen = false" @contextmenu.prevent="openContextMenu">
|
<UCard class="relative" body-class="h-64" @click="isContextMenuOpen = false" @contextmenu.prevent="openContextMenu">
|
||||||
<UContextMenu v-model="isContextMenuOpen" :virtual-element="virtualElement" width-class="w-48">
|
<UContextMenu v-model="isContextMenuOpen" :virtual-element="virtualElement" width-class="w-48">
|
||||||
<UCard @click.stop>
|
<div class="p-2">
|
||||||
Menu
|
Menu
|
||||||
</UCard>
|
</div>
|
||||||
</UContextMenu>
|
</UContextMenu>
|
||||||
</UCard>
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,18 +159,13 @@
|
|||||||
|
|
||||||
<UCard body-class="">
|
<UCard body-class="">
|
||||||
<UCommandPalette
|
<UCommandPalette
|
||||||
v-model="form.persons"
|
|
||||||
multiple
|
|
||||||
:placeholder="false"
|
:placeholder="false"
|
||||||
:options="{
|
:options="{
|
||||||
fuseOptions: {
|
fuseOptions: {
|
||||||
includeMatches: true
|
includeMatches: true
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
:groups="[{
|
:groups="commandPaletteGroups"
|
||||||
key: 'persons',
|
|
||||||
commands: people
|
|
||||||
}]"
|
|
||||||
command-attribute="name"
|
command-attribute="name"
|
||||||
/>
|
/>
|
||||||
</UCard>
|
</UCard>
|
||||||
@@ -174,7 +179,7 @@
|
|||||||
<UCard body-class="flex">
|
<UCard body-class="flex">
|
||||||
<div class="flex-1 px-4 py-5 sm:p-6 space-y-3">
|
<div class="flex-1 px-4 py-5 sm:p-6 space-y-3">
|
||||||
<UFormGroup label="Email" name="email" required>
|
<UFormGroup label="Email" name="email" required>
|
||||||
<UInput v-model="form.email" type="email" name="email" required icon="heroicons-outline:mail" />
|
<UInput v-model="form.email" type="email" name="email" required icon="i-heroicons-mail" />
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Description" name="description">
|
<UFormGroup label="Description" name="description">
|
||||||
@@ -189,7 +194,7 @@
|
|||||||
placeholder="Select a person"
|
placeholder="Select a person"
|
||||||
text-attribute="name"
|
text-attribute="name"
|
||||||
value-attribute="id"
|
value-attribute="id"
|
||||||
icon="heroicons-outline:user"
|
icon="i-heroicons-user"
|
||||||
/>
|
/>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
@@ -198,7 +203,7 @@
|
|||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Toggle" name="toggle">
|
<UFormGroup label="Toggle" name="toggle">
|
||||||
<UToggle v-model="form.toggle" name="toggle" icon-off="heroicons-solid:x" icon-on="heroicons-solid:check" />
|
<UToggle v-model="form.toggle" name="toggle" icon-off="i-heroicons-x-mark-20-solid" icon-on="i-heroicons-check-20-solid" />
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Notifications" label-class="text-base font-medium u-text-gray-900" description="How do you prefer to receive notifications?">
|
<UFormGroup label="Notifications" label-class="text-base font-medium u-text-gray-900" description="How do you prefer to receive notifications?">
|
||||||
@@ -235,6 +240,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const isModalOpen = ref(false)
|
const isModalOpen = ref(false)
|
||||||
|
const textToCopy = ref('Copied text')
|
||||||
|
|
||||||
const people = ref([
|
const people = ref([
|
||||||
{ id: 1, name: 'Durward Reynolds', disabled: false },
|
{ id: 1, name: 'Durward Reynolds', disabled: false },
|
||||||
@@ -256,12 +262,25 @@ const form = reactive({
|
|||||||
persons: ref([people.value[0]])
|
persons: ref([people.value[0]])
|
||||||
})
|
})
|
||||||
|
|
||||||
const { $toast } = useNuxtApp()
|
const toast = useToast()
|
||||||
|
const clipboard = useCopyToClipboard()
|
||||||
|
|
||||||
const x = ref(0)
|
const x = ref(0)
|
||||||
const y = ref(0)
|
const y = ref(0)
|
||||||
const isContextMenuOpen = ref(false)
|
const isContextMenuOpen = ref(false)
|
||||||
const contextMenuRef = ref(null)
|
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
|
||||||
|
|
||||||
|
const commandPaletteGroups = computed(() => ([{
|
||||||
|
key: 'people',
|
||||||
|
commands: people.value
|
||||||
|
}, {
|
||||||
|
key: 'search',
|
||||||
|
label: q => q && `Search results for "${q}"...`,
|
||||||
|
search: async (q) => {
|
||||||
|
if (!q) { return [] }
|
||||||
|
return await $fetch(`https://jsonplaceholder.typicode.com/users?q=${q}`)
|
||||||
|
}
|
||||||
|
}]))
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('mousemove', ({ clientX, clientY }) => {
|
document.addEventListener('mousemove', ({ clientX, clientY }) => {
|
||||||
@@ -270,8 +289,6 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
|
|
||||||
|
|
||||||
function openContextMenu () {
|
function openContextMenu () {
|
||||||
const top = unref(y)
|
const top = unref(y)
|
||||||
const left = unref(x)
|
const left = unref(x)
|
||||||
@@ -302,27 +319,27 @@ function onSubmit () {
|
|||||||
const dropdownItems = [
|
const dropdownItems = [
|
||||||
[{
|
[{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: 'heroicons-solid:pencil',
|
icon: 'i-heroicons-pencil-square-20-solid',
|
||||||
click: (e) => {
|
click: (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
label: 'Duplicate',
|
label: 'Duplicate',
|
||||||
icon: 'heroicons-solid:duplicate'
|
icon: 'i-heroicons-document-duplicate-20-solid'
|
||||||
}],
|
}],
|
||||||
[{
|
[{
|
||||||
label: 'Archive',
|
label: 'Archive',
|
||||||
icon: 'heroicons-solid:archive'
|
icon: 'i-heroicons-archive-box-20-solid'
|
||||||
}, {
|
}, {
|
||||||
label: 'Move',
|
label: 'Move',
|
||||||
icon: 'heroicons-solid:external-link',
|
icon: 'i-heroicons-arrow-right-circle-20-solid',
|
||||||
to: 'https://www.google.fr',
|
to: 'https://www.google.fr',
|
||||||
target: '_blank'
|
target: '_blank'
|
||||||
}],
|
}],
|
||||||
[{
|
[{
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
icon: 'heroicons-solid:trash'
|
icon: 'i-heroicons-trash-20-solid'
|
||||||
}]
|
}]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -335,12 +352,16 @@ const customDropdownItems = [
|
|||||||
}],
|
}],
|
||||||
[{
|
[{
|
||||||
label: 'About',
|
label: 'About',
|
||||||
icon: 'heroicons-solid:plus',
|
icon: 'i-heroicons-plus-20-solid',
|
||||||
to: '/about'
|
to: '/about'
|
||||||
}]
|
}]
|
||||||
]
|
]
|
||||||
|
|
||||||
const onNotificationClick = () => {
|
const onNotificationClick = () => {
|
||||||
$toast.error({ title: 'Error', description: 'This is an error message' })
|
toast.error({ title: 'Error', description: 'This is an error message' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCopyTextClick = () => {
|
||||||
|
clipboard.copy(textToCopy.value, { title: 'Text copied successfully!' })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
38
package.json
38
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nuxthq/ui",
|
"name": "@nuxthq/ui",
|
||||||
"version": "0.2.1",
|
"version": "1.2.4",
|
||||||
"repository": "https://github.com/nuxtlabs/ui",
|
"repository": "https://github.com/nuxtlabs/ui",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -25,39 +25,35 @@
|
|||||||
"release": "yarn lint && standard-version && git push --follow-tags"
|
"release": "yarn lint && standard-version && git push --follow-tags"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/vue": "^1.7.10",
|
"@egoist/tailwindcss-icons": "^1.0.7",
|
||||||
"@iconify/vue": "^4.1.0",
|
"@headlessui/vue": "1.7.10",
|
||||||
"@nuxt/kit": "^3.2.0",
|
"@iconify-json/heroicons": "^1.1.10",
|
||||||
|
"@nuxt/kit": "^3.3.1",
|
||||||
"@nuxtjs/color-mode": "^3.2.0",
|
"@nuxtjs/color-mode": "^3.2.0",
|
||||||
"@nuxtjs/tailwindcss": "^6.4.0",
|
"@nuxtjs/tailwindcss": "^6.6.0",
|
||||||
"@popperjs/core": "^2.11.6",
|
"@popperjs/core": "^2.11.6",
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@tailwindcss/line-clamp": "^0.4.2",
|
"@tailwindcss/line-clamp": "^0.4.2",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@vueuse/core": "^9.12.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
"@vueuse/integrations": "^9.12.0",
|
"@vueuse/integrations": "^9.13.0",
|
||||||
|
"@vueuse/math": "^9.13.0",
|
||||||
"defu": "^6.1.2",
|
"defu": "^6.1.2",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nuxt-icon": "^0.3.2",
|
"tailwindcss": "^3.2.7"
|
||||||
"tailwindcss": "^3.2.6"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/mdi": "^1.1.50",
|
||||||
"@nuxt/module-builder": "^0.2.1",
|
"@nuxt/module-builder": "^0.2.1",
|
||||||
"@nuxtjs/eslint-config-typescript": "^12.0.0",
|
"@nuxtjs/eslint-config-typescript": "^12.0.0",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.7",
|
||||||
"@types/node": "^18.13.0",
|
"@types/node": "^18.15.3",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.36.0",
|
||||||
"nuxt": "^3.2.0",
|
"nuxt": "^3.3.1",
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
"unbuild": "^1.1.1",
|
"unbuild": "^1.1.2",
|
||||||
"vue-tsc": "^1.1.0"
|
"vue-tsc": "^1.2.0"
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"externals": [
|
|
||||||
"tailwindcss",
|
|
||||||
"tailwindcss/colors.js"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, addTemplate, addPlugin, createResolver } from '@nuxt/kit'
|
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, addTemplate, createResolver } from '@nuxt/kit'
|
||||||
import { defu } from 'defu'
|
import { 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'
|
||||||
|
import { iconsPlugin, getIconCollections } from '@egoist/tailwindcss-icons'
|
||||||
import { name, version } from '../package.json'
|
import { name, version } from '../package.json'
|
||||||
import { colorsAsRegex, excludeColors } from './runtime/utils/colors'
|
import { colorsAsRegex, excludeColors } from './runtime/utils/colors'
|
||||||
import defaultPreset from './runtime/presets/default'
|
import defaultPreset from './runtime/presets/default'
|
||||||
@@ -39,6 +40,8 @@ export interface ModuleOptions {
|
|||||||
|
|
||||||
colors?: ColorsOptions
|
colors?: ColorsOptions
|
||||||
|
|
||||||
|
icons: string[]
|
||||||
|
|
||||||
tailwindcss?: Partial<Config>
|
tailwindcss?: Partial<Config>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +52,7 @@ const defaults = {
|
|||||||
primary: 'indigo',
|
primary: 'indigo',
|
||||||
gray: 'gray'
|
gray: 'gray'
|
||||||
},
|
},
|
||||||
|
icons: ['heroicons'],
|
||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
theme: {}
|
theme: {}
|
||||||
}
|
}
|
||||||
@@ -72,7 +76,7 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
// Transpile runtime
|
// Transpile runtime
|
||||||
const runtimeDir = resolve('./runtime')
|
const runtimeDir = resolve('./runtime')
|
||||||
nuxt.options.build.transpile.push(runtimeDir)
|
nuxt.options.build.transpile.push(runtimeDir)
|
||||||
nuxt.options.build.transpile.push('@popperjs/core', '@headlessui/vue', '@iconify/vue')
|
nuxt.options.build.transpile.push('@popperjs/core', '@headlessui/vue')
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
nuxt.hook('tailwindcss:config', function (tailwindConfig: TailwindConfig) {
|
nuxt.hook('tailwindcss:config', function (tailwindConfig: TailwindConfig) {
|
||||||
@@ -108,6 +112,9 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
variants: ['focus']
|
variants: ['focus']
|
||||||
}])
|
}])
|
||||||
|
|
||||||
|
tailwindConfig.plugins = tailwindConfig.plugins || []
|
||||||
|
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
|
||||||
|
|
||||||
const ui: object = defu(preset, defaultPreset(variantColors))
|
const ui: object = defu(preset, defaultPreset(variantColors))
|
||||||
|
|
||||||
addTemplate({
|
addTemplate({
|
||||||
@@ -121,9 +128,6 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
nuxt.options.appConfig.nuxtIcon = defu(nuxt.options.appConfig.nuxtIcon, { size: false })
|
|
||||||
|
|
||||||
await installModule('nuxt-icon')
|
|
||||||
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
|
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
|
||||||
await installModule('@nuxtjs/tailwindcss', {
|
await installModule('@nuxtjs/tailwindcss', {
|
||||||
viewer: false,
|
viewer: false,
|
||||||
@@ -151,9 +155,6 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
cssPath: resolve(runtimeDir, 'tailwind.css')
|
cssPath: resolve(runtimeDir, 'tailwind.css')
|
||||||
})
|
})
|
||||||
|
|
||||||
addPlugin(resolve(runtimeDir, 'plugins', 'toast.client'))
|
|
||||||
addPlugin(resolve(runtimeDir, 'plugins', 'clipboard.client'))
|
|
||||||
|
|
||||||
addComponentsDir({
|
addComponentsDir({
|
||||||
path: resolve(runtimeDir, 'components', 'elements'),
|
path: resolve(runtimeDir, 'components', 'elements'),
|
||||||
prefix,
|
prefix,
|
||||||
|
|||||||
@@ -38,12 +38,8 @@ const props = defineProps({
|
|||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
chip: {
|
chip: {
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
chipVariant: {
|
|
||||||
type: String,
|
type: String,
|
||||||
default: 'primary',
|
default: null,
|
||||||
validator (value: string) {
|
validator (value: string) {
|
||||||
return Object.keys($ui.avatar.chip.variant).includes(value)
|
return Object.keys($ui.avatar.chip.variant).includes(value)
|
||||||
}
|
}
|
||||||
@@ -92,8 +88,8 @@ const avatarClass = computed(() => {
|
|||||||
const chipClass = computed(() => {
|
const chipClass = computed(() => {
|
||||||
return classNames(
|
return classNames(
|
||||||
$ui.avatar.chip.base,
|
$ui.avatar.chip.base,
|
||||||
|
$ui.avatar.chip.variant[props.chip],
|
||||||
$ui.avatar.chip.position[props.chipPosition],
|
$ui.avatar.chip.position[props.chipPosition],
|
||||||
$ui.avatar.chip.variant[props.chipVariant],
|
|
||||||
$ui.avatar.chip.size[props.size]
|
$ui.avatar.chip.size[props.size]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
:aria-label="ariaLabel"
|
:aria-label="ariaLabel"
|
||||||
v-bind="buttonProps"
|
v-bind="buttonProps"
|
||||||
>
|
>
|
||||||
<IconCSS v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
|
<Icon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
|
||||||
<slot>
|
<slot>
|
||||||
<span :class="[truncate ? 'text-left break-all line-clamp-1' : '', compact ? 'hidden sm:block' : '']">
|
<span :class="[truncate ? 'text-left break-all line-clamp-1' : '', compact ? 'hidden sm:block' : '']">
|
||||||
<span :class="[labelCompact && 'hidden sm:block']">{{ label }}</span>
|
<span :class="[labelCompact && 'hidden sm:block']">{{ label }}</span>
|
||||||
<span v-if="labelCompact" class="sm:hidden">{{ labelCompact }}</span>
|
<span v-if="labelCompact" class="sm:hidden">{{ labelCompact }}</span>
|
||||||
</span>
|
</span>
|
||||||
</slot>
|
</slot>
|
||||||
<IconCSS v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
|
<Icon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ import { ref, computed, useSlots } from 'vue'
|
|||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import type { RouteLocationNormalized, RouteLocationRaw } from 'vue-router'
|
import type { RouteLocationNormalized, RouteLocationRaw } from 'vue-router'
|
||||||
import NuxtLink from '#app/components/nuxt-link'
|
import NuxtLink from '#app/components/nuxt-link'
|
||||||
|
import Icon from '../elements/Icon.vue'
|
||||||
import { classNames } from '../../utils'
|
import { classNames } from '../../utils'
|
||||||
import $ui from '#build/ui'
|
import $ui from '#build/ui'
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<div v-if="open && items.length" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
|
<div v-if="open && items.length" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
|
||||||
<transition appear v-bind="transitionClass">
|
<transition appear v-bind="transitionClass">
|
||||||
<MenuItems :class="baseClass" static>
|
<MenuItems :class="[baseClass, divideClass, ringClass, roundedClass, shadowClass, backgroundClass]" static>
|
||||||
<div v-for="(subItems, index) of items" :key="index" :class="groupClass">
|
<div v-for="(subItems, index) of items" :key="index" :class="groupClass">
|
||||||
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
|
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
|
||||||
<Component
|
<Component
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
@click="item.click"
|
@click="item.click"
|
||||||
>
|
>
|
||||||
<slot :name="item.slot" :item="item">
|
<slot :name="item.slot" :item="item">
|
||||||
<IconCSS v-if="item.icon" :name="item.icon" :class="[itemIconClass, item.iconClass]" />
|
<Icon v-if="item.icon" :name="item.icon" :class="[itemIconClass, item.iconClass]" />
|
||||||
<Avatar v-if="item.avatar" v-bind="{ size: 'xxs', ...item.avatar }" :class="itemAvatarClass" />
|
<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>
|
||||||
@@ -57,6 +57,7 @@ import type { RouteLocationNormalized } from 'vue-router'
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import NuxtLink from '#app/components/nuxt-link'
|
import NuxtLink from '#app/components/nuxt-link'
|
||||||
|
import Icon from '../elements/Icon.vue'
|
||||||
import Avatar from '../elements/Avatar.vue'
|
import Avatar from '../elements/Avatar.vue'
|
||||||
import { classNames, omit } from '../../utils'
|
import { classNames, omit } from '../../utils'
|
||||||
import { usePopper } from '../../composables/usePopper'
|
import { usePopper } from '../../composables/usePopper'
|
||||||
@@ -103,6 +104,26 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: () => $ui.dropdown.width
|
default: () => $ui.dropdown.width
|
||||||
},
|
},
|
||||||
|
backgroundClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.dropdown.background
|
||||||
|
},
|
||||||
|
shadowClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.dropdown.shadow
|
||||||
|
},
|
||||||
|
roundedClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.dropdown.rounded
|
||||||
|
},
|
||||||
|
ringClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.dropdown.ring
|
||||||
|
},
|
||||||
|
divideClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.dropdown.divide
|
||||||
|
},
|
||||||
baseClass: {
|
baseClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: () => $ui.dropdown.base
|
default: () => $ui.dropdown.base
|
||||||
|
|||||||
16
src/runtime/components/elements/Icon.vue
Normal file
16
src/runtime/components/elements/Icon.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<span :class="name" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default { name: 'UIcon' }
|
||||||
|
</script>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-md p-4" :class="variantClass">
|
<div class="rounded-md p-4" :class="variantClass">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="inline-flex flex-shrink-0">
|
||||||
<IconCSS v-if="iconName" :name="iconName" :class="iconClass" class="h-5 w-5" />
|
<Icon v-if="iconName" :name="iconName" :class="iconClass" class="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 flex-1 md:flex md:justify-between">
|
<div class="ml-3 flex-1 md:flex md:justify-between">
|
||||||
<p v-if="title" class="text-sm leading-5" :class="titleClass">
|
<p v-if="title" class="text-sm leading-5" :class="titleClass">
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import type { RouteLocationNormalized } from 'vue-router'
|
import type { RouteLocationNormalized } from 'vue-router'
|
||||||
|
import Icon from '../elements/Icon.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
variant: {
|
variant: {
|
||||||
@@ -56,10 +57,10 @@ const props = defineProps({
|
|||||||
|
|
||||||
const iconName = computed(() => {
|
const iconName = computed(() => {
|
||||||
return ({
|
return ({
|
||||||
info: 'heroicons-solid:information-circle',
|
info: 'i-heroicons-information-circle',
|
||||||
warning: 'heroicons-solid:exclamation',
|
warning: 'i-heroicons-exclamation',
|
||||||
error: 'heroicons-solid:x-circle',
|
error: 'i-heroicons-x-circle',
|
||||||
success: 'heroicons-solid:check-circle'
|
success: 'i-heroicons-check-circle'
|
||||||
})[props.variant]
|
})[props.variant]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Modal v-model="isOpen" :appear="appear" class="w-full" @close="onClose">
|
<Modal v-model="isOpen" :appear="appear" class="w-full" @close="onClose">
|
||||||
<div class="sm:flex sm:items-start">
|
<div class="sm:flex sm:items-start">
|
||||||
<div v-if="icon" :class="iconWrapperClass" class="mx-auto flex-shrink-0 flex items-center justify-center rounded-full sm:mx-0">
|
<div v-if="icon" :class="iconWrapperClass" class="mx-auto flex-shrink-0 flex items-center justify-center rounded-full sm:mx-0">
|
||||||
<IconCSS :name="icon" :class="iconClass" />
|
<Icon :name="icon" :class="iconClass" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left space-y-2">
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left space-y-2">
|
||||||
<DialogTitle v-if="title" as="h3" :class="titleClass">
|
<DialogTitle v-if="title" as="h3" :class="titleClass">
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
<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 Icon from '../elements/Icon.vue'
|
||||||
import Modal from '../overlays/Modal.vue'
|
import Modal from '../overlays/Modal.vue'
|
||||||
import Button from '../elements/Button.vue'
|
import Button from '../elements/Button.vue'
|
||||||
import $ui from '#build/ui'
|
import $ui from '#build/ui'
|
||||||
@@ -54,7 +55,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
titleClass: {
|
titleClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: () => $ui.alertDialog.icon.title
|
default: () => $ui.alertDialog.title
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -19,16 +19,17 @@
|
|||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<div v-if="isLeading" :class="iconLeadingWrapperClass">
|
<div v-if="isLeading" :class="iconLeadingWrapperClass">
|
||||||
<IconCSS v-if="iconName" :name="iconName" :class="iconClass" />
|
<Icon v-if="iconName" :name="iconName" :class="iconClass" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isTrailing" :class="iconTrailingWrapperClass">
|
<div v-if="isTrailing" :class="iconTrailingWrapperClass">
|
||||||
<IconCSS v-if="iconName" :name="iconName" :class="iconClass" />
|
<Icon v-if="iconName" :name="iconName" :class="iconClass" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import Icon from '../elements/Icon.vue'
|
||||||
import { classNames } from '../../utils'
|
import { classNames } from '../../utils'
|
||||||
import $ui from '#build/ui'
|
import $ui from '#build/ui'
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div v-if="icon" :class="iconWrapperClass">
|
<div v-if="icon" :class="iconWrapperClass">
|
||||||
<IconCSS :name="icon" :class="iconClass" />
|
<Icon :name="icon" :class="iconClass" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { get } from 'lodash-es'
|
import { get } from 'lodash-es'
|
||||||
import { classNames } from '../../utils'
|
import { classNames } from '../../utils'
|
||||||
|
import Icon from '../elements/Icon.vue'
|
||||||
import $ui from '#build/ui'
|
import $ui from '#build/ui'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</slot>
|
</slot>
|
||||||
<slot name="icon">
|
<slot name="icon">
|
||||||
<span :class="iconWrapperClass">
|
<span :class="iconWrapperClass">
|
||||||
<IconCSS v-if="icon" :name="icon" :class="iconClass" aria-hidden="true" />
|
<Icon v-if="icon" :name="icon" :class="iconClass" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
</slot>
|
</slot>
|
||||||
</button>
|
</button>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span v-if="selected" :class="resolveOptionIconClass({ active })">
|
<span v-if="selected" :class="resolveOptionIconClass({ active })">
|
||||||
<IconCSS v-if="listOptionIcon" :name="listOptionIcon" :class="listOptionIconSizeClass" aria-hidden="true" />
|
<Icon v-if="listOptionIcon" :name="listOptionIcon" :class="listOptionIconSizeClass" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ComboboxOption>
|
</ComboboxOption>
|
||||||
@@ -94,6 +94,7 @@ import {
|
|||||||
ComboboxOption,
|
ComboboxOption,
|
||||||
ComboboxInput
|
ComboboxInput
|
||||||
} from '@headlessui/vue'
|
} from '@headlessui/vue'
|
||||||
|
import Icon from '../elements/Icon.vue'
|
||||||
import { classNames } from '../../utils'
|
import { classNames } from '../../utils'
|
||||||
import { usePopper } from '../../composables/usePopper'
|
import { usePopper } from '../../composables/usePopper'
|
||||||
import type { PopperOptions } from '../../types'
|
import type { PopperOptions } from '../../types'
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
>
|
>
|
||||||
<span :class="[active ? containerActiveClass : containerInactiveClass, containerBaseClass]">
|
<span :class="[active ? containerActiveClass : containerInactiveClass, containerBaseClass]">
|
||||||
<span v-if="iconOn" :class="[active ? iconActiveClass : iconInactiveClass, iconBaseClass]" aria-hidden="true">
|
<span v-if="iconOn" :class="[active ? iconActiveClass : iconInactiveClass, iconBaseClass]" aria-hidden="true">
|
||||||
<IconCSS :name="iconOn" :class="iconOnClass" />
|
<Icon :name="iconOn" :class="iconOnClass" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="iconOff" :class="[active ? iconInactiveClass : iconActiveClass, iconBaseClass]" aria-hidden="true">
|
<span v-if="iconOff" :class="[active ? iconInactiveClass : iconActiveClass, iconBaseClass]" aria-hidden="true">
|
||||||
<IconCSS :name="iconOff" :class="iconOffClass" />
|
<Icon :name="iconOff" :class="iconOffClass" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -17,6 +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.vue'
|
||||||
import $ui from '#build/ui'
|
import $ui from '#build/ui'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
>
|
>
|
||||||
<div :class="$ui.commandPalette.wrapper">
|
<div :class="$ui.commandPalette.wrapper">
|
||||||
<div v-show="searchable" class="relative flex items-center">
|
<div v-show="searchable" class="relative flex items-center">
|
||||||
<IconCSS v-if="inputIcon" :name="inputIcon" :class="$ui.commandPalette.input.icon.base" aria-hidden="true" />
|
<Icon v-if="inputIcon" :name="inputIcon" :class="$ui.commandPalette.input.icon.base" aria-hidden="true" />
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
ref="comboboxInput"
|
ref="comboboxInput"
|
||||||
:value="query"
|
:value="query"
|
||||||
@@ -38,7 +38,14 @@
|
|||||||
aria-label="Commands"
|
aria-label="Commands"
|
||||||
class="relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2"
|
class="relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2"
|
||||||
>
|
>
|
||||||
<CommandPaletteGroup v-for="group of groups" :key="group.key" :group="group" :group-attribute="groupAttribute" :command-attribute="commandAttribute">
|
<CommandPaletteGroup
|
||||||
|
v-for="group of groups"
|
||||||
|
:key="group.key"
|
||||||
|
:query="query"
|
||||||
|
:group="group"
|
||||||
|
:group-attribute="groupAttribute"
|
||||||
|
:command-attribute="commandAttribute"
|
||||||
|
>
|
||||||
<template v-for="(_, name) in $slots" #[name]="slotData">
|
<template v-for="(_, name) in $slots" #[name]="slotData">
|
||||||
<slot :name="name" v-bind="slotData" />
|
<slot :name="name" v-bind="slotData" />
|
||||||
</template>
|
</template>
|
||||||
@@ -46,7 +53,7 @@
|
|||||||
</ComboboxOptions>
|
</ComboboxOptions>
|
||||||
|
|
||||||
<div v-else-if="placeholder" 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">
|
||||||
<IconCSS v-if="emptyIcon" :name="emptyIcon" class="w-6 h-6 mx-auto u-text-gray-400 mb-4" aria-hidden="true" />
|
<Icon v-if="emptyIcon" :name="emptyIcon" class="w-6 h-6 mx-auto u-text-gray-400 mb-4" aria-hidden="true" />
|
||||||
<p class="text-sm text-center u-text-gray-900">
|
<p class="text-sm text-center u-text-gray-900">
|
||||||
{{ query ? "We couldn't find any items with that term. Please try again." : "We couldn't find any items." }}
|
{{ query ? "We couldn't find any items with that term. Please try again." : "We couldn't find any items." }}
|
||||||
</p>
|
</p>
|
||||||
@@ -59,11 +66,13 @@
|
|||||||
import { ref, computed, watch, onMounted } from 'vue'
|
import { ref, computed, watch, 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 { useDebounceFn } from '@vueuse/core'
|
||||||
import { useFuse } from '@vueuse/integrations/useFuse'
|
import { useFuse } from '@vueuse/integrations/useFuse'
|
||||||
import { groupBy, map } from 'lodash-es'
|
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 Button from '../elements/Button.vue'
|
||||||
import CommandPaletteGroup from './CommandPaletteGroup.vue'
|
import CommandPaletteGroup from './CommandPaletteGroup.vue'
|
||||||
import $ui from '#build/ui'
|
import $ui from '#build/ui'
|
||||||
@@ -132,6 +141,10 @@ const props = defineProps({
|
|||||||
placeholder: {
|
placeholder: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
debounce: {
|
||||||
|
type: Number,
|
||||||
|
default: 200
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -167,29 +180,44 @@ const options: ComputedRef<Partial<UseFuseOptions<Command>>> = computed(() => de
|
|||||||
matchAllWhenSearchEmpty: true
|
matchAllWhenSearchEmpty: true
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const commands = computed(() => props.groups.reduce((acc, group) => {
|
const commands = computed(() => props.groups.filter(group => !group.search).reduce((acc, group) => {
|
||||||
return acc.concat(group.commands.map(command => ({ ...command, group: group.key })))
|
return acc.concat(group.commands.map(command => ({ ...command, group: group.key })))
|
||||||
}, [] as Command[]))
|
}, [] as Command[]))
|
||||||
|
|
||||||
|
const searchResults = ref<{ [key: string]: any }>({})
|
||||||
|
|
||||||
const { results } = useFuse(query, commands, options)
|
const { results } = useFuse(query, commands, options)
|
||||||
|
|
||||||
const groups = computed(() => map(groupBy(results.value, command => command.item.group), (results, key) => {
|
const groups = computed(() => ([
|
||||||
const commands = results.map((result) => {
|
...map(groupBy(results.value, command => command.item.group), (results, key) => {
|
||||||
const { item, ...data } = result
|
const commands = results.map((result) => {
|
||||||
|
const { item, ...data } = result
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...props.groups.find(group => group.key === key),
|
||||||
...data
|
commands: commands.slice(0, options.value.resultLimit)
|
||||||
}
|
} as Group
|
||||||
})
|
}),
|
||||||
|
...props.groups.filter(group => !!group.search).map(group => ({ ...group, commands: (searchResults.value[group.key] || []).slice(0, options.value.resultLimit) })).filter(group => group.commands.length)
|
||||||
|
]))
|
||||||
|
|
||||||
return {
|
const debouncedSearch = useDebounceFn(async () => {
|
||||||
...props.groups.find(group => group.key === key),
|
const searchableGroups = props.groups.filter(group => !!group.search)
|
||||||
commands: commands.slice(0, options.value.resultLimit)
|
|
||||||
} as Group
|
await Promise.all(searchableGroups.map(async (group) => {
|
||||||
}))
|
searchResults.value[group.key] = await group.search(query.value)
|
||||||
|
}))
|
||||||
|
}, props.debounce)
|
||||||
|
|
||||||
watch(query, () => {
|
watch(query, () => {
|
||||||
|
debouncedSearch()
|
||||||
|
|
||||||
// Select first item on search changes
|
// Select first item on search changes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L804
|
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L804
|
||||||
@@ -227,6 +255,7 @@ function onClear () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
query,
|
||||||
updateQuery: (q: string) => {
|
updateQuery: (q: string) => {
|
||||||
query.value = q
|
query.value = q
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-2" role="option">
|
<div class="p-2" role="option">
|
||||||
<h2 v-if="group[groupAttribute]" class="px-3 my-2 text-xs font-semibold u-text-gray-900">
|
<h2 v-if="label" class="px-3 my-2 text-xs font-semibold u-text-gray-900">
|
||||||
{{ group[groupAttribute] }}
|
{{ label }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="text-sm u-text-gray-700" role="listbox" :aria-label="group[groupAttribute]">
|
<div class="text-sm u-text-gray-700" role="listbox" :aria-label="group[groupAttribute]">
|
||||||
@@ -15,14 +15,16 @@
|
|||||||
>
|
>
|
||||||
<div :class="['flex justify-between select-none items-center rounded-md px-3 py-2 gap-3 relative', active && 'bg-gray-100 dark:bg-gray-800 u-text-gray-900', command.disabled ? 'cursor-not-allowed' : 'cursor-pointer']">
|
<div :class="['flex justify-between select-none items-center rounded-md px-3 py-2 gap-3 relative', active && 'bg-gray-100 dark:bg-gray-800 u-text-gray-900', command.disabled ? 'cursor-not-allowed' : 'cursor-pointer']">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<IconCSS 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" />
|
<slot :name="`${group.key}-icon`" :group="group" :command="command">
|
||||||
<Avatar
|
<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" />
|
||||||
v-else-if="command.avatar"
|
<Avatar
|
||||||
v-bind="{ size: 'xxxs', ...command.avatar }"
|
v-else-if="command.avatar"
|
||||||
class="flex-shrink-0"
|
v-bind="{ size: 'xxxs', ...command.avatar }"
|
||||||
aria-hidden="true"
|
class="flex-shrink-0"
|
||||||
/>
|
aria-hidden="true"
|
||||||
<span v-else-if="command.chip" class="flex-shrink-0 w-2 h-2 mx-1 rounded-full" :style="{ background: `#${command.chip}` }" />
|
/>
|
||||||
|
<span v-else-if="command.chip" class="flex-shrink-0 w-2 h-2 mx-1 rounded-full" :style="{ background: `#${command.chip}` }" />
|
||||||
|
</slot>
|
||||||
|
|
||||||
<div class="flex items-center gap-1.5 min-w-0" :class="{ 'opacity-50': command.disabled }">
|
<div class="flex items-center gap-1.5 min-w-0" :class="{ 'opacity-50': command.disabled }">
|
||||||
<slot :name="`${group.key}-command`" :group="group" :command="command">
|
<slot :name="`${group.key}-command`" :group="group" :command="command">
|
||||||
@@ -35,12 +37,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IconCSS v-if="selected" name="heroicons-outline:check" class="h-5 w-5 u-text-gray-900 flex-shrink-0" aria-hidden="true" />
|
<Icon v-if="selected" :name="$ui.commandPalette.option.selected.icon.name" class="h-5 w-5 u-text-gray-900 flex-shrink-0" aria-hidden="true" />
|
||||||
<slot v-else-if="active && (group.active || $slots[`${group.key}-active`])" :name="`${group.key}-active`" :group="group" :command="command">
|
<slot v-else-if="active && (group.active || $slots[`${group.key}-active`])" :name="`${group.key}-active`" :group="group" :command="command">
|
||||||
<span v-if="group.active" class="flex-shrink-0 u-text-gray-500">{{ group.active }}</span>
|
<span v-if="group.active" class="flex-shrink-0 u-text-gray-500">{{ group.active }}</span>
|
||||||
</slot>
|
</slot>
|
||||||
<slot v-else :name="`${group.key}-inactive`" :group="group" :command="command">
|
<slot v-else :name="`${group.key}-inactive`" :group="group" :command="command">
|
||||||
<span v-if="command.shortcuts?.length" class="flex-shrink-0 text-xs font-semibold u-text-gray-500">
|
<span v-if="command.shortcuts?.length" :class="$ui.commandPalette.option.shortcuts">
|
||||||
<kbd v-for="shortcut of command.shortcuts" :key="shortcut" class="font-sans">{{ shortcut }}</kbd>
|
<kbd v-for="shortcut of command.shortcuts" :key="shortcut" class="font-sans">{{ shortcut }}</kbd>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="!command.disabled && group.inactive" class="flex-shrink-0 u-text-gray-500">{{ group.inactive }}</span>
|
<span v-else-if="!command.disabled && group.inactive" class="flex-shrink-0 u-text-gray-500">{{ group.inactive }}</span>
|
||||||
@@ -52,16 +54,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
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 Avatar from '../elements/Avatar.vue'
|
||||||
import type { Group } from '../../types/command-palette'
|
import type { Group } from '../../types/command-palette'
|
||||||
|
import $ui from '#build/ui'
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
group: {
|
group: {
|
||||||
type: Object as PropType<Group>,
|
type: Object as PropType<Group>,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
query: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
groupAttribute: {
|
groupAttribute: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@@ -72,6 +81,12 @@ defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const label = computed(() => {
|
||||||
|
const label = props.group[props.groupAttribute]
|
||||||
|
|
||||||
|
return typeof label === 'function' ? label(props.query) : label
|
||||||
|
})
|
||||||
|
|
||||||
function highlight ({ indices, value }: { indices: number[][], value:string }, i = 1): string {
|
function highlight ({ indices, value }: { indices: number[][], value:string }, i = 1): string {
|
||||||
const pair = indices[indices.length - i]
|
const pair = indices[indices.length - i]
|
||||||
if (!pair) {
|
if (!pair) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
<slot name="icon" :link="link" :is-active="isActive">
|
<slot name="icon" :link="link" :is-active="isActive">
|
||||||
<IconCSS
|
<Icon
|
||||||
v-if="link.icon"
|
v-if="link.icon"
|
||||||
:name="link.icon"
|
:name="link.icon"
|
||||||
:class="[iconBaseClass, link.label && iconSpacingClass, isActive ? iconActiveClass : iconInactiveClass, link.iconClass]"
|
:class="[iconBaseClass, link.label && iconSpacingClass, isActive ? iconActiveClass : iconInactiveClass, link.iconClass]"
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import type { RouteLocationNormalized } from 'vue-router'
|
import type { RouteLocationNormalized } from 'vue-router'
|
||||||
|
import Icon from '../elements/Icon.vue'
|
||||||
import Link from '../elements/Link.vue'
|
import Link from '../elements/Link.vue'
|
||||||
import Avatar from '../elements/Avatar.vue'
|
import Avatar from '../elements/Avatar.vue'
|
||||||
import type { Avatar as AvatarType } from '../../types/avatar'
|
import type { Avatar as AvatarType } from '../../types/avatar'
|
||||||
@@ -48,7 +49,7 @@ import $ui from '#build/ui'
|
|||||||
defineProps({
|
defineProps({
|
||||||
links: {
|
links: {
|
||||||
type: Array as PropType<{
|
type: Array as PropType<{
|
||||||
to?: RouteLocationNormalized
|
to?: RouteLocationNormalized | string
|
||||||
exact?: boolean
|
exact?: boolean
|
||||||
label: string
|
label: string
|
||||||
icon?: string
|
icon?: string
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isOpen" ref="container" :class="[containerClass, widthClass]">
|
<div v-if="isOpen" ref="container" :class="[containerClass, widthClass]">
|
||||||
<transition appear v-bind="transitionClass">
|
<transition appear v-bind="transitionClass">
|
||||||
<div :class="baseClass">
|
<div :class="[baseClass, ringClass, roundedClass, shadowClass, backgroundClass]">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -38,6 +38,22 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: () => $ui.contextMenu.width
|
default: () => $ui.contextMenu.width
|
||||||
},
|
},
|
||||||
|
backgroundClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.contextMenu.background
|
||||||
|
},
|
||||||
|
shadowClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.contextMenu.shadow
|
||||||
|
},
|
||||||
|
roundedClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.contextMenu.rounded
|
||||||
|
},
|
||||||
|
ringClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.contextMenu.ring
|
||||||
|
},
|
||||||
baseClass: {
|
baseClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: () => $ui.contextMenu.base
|
default: () => $ui.contextMenu.base
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }">
|
<div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }">
|
||||||
<div v-if="iconName" class="flex-shrink-0">
|
<div v-if="iconName" class="flex-shrink-0">
|
||||||
<IconCSS :name="iconName" :class="iconClass" />
|
<Icon :name="iconName" :class="iconClass" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 flex-1">
|
<div class="w-0 flex-1">
|
||||||
<p class="text-sm font-medium u-text-gray-900">
|
<p class="text-sm font-medium u-text-gray-900">
|
||||||
@@ -33,11 +33,11 @@
|
|||||||
</div>
|
</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="inline-flex transition duration-150 ease-in-out u-text-gray-400 focus:outline-none hover:u-text-gray-500 focus:u-text-gray-500"
|
||||||
@click.stop="onClose"
|
@click.stop="onClose"
|
||||||
>
|
>
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
<IconCSS name="heroicons-solid:x" class="w-5 h-5" />
|
<Icon :name="$ui.notification.close.icon.name" class="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
<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 type { PropType } from 'vue'
|
||||||
|
import Icon from '../elements/Icon.vue'
|
||||||
import { useTimer } from '../../composables/useTimer'
|
import { useTimer } from '../../composables/useTimer'
|
||||||
import { classNames } from '../../utils'
|
import { classNames } from '../../utils'
|
||||||
import type { ToastNotificationAction } from '../../types'
|
import type { ToastNotificationAction } from '../../types'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
v-bind="notification"
|
v-bind="notification"
|
||||||
:class="notification.click && 'cursor-pointer'"
|
:class="notification.click && 'cursor-pointer'"
|
||||||
@click="notification.click && notification.click(notification)"
|
@click="notification.click && notification.click(notification)"
|
||||||
@close="$toast.removeNotification(notification.id)"
|
@close="toast.removeNotification(notification.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,10 +18,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ToastNotification } from '../../types'
|
import type { ToastNotification } from '../../types'
|
||||||
|
import { useToast } from '../../composables/useToast'
|
||||||
import Notification from './Notification.vue'
|
import Notification from './Notification.vue'
|
||||||
import { useNuxtApp, useState } from '#imports'
|
import { useState } from '#imports'
|
||||||
|
|
||||||
const { $toast } = useNuxtApp()
|
const toast = useToast()
|
||||||
const notifications = useState<ToastNotification[]>('notifications', () => [])
|
const notifications = useState<ToastNotification[]>('notifications', () => [])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<div v-if="open" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
|
<div v-if="open" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
|
||||||
<transition appear v-bind="transitionClass">
|
<transition appear v-bind="transitionClass">
|
||||||
<PopoverPanel :class="baseClass" static>
|
<PopoverPanel :class="[baseClass, ringClass, roundedClass, shadowClass, backgroundClass]" static>
|
||||||
<slot name="panel" :open="open" :close="close" />
|
<slot name="panel" :open="open" :close="close" />
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -62,6 +62,22 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: () => $ui.popover.base
|
default: () => $ui.popover.base
|
||||||
},
|
},
|
||||||
|
backgroundClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.popover.background
|
||||||
|
},
|
||||||
|
shadowClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.popover.shadow
|
||||||
|
},
|
||||||
|
roundedClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.popover.rounded
|
||||||
|
},
|
||||||
|
ringClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.popover.ring
|
||||||
|
},
|
||||||
transitionClass: {
|
transitionClass: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => $ui.popover.transition
|
default: () => $ui.popover.transition
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
<transition appear v-bind="transitionClass">
|
<transition appear v-bind="transitionClass">
|
||||||
<div :class="[baseClass, backgroundClass, roundedClass, shadowClass, ringClass]">
|
<div :class="[baseClass, backgroundClass, roundedClass, shadowClass, ringClass]">
|
||||||
<slot name="text">
|
<slot name="text">
|
||||||
<span class="truncate">{{ text }}</span>
|
{{ text }}
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span v-if="shortcuts?.length" class="inline-flex items-center justify-end flex-shrink-0 gap-0.5 ml-1">
|
<span v-if="shortcuts?.length" :class="shortcutsClass">
|
||||||
<span class="mr-1 u-text-gray-700">·</span>
|
<span class="mr-1 u-text-gray-700">·</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">
|
<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 }}
|
{{ shortcut }}
|
||||||
@@ -84,6 +84,10 @@ const props = defineProps({
|
|||||||
type: Object as PropType<PopperOptions>,
|
type: Object as PropType<PopperOptions>,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
shortcutsClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.tooltip.shortcuts
|
||||||
|
},
|
||||||
openDelay: {
|
openDelay: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
|
|||||||
109
src/runtime/composables/defineShortcuts.ts
Normal file
109
src/runtime/composables/defineShortcuts.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import type { Ref, ComputedRef } from 'vue'
|
||||||
|
import { logicAnd, logicNot } from '@vueuse/math'
|
||||||
|
import { computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { useShortcuts } from './useShortcuts'
|
||||||
|
|
||||||
|
export interface ShortcutConfig {
|
||||||
|
handler: Function
|
||||||
|
usingInput?: string | boolean
|
||||||
|
whenever?: Ref<Boolean>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShortcutsConfig {
|
||||||
|
[key: string]: ShortcutConfig | Function
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Shortcut {
|
||||||
|
handler: Function
|
||||||
|
condition: ComputedRef<Boolean>
|
||||||
|
// KeyboardEvent attributes
|
||||||
|
key: string
|
||||||
|
ctrlKey: boolean
|
||||||
|
metaKey: boolean
|
||||||
|
shiftKey: boolean
|
||||||
|
altKey: boolean
|
||||||
|
// code?: string
|
||||||
|
// keyCode?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defineShortcuts = (config: ShortcutsConfig) => {
|
||||||
|
const { macOS, usingInput } = useShortcuts()
|
||||||
|
|
||||||
|
let shortcuts: Shortcut[] = []
|
||||||
|
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key)
|
||||||
|
|
||||||
|
for (const shortcut of shortcuts) {
|
||||||
|
if (e.key.toLowerCase() !== shortcut.key) { continue }
|
||||||
|
if (e.metaKey !== shortcut.metaKey) { continue }
|
||||||
|
if (e.ctrlKey !== shortcut.ctrlKey) { continue }
|
||||||
|
// shift modifier is only checked in combination with alphabetical keys
|
||||||
|
// (shift with non-alphabetical keys would change the key)
|
||||||
|
if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) { continue }
|
||||||
|
// alt modifier changes the combined key anyways
|
||||||
|
// if (e.altKey !== shortcut.altKey) { continue }
|
||||||
|
|
||||||
|
if (shortcut.condition.value) {
|
||||||
|
e.preventDefault()
|
||||||
|
shortcut.handler()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Map config to full detailled shortcuts
|
||||||
|
shortcuts = Object.entries(config).map(([key, shortcutConfig]) => {
|
||||||
|
if (!shortcutConfig) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse key and modifiers
|
||||||
|
const keySplit = key.toLowerCase().split('_').map(k => k)
|
||||||
|
let shortcut: Partial<Shortcut> = {
|
||||||
|
key: keySplit.filter(k => !['meta', 'ctrl', 'shift', 'alt'].includes(k)).join('_'),
|
||||||
|
metaKey: keySplit.includes('meta'),
|
||||||
|
ctrlKey: keySplit.includes('ctrl'),
|
||||||
|
shiftKey: keySplit.includes('shift'),
|
||||||
|
altKey: keySplit.includes('alt')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Meta to Ctrl for non-MacOS
|
||||||
|
if (!macOS.value && shortcut.metaKey && !shortcut.ctrlKey) {
|
||||||
|
shortcut.metaKey = false
|
||||||
|
shortcut.ctrlKey = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve handler function
|
||||||
|
if (typeof shortcutConfig === 'function') {
|
||||||
|
shortcut.handler = shortcutConfig
|
||||||
|
} else if (typeof shortcutConfig === 'object') {
|
||||||
|
shortcut = { ...shortcut, handler: shortcutConfig.handler }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shortcut.handler) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.trace('[Shortcut] Invalid value')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create shortcut computed
|
||||||
|
const conditions = []
|
||||||
|
if (!(shortcutConfig as ShortcutConfig).usingInput) {
|
||||||
|
conditions.push(logicNot(usingInput))
|
||||||
|
} else if (typeof (shortcutConfig as ShortcutConfig).usingInput === 'string') {
|
||||||
|
conditions.push(computed(() => usingInput.value === (shortcutConfig as ShortcutConfig).usingInput))
|
||||||
|
}
|
||||||
|
shortcut.condition = logicAnd(...conditions, ...((shortcutConfig as ShortcutConfig).whenever || []))
|
||||||
|
|
||||||
|
return shortcut as Shortcut
|
||||||
|
}).filter(Boolean) as Shortcut[]
|
||||||
|
|
||||||
|
document.addEventListener('keydown', onKeyDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('keydown', onKeyDown)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useClipboard } from '@vueuse/core'
|
import { useClipboard } from '@vueuse/core'
|
||||||
import { defineNuxtPlugin } from '#app'
|
import { useToast } from './useToast'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export function useCopyToClipboard () {
|
||||||
const { copy: copyToClipboard, isSupported } = useClipboard()
|
const { copy: copyToClipboard, isSupported } = useClipboard()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
function copy (text: string, success: { title?: string, description?: string } = {}, failure: { title?: string, description?: string } = {}) {
|
function copy (text: string, success: { title?: string, description?: string } = {}, failure: { title?: string, description?: string } = {}) {
|
||||||
if (!isSupported) {
|
if (!isSupported) {
|
||||||
@@ -14,9 +15,9 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nuxtApp.$toast.success(success)
|
toast.success(success)
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
nuxtApp.$toast.error({
|
toast.error({
|
||||||
...failure,
|
...failure,
|
||||||
description: failure.description || e.message
|
description: failure.description || e.message
|
||||||
})
|
})
|
||||||
@@ -24,10 +25,6 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provide: {
|
copy
|
||||||
clipboard: {
|
|
||||||
copy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
32
src/runtime/composables/useShortcuts.ts
Normal file
32
src/runtime/composables/useShortcuts.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createSharedComposable, useActiveElement } from '@vueuse/core'
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
|
export const _useShortcuts = () => {
|
||||||
|
const macOS = computed(() => process.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/))
|
||||||
|
|
||||||
|
const metaSymbol = ref(' ')
|
||||||
|
|
||||||
|
const activeElement = useActiveElement()
|
||||||
|
const usingInput = computed(() => {
|
||||||
|
const usingInput = !!(activeElement.value?.tagName === 'INPUT' || activeElement.value?.tagName === 'TEXTAREA' || activeElement.value?.contentEditable === 'true')
|
||||||
|
|
||||||
|
if (usingInput) {
|
||||||
|
return ((activeElement.value as any)?.name as string) || true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
metaSymbol.value = macOS.value ? '⌘' : 'Ctrl'
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
macOS,
|
||||||
|
metaSymbol,
|
||||||
|
activeElement,
|
||||||
|
usingInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useShortcuts = createSharedComposable(_useShortcuts)
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ref, computed } from 'vue-demi'
|
import { ref, computed } from 'vue-demi'
|
||||||
import { useTimestamp } from '@vueuse/core'
|
import { useTimestamp } from '@vueuse/core'
|
||||||
|
import type { UseTimestampOptions } from '@vueuse/core'
|
||||||
|
|
||||||
export function useTimer (cb: (...args: unknown[]) => any, interval: number) {
|
export function useTimer (cb: (...args: unknown[]) => any, interval: number, options: UseTimestampOptions<true> = { controls: true }) {
|
||||||
let timer: number | null = null
|
let timer: number | null = null
|
||||||
const timestamp = useTimestamp({ controls: true })
|
const timestamp = useTimestamp(options)
|
||||||
const startTime = ref<number | null>(null)
|
const startTime = ref<number | null>(null)
|
||||||
|
|
||||||
const remaining = computed(() => {
|
const remaining = computed(() => {
|
||||||
|
|||||||
41
src/runtime/composables/useToast.ts
Normal file
41
src/runtime/composables/useToast.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { ToastNotification } from '../types'
|
||||||
|
import { useState } from '#imports'
|
||||||
|
|
||||||
|
export function useToast () {
|
||||||
|
const notifications = useState<ToastNotification[]>('notifications', () => [])
|
||||||
|
|
||||||
|
function addNotification (notification: Partial<ToastNotification>) {
|
||||||
|
const body = {
|
||||||
|
id: new Date().getTime().toString(),
|
||||||
|
...notification
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = notifications.value.findIndex((n: ToastNotification) => n.id === body.id)
|
||||||
|
if (index === -1) {
|
||||||
|
notifications.value.push(body as ToastNotification)
|
||||||
|
}
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNotification (id: string) {
|
||||||
|
notifications.value = notifications.value.filter((n: ToastNotification) => n.id !== id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'success', ...notification })
|
||||||
|
|
||||||
|
const info = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'info', ...notification })
|
||||||
|
|
||||||
|
const warning = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'warning', ...notification })
|
||||||
|
|
||||||
|
const error = (notification: Partial<ToastNotification>) => addNotification({ type: 'error', title: 'An error occurred!', ...notification })
|
||||||
|
|
||||||
|
return {
|
||||||
|
addNotification,
|
||||||
|
removeNotification,
|
||||||
|
success,
|
||||||
|
info,
|
||||||
|
warning,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { defineNuxtPlugin, useState } from '#app'
|
|
||||||
import type { ToastNotification } from '../types'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
|
||||||
const notifications = useState<ToastNotification[]>('notifications', () => [])
|
|
||||||
|
|
||||||
function addNotification (notification: Partial<ToastNotification>) {
|
|
||||||
const body = {
|
|
||||||
id: new Date().getTime().toString(),
|
|
||||||
...notification
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = notifications.value.findIndex((n: ToastNotification) => n.id === body.id)
|
|
||||||
if (index === -1) {
|
|
||||||
notifications.value.push(body as ToastNotification)
|
|
||||||
}
|
|
||||||
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeNotification (id: string) {
|
|
||||||
notifications.value = notifications.value.filter((n: ToastNotification) => n.id !== id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
provide: {
|
|
||||||
toast: {
|
|
||||||
addNotification,
|
|
||||||
removeNotification,
|
|
||||||
success (notification: Partial<ToastNotification> = {}) {
|
|
||||||
return addNotification({ type: 'success', ...notification })
|
|
||||||
},
|
|
||||||
info (notification: Partial<ToastNotification> = {}) {
|
|
||||||
return addNotification({ type: 'info', ...notification })
|
|
||||||
},
|
|
||||||
warning (notification: Partial<ToastNotification> = {}) {
|
|
||||||
return addNotification({ type: 'warning', ...notification })
|
|
||||||
},
|
|
||||||
error (notification: Partial<ToastNotification>) {
|
|
||||||
return addNotification({ type: 'error', title: 'An error occurred!', ...notification })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -48,7 +48,7 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
base: 'flex-shrink-0',
|
base: 'flex-shrink-0',
|
||||||
loading: 'heroicons-outline:refresh',
|
loading: 'i-heroicons-arrow-path',
|
||||||
size: {
|
size: {
|
||||||
xxs: 'h-3.5 w-3.5',
|
xxs: 'h-3.5 w-3.5',
|
||||||
xs: 'h-4 w-4',
|
xs: 'h-4 w-4',
|
||||||
@@ -168,7 +168,7 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
base: 'u-text-gray-400',
|
base: 'u-text-gray-400',
|
||||||
loading: 'heroicons-outline:refresh',
|
loading: 'i-heroicons-arrow-path',
|
||||||
size: {
|
size: {
|
||||||
xxs: 'h-3 w-3',
|
xxs: 'h-3 w-3',
|
||||||
xs: 'h-4 w-4',
|
xs: 'h-4 w-4',
|
||||||
@@ -215,7 +215,7 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
wrapper: 'relative',
|
wrapper: 'relative',
|
||||||
base: `${select.base} text-left cursor-default`,
|
base: `${select.base} text-left cursor-default`,
|
||||||
icon: {
|
icon: {
|
||||||
name: 'heroicons-solid:selector',
|
name: 'i-heroicons-chevron-up-down-20-solid',
|
||||||
...select.icon
|
...select.icon
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
@@ -233,7 +233,7 @@ export default function defaultPreset (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',
|
name: 'i-heroicons-check-20-solid',
|
||||||
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',
|
||||||
@@ -355,18 +355,23 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
|
|
||||||
const alertDialog = {
|
const alertDialog = {
|
||||||
icon: {
|
icon: {
|
||||||
wrapper: 'h-12 w-12 sm:h-10 sm:w-10 bg-primary-100',
|
wrapper: 'mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-primary-100 sm:mx-0 sm:h-10 sm:w-10',
|
||||||
base: 'text-primary-600'
|
base: 'h-6 w-6 text-primary-600'
|
||||||
},
|
},
|
||||||
title: 'text-lg leading-6 font-medium text-gray-900',
|
title: 'text-lg leading-6 font-medium u-text-gray-900',
|
||||||
description: 'text-sm text-gray-500'
|
description: 'text-sm u-text-gray-500'
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropdown = {
|
const dropdown = {
|
||||||
wrapper: 'relative inline-flex text-left',
|
wrapper: 'relative inline-flex text-left',
|
||||||
container: 'z-20',
|
container: 'z-20',
|
||||||
width: 'w-48',
|
width: 'w-48',
|
||||||
base: 'u-bg-white divide-y u-divide-gray-100 rounded-md ring-1 u-ring-gray-200 shadow-lg focus:outline-none',
|
background: 'u-bg-white',
|
||||||
|
shadow: 'shadow-lg',
|
||||||
|
rounded: 'rounded-md',
|
||||||
|
ring: 'ring-1 u-ring-gray-200',
|
||||||
|
base: 'focus:outline-none',
|
||||||
|
divide: 'divide-y u-divide-gray-100',
|
||||||
group: 'py-1',
|
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',
|
||||||
@@ -375,7 +380,7 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
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'
|
shortcuts: 'hidden md:inline-flex flex-shrink-0 text-xs font-semibold u-text-gray-500 ml-auto'
|
||||||
},
|
},
|
||||||
transition: {
|
transition: {
|
||||||
enterActiveClass: 'transition duration-100 ease-out',
|
enterActiveClass: 'transition duration-100 ease-out',
|
||||||
@@ -483,10 +488,10 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
rounded: 'rounded-lg',
|
rounded: 'rounded-lg',
|
||||||
ring: 'ring-1 u-ring-gray-200',
|
ring: 'ring-1 u-ring-gray-200',
|
||||||
type: {
|
type: {
|
||||||
info: 'heroicons-outline:information-circle',
|
info: 'i-heroicons-information-circle',
|
||||||
success: 'heroicons-outline:check-circle',
|
success: 'i-heroicons-check-circle',
|
||||||
warning: 'heroicons-outline:exclamation-circle',
|
warning: 'i-heroicons-exclamation-circle',
|
||||||
error: 'heroicons-outline:x-circle'
|
error: 'i-heroicons-x-circle'
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
base: 'w-6 h-6',
|
base: 'w-6 h-6',
|
||||||
@@ -497,6 +502,11 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
error: 'text-red-400'
|
error: 'text-red-400'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
close: {
|
||||||
|
icon: {
|
||||||
|
name: 'i-heroicons-x-mark-20-solid'
|
||||||
|
}
|
||||||
|
},
|
||||||
transition: {
|
transition: {
|
||||||
enterActiveClass: 'transform ease-out duration-300 transition',
|
enterActiveClass: 'transform ease-out duration-300 transition',
|
||||||
enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2',
|
enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2',
|
||||||
@@ -515,7 +525,8 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
shadow: 'shadow',
|
shadow: 'shadow',
|
||||||
rounded: 'rounded',
|
rounded: 'rounded',
|
||||||
ring: 'ring-1 u-ring-gray-200',
|
ring: 'ring-1 u-ring-gray-200',
|
||||||
base: 'invisible lg:visible h-6 px-2 py-1 text-xs font-normal',
|
base: 'invisible lg:visible h-6 px-2 py-1 text-xs font-normal truncate',
|
||||||
|
shortcuts: 'hidden md:inline-flex items-center justify-end flex-shrink-0 gap-0.5 ml-1',
|
||||||
transition: {
|
transition: {
|
||||||
enterActiveClass: 'transition ease-out duration-200',
|
enterActiveClass: 'transition ease-out duration-200',
|
||||||
enterFromClass: 'opacity-0 translate-y-1',
|
enterFromClass: 'opacity-0 translate-y-1',
|
||||||
@@ -533,7 +544,11 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
wrapper: 'relative',
|
wrapper: 'relative',
|
||||||
container: 'z-20',
|
container: 'z-20',
|
||||||
width: '',
|
width: '',
|
||||||
base: '',
|
background: 'u-bg-white',
|
||||||
|
shadow: 'shadow-lg',
|
||||||
|
rounded: 'rounded-md',
|
||||||
|
ring: 'ring-1 u-ring-gray-200',
|
||||||
|
base: 'overflow-hidden focus:outline-none',
|
||||||
transition: {
|
transition: {
|
||||||
enterActiveClass: 'transition ease-out duration-200',
|
enterActiveClass: 'transition ease-out duration-200',
|
||||||
enterFromClass: 'opacity-0 translate-y-1',
|
enterFromClass: 'opacity-0 translate-y-1',
|
||||||
@@ -551,7 +566,11 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
wrapper: 'relative',
|
wrapper: 'relative',
|
||||||
container: 'z-20',
|
container: 'z-20',
|
||||||
width: '',
|
width: '',
|
||||||
base: '',
|
background: 'u-bg-white',
|
||||||
|
shadow: 'shadow-lg',
|
||||||
|
rounded: 'rounded-md',
|
||||||
|
ring: 'ring-1 u-ring-gray-200',
|
||||||
|
base: 'overflow-hidden focus:outline-none',
|
||||||
transition: {
|
transition: {
|
||||||
enterActiveClass: 'transition ease-out duration-200',
|
enterActiveClass: 'transition ease-out duration-200',
|
||||||
enterFromClass: 'opacity-0 translate-y-1',
|
enterFromClass: 'opacity-0 translate-y-1',
|
||||||
@@ -572,7 +591,7 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
base: 'w-full h-12 pr-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 pl-[3.25rem] u-text-gray-900 focus:ring-0 sm:text-sm',
|
base: 'w-full h-12 pr-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 pl-[3.25rem] u-text-gray-900 focus:ring-0 sm:text-sm',
|
||||||
icon: {
|
icon: {
|
||||||
base: 'pointer-events-none absolute top-3.5 left-5 h-5 w-5 u-text-gray-400',
|
base: 'pointer-events-none absolute top-3.5 left-5 h-5 w-5 u-text-gray-400',
|
||||||
name: 'heroicons-outline:search'
|
name: 'i-heroicons-magnifying-glass'
|
||||||
},
|
},
|
||||||
close: {
|
close: {
|
||||||
base: 'absolute right-2',
|
base: 'absolute right-2',
|
||||||
@@ -585,8 +604,16 @@ export default function defaultPreset (variantColors: string[]) {
|
|||||||
},
|
},
|
||||||
empty: {
|
empty: {
|
||||||
icon: {
|
icon: {
|
||||||
name: 'heroicons-outline:search'
|
name: 'i-heroicons-magnifying-glass'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
option: {
|
||||||
|
selected: {
|
||||||
|
icon: {
|
||||||
|
name: 'i-heroicons-check-20-solid'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shortcuts: 'hidden md:inline-flex flex-shrink-0 text-xs font-semibold u-text-gray-500'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
src/runtime/types/avatar.d.ts
vendored
3
src/runtime/types/avatar.d.ts
vendored
@@ -4,7 +4,6 @@ export interface Avatar {
|
|||||||
text: string
|
text: string
|
||||||
size: string
|
size: string
|
||||||
rounded: boolean
|
rounded: boolean
|
||||||
chip: boolean
|
chip: string
|
||||||
chipVariant: string
|
|
||||||
chipPosition: string
|
chipPosition: string
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/runtime/types/command-palette.d.ts
vendored
2
src/runtime/types/command-palette.d.ts
vendored
@@ -1,6 +1,7 @@
|
|||||||
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
|
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
|
||||||
import type { FuseSortFunctionMatch, FuseSortFunctionMatchList } from 'fuse.js'
|
import type { FuseSortFunctionMatch, FuseSortFunctionMatchList } from 'fuse.js'
|
||||||
import type { Avatar } from './avatar'
|
import type { Avatar } from './avatar'
|
||||||
|
|
||||||
export interface Command {
|
export interface Command {
|
||||||
id: string | number
|
id: string | number
|
||||||
prefix?: string
|
prefix?: string
|
||||||
@@ -22,6 +23,5 @@ export interface Group {
|
|||||||
active?: string
|
active?: string
|
||||||
inactive?: string
|
inactive?: string
|
||||||
commands: Command[]
|
commands: Command[]
|
||||||
options?: Partial<UseFuseOptions<Command>>
|
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/runtime/types/toast.d.ts
vendored
7
src/runtime/types/toast.d.ts
vendored
@@ -14,10 +14,3 @@ export interface ToastNotification {
|
|||||||
click?: Function
|
click?: Function
|
||||||
callback?: Function
|
callback?: Function
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToastPlugin {
|
|
||||||
addNotification: (notification: Partial<Notification>) => Notification
|
|
||||||
removeNotification: (id: string) => void
|
|
||||||
success: (options: { title?: string, description?: string }) => void
|
|
||||||
error: (options: { title?: string, description?: string }) => void
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user