Compare commits

...

43 Commits

Author SHA1 Message Date
Benjamin Canac
10a9a3ea2b chore(release): 2.5.0 2023-06-27 17:05:34 +02:00
Benjamin Canac
1ff11ac1a3 feat(Table): reset sort on third click
Resolves #300
2023-06-27 15:32:19 +02:00
Benjamin Canac
07b27a228d fix(Table): default sortButton icon
Fixes 0f3fe0d54e
2023-06-27 15:31:31 +02:00
Benjamin Canac
6be9290f68 fix(FormGroup): prevent overriding color of children
Resolves #352
2023-06-27 15:17:53 +02:00
Benjamin Canac
0f3fe0d54e fix(Table): missing default sort icon when overriding sort-button prop 2023-06-27 15:11:32 +02:00
Benjamin Canac
0815f688ed chore(deps): bump 2023-06-27 14:34:07 +02:00
Benjamin Canac
8399ffe1f1 docs(installation): add IntelliSense section 2023-06-27 13:14:07 +02:00
Haytham A. Salama
91f6103719 docs: add support for RTL and LTR (#348) 2023-06-27 12:42:41 +02:00
Haytham A. Salama
278a1ea93c chore: improve RTL support (#334)
Co-authored-by: Hassan Kadhim <hassan57905@gmail.com>
2023-06-23 23:19:28 +02:00
Benjamin Canac
3f27c0ccae chore(deps): revert node engines bump 2023-06-23 22:43:37 +02:00
renovate[bot]
5a2f46683a chore(deps): update all non-major dependencies (#136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-23 22:36:35 +02:00
Benjamin Canac
c8a0005253 chore(github): use pnpm 8 2023-06-23 21:35:18 +02:00
Haytham A. Salama
881f3547f2 docs(ComponentCard): preview component only (#302) 2023-06-22 18:38:04 +02:00
Benjamin Canac
8c99b871e2 docs(Avatar): add edge badge on chip-text prop 2023-06-22 14:53:45 +02:00
Benjamin Canac
41b85d50a8 fix(components): prefix @headlessui/vue components
Resolves #315
2023-06-22 13:01:58 +02:00
JPB
759af058df feat(Avatar): handle chipText (#306)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-06-22 12:24:56 +02:00
Benjamin Canac
48636363d1 chore(release-it): add header to changelog 2023-06-21 19:09:11 +02:00
Hassan Kadhim
4ea114a4d6 feat: RTL support (#320) 2023-06-21 19:09:11 +02:00
Benjamin Canac
ad2349e570 chore(deps): bump 2023-06-21 19:09:11 +02:00
Haytham A. Salama
ffb312d34d feat(Radio/Checkbox/Toggle)!: handle color prop for form elements (#323)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-06-21 19:09:11 +02:00
TomSmith27
97a1c86433 feat(Range): new component (#290)
Co-authored-by: Tom Smith <tom.smith2711@gmail.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
Co-authored-by: Tom Smith <tom.smith@qunifi.com>
2023-06-21 19:09:11 +02:00
Benjamin Canac
c2ebb0416e fix(Toggle): add opacity-50 when disabled 2023-06-21 19:09:01 +02:00
Alex Liu
e1548062c7 chore(utils): types (#321) 2023-06-21 19:09:01 +02:00
Benjamin Canac
9cd73aa49d fix(defineShortcuts): missing useDebounceFn import 2023-06-21 19:09:01 +02:00
Benjamin Canac
a880379480 fix(defineShortcuts): missing ref import 2023-06-21 19:09:01 +02:00
TomSmith27
71c2465d7b feat(Table): pass row index to table cell (#291)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-06-21 19:08:51 +02:00
Benjamin Canac
0272307f28 docs: improve notification page 2023-06-21 19:08:51 +02:00
Benjamin Canac
2ea358703e docs: fix focus on raycast command palette example 2023-06-21 19:08:51 +02:00
Benjamin Canac
c458f388bb docs: improve with examples 2023-06-21 19:08:51 +02:00
Benjamin Canac
1b03b8a531 fix(Tooltip): add color in config 2023-06-21 19:08:40 +02:00
Benjamin Canac
e2f7d82d62 docs: disable documentDriven mode 2023-06-21 19:08:40 +02:00
Benjamin Canac
c8e6ed8df9 chore(deps): bump 2023-06-21 19:08:40 +02:00
Sylvain Marroufin
a67f691a00 feat(defineShortcuts): chained shortcuts + docs update (#282)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-06-21 19:08:32 +02:00
Benjamin Canac
dfccbcf1a9 docs: add missing icons to override in theming 2023-06-21 17:59:30 +02:00
Benjamin Canac
38ecb088ec docs: improve theming dark mode section 2023-06-21 17:59:30 +02:00
Benjamin Canac
8236b18d0d docs: edge badges to new following 2.4.0 2023-06-21 17:59:30 +02:00
Benjamin Canac
1e05b0f072 chore(release): 2.4.1 2023-06-21 17:53:38 +02:00
Benjamin Canac
87e98f038a chore(deps): migrate from standard-version to release-it 2023-06-21 17:53:21 +02:00
Benjamin Canac
f7e2082983 fix(module): safelist regex when a : was present before color
Also prevents parsing colors already safelisted initially.
2023-06-21 17:42:02 +02:00
Benjamin Canac
f719111abb fix(module): safelist aliases for input
To make it work when doing `<USelect color="yellow" />` for example
2023-06-21 17:41:51 +02:00
Benjamin Canac
3bac0874f1 fix(Radio/Checkbox): remove legacy custom 2023-06-21 17:41:39 +02:00
Selemondev
457b7a9fb7 fix(forms): precise type assertion for onInput event handler (#293)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-06-21 17:41:15 +02:00
Benjamin Canac
4023fbec29 fix(module): let tailwindcss viewer enabled by default
Resolves #292
2023-06-21 17:40:54 +02:00
86 changed files with 4218 additions and 1251 deletions

View File

@@ -2,6 +2,9 @@ module.exports = {
root: true, root: true,
extends: ['@nuxt/eslint-config'], extends: ['@nuxt/eslint-config'],
rules: { rules: {
'semi': ['error', 'never'],
'comma-dangle': ['error', 'never'],
'space-before-function-paren': ['error', 'always'],
'vue/multi-word-component-names': 0, 'vue/multi-word-component-names': 0,
'vue/max-attributes-per-line': ['error', { 'vue/max-attributes-per-line': ['error', {
singleline: { singleline: {

View File

@@ -29,7 +29,7 @@ jobs:
name: Install pnpm name: Install pnpm
id: pnpm-install id: pnpm-install
with: with:
version: 7 version: 8
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory

View File

@@ -29,7 +29,7 @@ jobs:
name: Install pnpm name: Install pnpm
id: pnpm-install id: pnpm-install
with: with:
version: 7 version: 8
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ dist
.DS_Store .DS_Store
.history .history
.vercel .vercel
.idea

6
.prettierrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}

23
.release-it.json Normal file
View File

@@ -0,0 +1,23 @@
{
"git": {
"commitMessage": "chore(release): ${version}"
},
"npm": {
"publish": false
},
"github": {
"release": true,
"web": true
},
"hooks": {
"before:init": ["pnpm lint"]
},
"plugins": {
"@release-it/conventional-changelog": {
"preset": "conventionalcommits",
"infile": "CHANGELOG.md",
"header": "# Changelog",
"ignoreRecommendedBump": true
}
}
}

View File

@@ -1,6 +1,45 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. ## [2.5.0](https://github.com/nuxtlabs/ui/compare/v2.4.1...v2.5.0) (2023-06-27)
### ⚠ BREAKING CHANGES
* **Radio/Checkbox/Toggle:** handle `color` prop for form elements (#323)
### Features
* **Avatar:** handle `chipText` ([#306](https://github.com/nuxtlabs/ui/issues/306)) ([759af05](https://github.com/nuxtlabs/ui/commit/759af058df636f55a54326b21ebb1c315c73c26b))
* **defineShortcuts:** chained shortcuts + docs update ([#282](https://github.com/nuxtlabs/ui/issues/282)) ([a67f691](https://github.com/nuxtlabs/ui/commit/a67f691a0066e4d017f580388df31b22d1c45372))
* **Radio/Checkbox/Toggle:** handle `color` prop for form elements ([#323](https://github.com/nuxtlabs/ui/issues/323)) ([ffb312d](https://github.com/nuxtlabs/ui/commit/ffb312d34dfc2ac7a7aabdcbdf9ddb1d04d8a66f))
* **Range:** new component ([#290](https://github.com/nuxtlabs/ui/issues/290)) ([97a1c86](https://github.com/nuxtlabs/ui/commit/97a1c8643314d5ff950b122f46f31b206485cd50))
* RTL support ([#320](https://github.com/nuxtlabs/ui/issues/320)) ([4ea114a](https://github.com/nuxtlabs/ui/commit/4ea114a4d6b11277674c121130f746927045ade3))
* **Table:** pass row index to table cell ([#291](https://github.com/nuxtlabs/ui/issues/291)) ([71c2465](https://github.com/nuxtlabs/ui/commit/71c2465d7be78cfb0e274b107aceda9de5384fb7))
* **Table:** reset sort on third click ([1ff11ac](https://github.com/nuxtlabs/ui/commit/1ff11ac1a3eff537a4ee854a049668f312f1d415)), closes [#300](https://github.com/nuxtlabs/ui/issues/300)
### Bug Fixes
* **components:** prefix `@headlessui/vue` components ([41b85d5](https://github.com/nuxtlabs/ui/commit/41b85d50a865cfe4aa0f06a62f5209358422eaec)), closes [#315](https://github.com/nuxtlabs/ui/issues/315)
* **defineShortcuts:** missing `ref` import ([a880379](https://github.com/nuxtlabs/ui/commit/a8803794802c4032f703a0a0a6343a8204b19bc8))
* **defineShortcuts:** missing `useDebounceFn` import ([9cd73aa](https://github.com/nuxtlabs/ui/commit/9cd73aa49d1dd43bac8ec71932b850bdcb375fcf))
* **FormGroup:** prevent overriding `color` of children ([6be9290](https://github.com/nuxtlabs/ui/commit/6be9290f689c449b6a6435a3ef25e89a106e1c06)), closes [#352](https://github.com/nuxtlabs/ui/issues/352)
* **Table:** default `sortButton` icon ([07b27a2](https://github.com/nuxtlabs/ui/commit/07b27a228d293655368825979a6ca0bc1dd6e51a))
* **Table:** missing default sort icon when overriding `sort-button` prop ([0f3fe0d](https://github.com/nuxtlabs/ui/commit/0f3fe0d54ef8b45a046b84ceb31ae55a26e153fb))
* **Toggle:** add `opacity-50` when disabled ([c2ebb04](https://github.com/nuxtlabs/ui/commit/c2ebb0416eb2c92b759be5a4bf0d219031889b4b))
* **Tooltip:** add `color` in config ([1b03b8a](https://github.com/nuxtlabs/ui/commit/1b03b8a531d397871e0df4f8574d7f47ac4ec610))
### [2.4.1](https://github.com/nuxtlabs/ui/compare/v2.4.0...v2.4.1) (2023-06-21)
### Bug Fixes
* **forms:** precise type assertion for `onInput` event handler ([#293](https://github.com/nuxtlabs/ui/issues/293)) ([457b7a9](https://github.com/nuxtlabs/ui/commit/457b7a9fb72e6469014b6ca18e7034dd5c6f44b8))
* **module:** let `tailwindcss` viewer enabled by default ([4023fbe](https://github.com/nuxtlabs/ui/commit/4023fbec29e5b4d40fd23e8c2ae3d0cf23addc64)), closes [#292](https://github.com/nuxtlabs/ui/issues/292)
* **module:** safelist aliases for input ([f719111](https://github.com/nuxtlabs/ui/commit/f719111abb94c81f3932927a0154b3e1bed73a9a))
* **module:** safelist regex when a `:` was present before color ([f7e2082](https://github.com/nuxtlabs/ui/commit/f7e2082983c2eb650e95a9040aafde4ce2c88c54))
* **Radio/Checkbox:** remove legacy `custom` ([3bac087](https://github.com/nuxtlabs/ui/commit/3bac0874f106a8ff7436b541f9d064c1c7c27464))
## [2.4.0](https://github.com/nuxtlabs/ui/compare/v2.3.0...v2.4.0) (2023-06-13) ## [2.4.0](https://github.com/nuxtlabs/ui/compare/v2.3.0...v2.4.0) (2023-06-13)

View File

@@ -14,6 +14,7 @@ This module has been developed by [NuxtLabs](https://nuxtlabs.com/) for [Volta](
- Built with [Headless UI](https://headlessui.dev/) and [Tailwind CSS](https://tailwindcss.com/) - Built with [Headless UI](https://headlessui.dev/) and [Tailwind CSS](https://tailwindcss.com/)
- HMR support through Nuxt App Config - HMR support through Nuxt App Config
- Dark mode support - Dark mode support
- Support for LTR and RTL languages
- Keyboard shortcuts - Keyboard shortcuts
- Bundled icons - Bundled icons
- Fully typed - Fully typed

View File

@@ -2,25 +2,11 @@
<div> <div>
<Header /> <Header />
<UContainer> <UContainer class="grid lg:grid-cols-10 lg:gap-8">
<div class="relative grid lg:grid-cols-10 lg:gap-8"> <DocsAside class="lg:col-span-2" />
<DocsAside class="lg:col-span-2" />
<div class="relative pt-8 pb-16" :class="[toc ? 'lg:col-span-6' : 'lg:col-span-8']"> <div class="lg:col-span-8 min-h-0 flex flex-col">
<DocsPageHeader /> <NuxtPage />
<NuxtPage />
<DocsPageFooter class="mt-12" />
<hr class="border-gray-200 dark:border-gray-800 my-6">
<DocsPrevNext />
<DocsFooter class="mt-16" />
</div>
<DocsToc v-if="toc" class="lg:col-span-2 order-first lg:order-last" />
</div> </div>
</UContainer> </UContainer>
@@ -33,9 +19,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { toc } = useContent()
const colorMode = useColorMode() const colorMode = useColorMode()
const { data: navigation } = await useAsyncData('navigation', () => fetchContentNavigation())
provide('navigation', navigation)
// Computed // Computed
const color = computed(() => colorMode.value === 'dark' ? '#18181b' : 'white') const color = computed(() => colorMode.value === 'dark' ? '#18181b' : 'white')
@@ -56,7 +45,7 @@ useHead({
lang: 'en' lang: 'en'
}, },
bodyAttrs: { bodyAttrs: {
class: 'antialiased font-sans text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-900' class: 'antialiased font-sans text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-900'
} }
}) })

View File

@@ -0,0 +1,46 @@
<template>
<div class="p-2">
<div class="grid grid-cols-5 gap-px">
<ColorPickerButton v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" />
</div>
<hr class="border-gray-200 dark:border-gray-800 my-2">
<div class="grid grid-cols-5 gap-px">
<ColorPickerButton v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" />
</div>
</div>
</template>
<script setup lang="ts">
import colors from '#tailwind-config/theme/colors'
const appConfig = useAppConfig()
const colorMode = useColorMode()
// Computed
const primaryColors = computed(() => useWithout(appConfig.ui.colors, 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const primary = computed({
get () {
return primaryColors.value.find(option => option.value === appConfig.ui.primary)
},
set (option) {
appConfig.ui.primary = option.value
window.localStorage.setItem('nuxt-ui-primary', appConfig.ui.primary)
}
})
const grayColors = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const gray = computed({
get () {
return grayColors.value.find(option => option.value === appConfig.ui.gray)
},
set (option) {
appConfig.ui.gray = option.value
window.localStorage.setItem('nuxt-ui-gray', appConfig.ui.gray)
}
})
</script>

View File

@@ -0,0 +1,25 @@
<template>
<UTooltip :text="color.value" class="capitalize" :open-delay="500">
<UButton
color="gray"
square
:ui="{
color: {
gray: {
solid: 'bg-gray-100 dark:bg-gray-800',
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
}
}
}"
:variant="color.value === selected.value ? 'solid' : 'ghost'"
@click.stop.prevent="$emit('select')"
>
<span class="inline-block w-3 h-3 rounded-full" :style="{ backgroundColor: color.hex }" />
</UButton>
</UTooltip>
</template>
<script setup lang="ts">
defineProps<{ color: { value: string, hex: string }, selected: { value: string} }>()
defineEmits(['select'])
</script>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import { DatePicker as VCalendarDatePicker } from 'v-calendar'
import 'v-calendar/dist/style.css'
const props = defineProps({
modelValue: {
type: Date,
default: null
}
})
const emit = defineEmits(['update:model-value', 'close'])
const colorMode = useColorMode()
const isDark = computed(() => colorMode.value === 'dark')
const date = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:model-value', value)
emit('close')
}
})
const attrs = [{
key: 'today',
highlight: {
color: 'blue',
fillMode: 'outline',
class: '!bg-gray-100 dark:!bg-gray-800'
},
dates: new Date()
}]
</script>
<template>
<VCalendarDatePicker
v-model="date"
transparent
borderless
:attributes="attrs"
:is-dark="isDark"
title-position="left"
trim-weeks
:first-day-of-week="2"
/>
</template>
<style>
:root {
--vc-gray-50: rgb(var(--color-gray-50));
--vc-gray-100: rgb(var(--color-gray-100));
--vc-gray-200: rgb(var(--color-gray-200));
--vc-gray-300: rgb(var(--color-gray-300));
--vc-gray-400: rgb(var(--color-gray-400));
--vc-gray-500: rgb(var(--color-gray-500));
--vc-gray-600: rgb(var(--color-gray-600));
--vc-gray-700: rgb(var(--color-gray-700));
--vc-gray-800: rgb(var(--color-gray-800));
--vc-gray-900: rgb(var(--color-gray-900));
}
.vc-blue {
--vc-accent-50: rgb(var(--color-primary-50));
--vc-accent-100: rgb(var(--color-primary-100));
--vc-accent-200: rgb(var(--color-primary-200));
--vc-accent-300: rgb(var(--color-primary-300));
--vc-accent-400: rgb(var(--color-primary-400));
--vc-accent-500: rgb(var(--color-primary-500));
--vc-accent-600: rgb(var(--color-primary-600));
--vc-accent-700: rgb(var(--color-primary-700));
--vc-accent-800: rgb(var(--color-primary-800));
--vc-accent-900: rgb(var(--color-primary-900));
}
</style>

View File

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

View File

@@ -0,0 +1,64 @@
<template>
<div class="flex items-center justify-between gap-3 h-16">
<div class="flex items-center gap-6">
<div class="flex items-center gap-3">
<NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
<span class="hidden sm:block">NuxtLabs</span><span class="sm:text-primary-500 dark:sm:text-primary-400">UI</span>
</NuxtLink>
</div>
</div>
<div class="flex items-center justify-end flex-1 -mr-1.5 gap-3">
<DocsSearchButton class="ml-1.5 flex-1 lg:flex-none lg:w-48" />
<div class="flex items-center lg:gap-1.5">
<UPopover>
<template #default="{ open }">
<UButton color="gray" variant="ghost" square :class="[open && 'bg-gray-50 dark:bg-gray-800']">
<UIcon name="i-heroicons-swatch-20-solid" class="w-5 h-5 text-primary-500 dark:text-primary-400" />
</UButton>
</template>
<template #panel>
<ColorPicker />
</template>
</UPopover>
<ColorModeButton />
<UButton
to="https://twitter.com/nuxtlabs"
target="_blank"
color="gray"
variant="ghost"
icon="i-simple-icons-twitter"
/>
<UButton
to="https://github.com/nuxtlabs/ui"
target="_blank"
color="gray"
variant="ghost"
icon="i-simple-icons-github"
/>
<UButton
color="gray"
variant="ghost"
class="lg:hidden"
:icon="isDialogOpen ? 'i-heroicons-x-mark-20-solid' : 'i-heroicons-bars-3-20-solid'"
@click="isDialogOpen = !isDialogOpen"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ modelValue: boolean, links: { to: string, label: string }[] }>()
const emit = defineEmits(['update:modelValue'])
const isDialogOpen = useVModel(props, 'modelValue', emit)
</script>

View File

@@ -1,128 +0,0 @@
<template>
<ClientOnly>
<div class="inline-flex shadow-sm rounded-md">
<USelectMenu
v-model="primary"
name="primary"
class="!rounded-r-none !shadow-none focus:z-[1]"
color="gray"
:ui="{ width: 'w-[194px]' }"
:popper="{ placement: 'bottom-start' }"
:options="primaryOptions"
>
<template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${primary.hex}`}" />
{{ primary.text }}
</template>
<template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
{{ option.text }}
</template>
</USelectMenu>
<USelectMenu
v-model="gray"
name="gray"
class="!rounded-l-none !shadow-none"
color="gray"
:ui="{ width: 'w-[194px]', wrapper: '-ml-px' }"
:popper="{ placement: 'bottom-end' }"
:options="grayOptions"
>
<template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${gray.hex}`}" />
{{ gray.text }}
</template>
<template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
{{ option.text }}
</template>
</USelectMenu>
</div>
</ClientOnly>
</template>
<script setup lang="ts">
import colors from '#tailwind-config/theme/colors'
const appConfig = useAppConfig()
const colorMode = useColorMode()
const primaryCookie = useCookie('primary', { path: '/', default: () => appConfig.ui.primary })
const grayCookie = useCookie('gray', { path: '/', default: () => appConfig.ui.gray })
watch(primaryCookie, (primary) => {
appConfig.ui.primary = primary
}, { immediate: true })
watch(grayCookie, (gray) => {
appConfig.ui.gray = gray
}, { immediate: true })
// Computed
const primaryOptions = computed(() => useWithout(appConfig.ui.colors, 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const primary = computed({
get () {
return primaryOptions.value.find(option => option.value === primaryCookie.value) || primaryOptions.value.find(option => option.value === 'green')
},
set (option) {
primaryCookie.value = option.value
}
})
const grayOptions = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const gray = computed({
get () {
return grayOptions.value.find(option => option.value === grayCookie.value) || grayOptions.value.find(option => option.value === 'cool')
},
set (option) {
grayCookie.value = option.value
}
})
// Hack for SSG
const hexToRgb = (hex) => {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
hex = hex.replace(shorthandRegex, function (_, r, g, b) {
return r + r + g + g + b + b
})
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}`
: null
}
const root = computed(() => {
return `:root {
${Object.entries(colors[primary.value.value] || colors.green).map(([key, value]) => `--color-primary-${key}: ${hexToRgb(value)};`).join('\n')}
${Object.entries(colors[gray.value.value] || colors.cool).map(([key, value]) => `--color-gray-${key}: ${hexToRgb(value)};`).join('\n')}
}`
})
if (process.client) {
watch(root, () => {
window.localStorage.setItem('nuxt-ui-root', root.value)
}, { immediate: true })
}
if (process.server) {
useHead({
script: [
{
innerHTML: `
if (localStorage.getItem('nuxt-ui-root')) {
document.querySelector('style#nuxt-ui-colors').innerHTML = localStorage.getItem('nuxt-ui-root')
}`.replace(/\s+/g, ' '),
type: 'text/javascript',
tagPriority: -1
}
]
})
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<ClientOnly>
<UButton
:icon="isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'"
color="gray"
variant="ghost"
aria-label="Theme"
@click="isDark = !isDark"
/>
<template #fallback>
<div class="w-8 h-8" />
</template>
</ClientOnly>
</template>
<script setup lang="ts">
const colorMode = useColorMode()
const isDark = computed({
get () {
return colorMode.value === 'dark'
},
set () {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
</script>

View File

@@ -7,7 +7,7 @@
v-if="prop.type === 'boolean'" v-if="prop.type === 'boolean'"
v-model="componentProps[prop.name]" v-model="componentProps[prop.name]"
:name="`prop-${prop.name}`" :name="`prop-${prop.name}`"
variant="none" tabindex="-1"
:ui="{ wrapper: 'relative flex items-start justify-center' }" :ui="{ wrapper: 'relative flex items-start justify-center' }"
/> />
<USelectMenu <USelectMenu
@@ -18,6 +18,7 @@
variant="none" variant="none"
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md', wrapper: 'relative inline-flex' }" :ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md', wrapper: 'relative inline-flex' }"
class="!py-0" class="!py-0"
tabindex="-1"
:popper="{ strategy: 'fixed', placement: 'bottom-start' }" :popper="{ strategy: 'fixed', placement: 'bottom-start' }"
/> />
<UInput <UInput
@@ -28,6 +29,7 @@
variant="none" variant="none"
autocomplete="off" autocomplete="off"
class="!py-0" class="!py-0"
tabindex="-1"
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val" @update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
/> />
</div> </div>
@@ -45,7 +47,7 @@
</component> </component>
</div> </div>
<ContentRenderer :value="ast" class="[&>div>pre]:!rounded-t-none" /> <ContentRenderer v-if="!previewOnly" :value="ast" class="[&>div>pre]:!rounded-t-none" />
</div> </div>
</template> </template>
@@ -98,6 +100,10 @@ const props = defineProps({
overflowClass: { overflowClass: {
type: String, type: String,
default: '' default: ''
},
previewOnly: {
type: Boolean,
default: false
} }
}) })

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
const selected = ref(false) const selected = ref(true)
</script> </script>
<template> <template>

View File

@@ -0,0 +1,16 @@
<script setup>
const date = ref(new Date())
const label = computed(() => date.value.toLocaleDateString('en-us', { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric' })
)
</script>
<template>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton icon="i-heroicons-calendar-days-20-solid" :label="label" />
<template #panel="{ close }">
<DatePicker v-model="date" @close="close" />
</template>
</UPopover>
</template>

View File

@@ -4,16 +4,16 @@ const items = ref(Array(55))
</script> </script>
<template> <template>
<UPagination v-model="page" :total="items.length" :ui="{ rounded: 'first-of-type:rounded-l-md last-of-type:rounded-r-md' }"> <UPagination v-model="page" :total="items.length" :ui="{ rounded: 'first-of-type:rounded-s-md last-of-type:rounded-e-md' }">
<template #prev="{ onClick }"> <template #prev="{ onClick }">
<UTooltip text="Previous page"> <UTooltip text="Previous page">
<UButton icon="i-heroicons-arrow-small-left-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="mr-2" @click="onClick" /> <UButton icon="i-heroicons-arrow-small-left-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="rtl:[&_span:first-child]:rotate-180 me-2" @click="onClick" />
</UTooltip> </UTooltip>
</template> </template>
<template #next="{ onClick }"> <template #next="{ onClick }">
<UTooltip text="Next page"> <UTooltip text="Next page">
<UButton icon="i-heroicons-arrow-small-right-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="ml-2" @click="onClick" /> <UButton icon="i-heroicons-arrow-small-right-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="rtl:[&_span:last-child]:rotate-180 ms-2" @click="onClick" />
</UTooltip> </UTooltip>
</template> </template>
</UPagination> </UPagination>

View File

@@ -0,0 +1,7 @@
<script setup>
const value = ref(50)
</script>
<template>
<URange v-model="value" />
</template>

View File

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

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
const commandPaletteRef = ref() const commandPaletteRef = ref()
const { navigation } = useContent() const navigation = inject('navigation')
const { data: files } = await useLazyAsyncData('search', () => queryContent().where({ _type: 'markdown' }).find(), { default: () => [] }) const { data: files } = await useLazyAsyncData('search', () => queryContent().where({ _type: 'markdown' }).find(), { default: () => [] })

View File

@@ -31,7 +31,7 @@ const ui = {
wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-200 dark:divide-gray-700 bg-gray-50 dark:bg-gray-800', wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-200 dark:divide-gray-700 bg-gray-50 dark:bg-gray-800',
container: 'relative flex-1 overflow-y-auto divide-y divide-gray-200 dark:divide-gray-700 scroll-py-2', container: 'relative flex-1 overflow-y-auto divide-y divide-gray-200 dark:divide-gray-700 scroll-py-2',
input: { input: {
base: 'w-full h-14 px-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 text-gray-900 dark:text-white focus:ring-0' base: 'w-full h-14 px-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 text-gray-900 dark:text-white focus:ring-0 focus:outline-none'
}, },
group: { group: {
label: 'px-2 my-2 text-xs font-semibold text-gray-500 dark:text-gray-400', label: 'px-2 my-2 text-xs font-semibold text-gray-500 dark:text-gray-400',

View File

@@ -0,0 +1,68 @@
<script setup>
const page = ref(1)
const items = ref(Array(55))
</script>
<template>
<div class="w-full gap-3">
<div class="flex justify-between w-full mb-2 border-b pb-4">
<div dir="ltr">
<UInput
icon="i-heroicons-magnifying-glass-20-solid"
size="sm"
color="white"
placeholder="Search..."
:trailing="false"
/>
</div>
<div dir="rtl">
<UInput
icon="i-heroicons-magnifying-glass-20-solid"
size="sm"
color="white"
placeholder="ابحث..."
:trailing="false"
/>
</div>
</div>
<div class="flex justify-between w-full mt-4">
<div dir="ltr">
<UPagination
v-model="page"
:total="items.length"
:prev-button="{
icon: 'i-heroicons-arrow-small-left-20-solid',
label: 'Prev',
color: 'gray'
}"
:next-button="{
icon: 'i-heroicons-arrow-small-right-20-solid',
trailing: true,
label: 'Next',
color: 'gray'
}"
/>
</div>
<div dir="rtl">
<UPagination
v-model="page"
:total="items.length"
:prev-button="{
icon: 'i-heroicons-arrow-small-left-20-solid',
label: 'السابق',
color: 'gray'
}"
:next-button="{
icon: 'i-heroicons-arrow-small-right-20-solid',
trailing: true,
label: 'التالي',
color: 'gray'
}"
/>
</div>
</div>
</div>
</template>

View File

@@ -1,13 +1,22 @@
<script setup> <script setup>
const links = [{ const links = [{
label: 'Introduction',
to: '/getting-started'
}, {
label: 'Installation', label: 'Installation',
to: '/getting-started/installation' to: '/getting-started/installation'
}, { }, {
label: 'Vertical Navigation', label: 'Theming',
to: '/navigation/vertical-navigation' to: '/getting-started/theming'
}, { }, {
label: 'Command Palette', label: 'Shortcuts',
to: '/navigation/command-palette' to: '/getting-started/shortcuts'
}, {
label: 'Examples',
to: '/getting-started/examples'
}, {
label: 'Roadmap',
to: '/getting-started/roadmap'
}] }]
</script> </script>
@@ -15,9 +24,9 @@ const links = [{
<UVerticalNavigation <UVerticalNavigation
:links="links" :links="links"
:ui="{ :ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2', wrapper: 'border-s border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-l -ml-px lg:leading-6', base: 'group block border-s -ms-px lg:leading-6',
padding: 'pl-4', padding: 'ps-4',
rounded: '', rounded: '',
font: '', font: '',
ring: '', ring: '',

View File

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

View File

@@ -1,9 +1,9 @@
<template> <template>
<div class="space-y-8"> <div class="space-y-8">
<div v-for="(group, index) in navigation" :key="index" class="space-y-3"> <div v-for="(group, index) in navigation" :key="index" class="space-y-3">
<div class="text-sm font-semibold text-gray-900 dark:text-gray-200"> <p class="text-sm font-semibold text-gray-900 dark:text-gray-200 truncate leading-6">
<span class="truncate">{{ group.title }}</span> {{ group.title }}
</div> </p>
<UVerticalNavigation <UVerticalNavigation
:links="mapContentLinks(group.children)" :links="mapContentLinks(group.children)"
@@ -32,7 +32,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types' import type { NavItem } from '@nuxt/content/dist/runtime/types'
const { navigation } = useContent() as { navigation: NavItem[] } const navigation: Ref<NavItem[]> = inject('navigation')
function mapContentLinks (links: NavItem[]) { function mapContentLinks (links: NavItem[]) {
return links?.map(link => ({ label: link.title, icon: link.icon, to: link._path, badge: link.badge })) || [] return links?.map(link => ({ label: link.title, icon: link.icon, to: link._path, badge: link.badge })) || []

View File

@@ -1,10 +1,14 @@
<template> <template>
<footer class="flex items-center justify-end gap-1.5"> <footer class="flex items-center justify-between gap-1.5">
<div class="flex items-baseline gap-1.5 text-sm text-center text-gray-500 dark:text-gray-400"> <div class="flex items-baseline gap-1.5 text-sm text-center text-gray-500 dark:text-gray-400">
Made by Made by
<NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs"> <NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs">
<LogoLabs class="text-primary-500 w-14 h-auto dark:text-primary-400" /> <LogoLabs class="text-primary-500 w-14 h-auto dark:text-primary-400" />
</NuxtLink> </NuxtLink>
</div> </div>
<NuxtLink to="https://github.com/nuxtlabs/ui/releases" target="_blank">
<UBadge label="v2.4.0" />
</NuxtLink>
</footer> </footer>
</template> </template>

View File

@@ -13,5 +13,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { page } = useContent() defineProps({
page: {
type: Object,
default: null
}
})
</script> </script>

View File

@@ -26,12 +26,17 @@
/> />
</div> </div>
</div> </div>
<p v-if="page.description" class="mt-4 text-lg"> <p v-if="page.description" class="mt-4 text-lg text-gray-500 dark:text-gray-400">
{{ page.description }} {{ page.description }}
</p> </p>
</header> </header>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { page } = useContent() defineProps({
page: {
type: Object,
default: null
}
})
</script> </script>

View File

@@ -1,11 +1,17 @@
<template> <template>
<div class="grid gap-6 sm:grid-cols-2"> <div class="grid gap-6 sm:grid-cols-2">
<DocsPrevNextCard v-if="prev" :title="prev.navigation?.title || prev.title" :description="prev.navigation?.description || prev.description" :to="prev._path" icon="i-heroicons-arrow-left-20-solid" /> <DocsPrevNextCard
v-if="prev"
:title="prev.title"
:description="prev.description"
:to="prev._path"
icon="i-heroicons-arrow-left-20-solid"
/>
<span v-else class="hidden sm:block">&nbsp;</span> <span v-else class="hidden sm:block">&nbsp;</span>
<DocsPrevNextCard <DocsPrevNextCard
v-if="next" v-if="next"
:title="next.navigation?.title || next.title" :title="next.title"
:description="next.navigation?.description || next.description" :description="next.description"
:to="next._path" :to="next._path"
icon="i-heroicons-arrow-right-20-solid" icon="i-heroicons-arrow-right-20-solid"
class="text-right" class="text-right"
@@ -14,5 +20,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { prev, next } = useContent() defineProps({
prev: {
type: Object,
default: null
},
next: {
type: Object,
default: null
}
})
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<NuxtLink :to="to" class="block px-5 py-8 border not-prose rounded-lg border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/25 group"> <NuxtLink :to="to" class="block px-5 py-8 border not-prose rounded-lg border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 group">
<div v-if="icon" class="inline-flex items-center rounded-full p-1.5 bg-gray-50 dark:bg-gray-800 group-hover:bg-primary-50 dark:group-hover:bg-primary-400/10 ring-1 ring-gray-300 dark:ring-gray-700 mb-4 group-hover:ring-primary-500/50 dark:group-hover:ring-primary-400/50"> <div v-if="icon" class="inline-flex items-center rounded-full p-1.5 bg-gray-50 dark:bg-gray-800 group-hover:bg-primary-50 dark:group-hover:bg-primary-400/10 ring-1 ring-gray-300 dark:ring-gray-700 mb-4 group-hover:ring-primary-500/50 dark:group-hover:ring-primary-400/50">
<UIcon :name="icon" class="w-5 h-5 text-gray-900 dark:text-gray-100 group-hover:text-primary-500 dark:group-hover:text-primary-400" /> <UIcon :name="icon" class="w-5 h-5 text-gray-900 dark:text-gray-100 group-hover:text-primary-500 dark:group-hover:text-primary-400" />
</div> </div>

View File

@@ -12,6 +12,8 @@
ref="commandPaletteRef" ref="commandPaletteRef"
:groups="groups" :groups="groups"
command-attribute="title" command-attribute="title"
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', color: 'gray', variant: 'ghost', size: 'sm', class: '-mr-1.5' }"
:ui="{ input: { height: 'h-16 sm:h-12', icon: { size: 'h-5 w-5', padding: 'pl-11' } } }"
:fuse="{ :fuse="{
fuseOptions: { ignoreLocation: true, includeMatches: true, threshold: 0, keys: ['title', 'description', 'children.children.value', 'children.children.children.value'] }, fuseOptions: { ignoreLocation: true, includeMatches: true, threshold: 0, keys: ['title', 'description', 'children.children.value', 'children.children.children.value'] },
resultLimit: 10 resultLimit: 10
@@ -23,9 +25,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
import type { Command } from '../../../src/runtime/types' import type { Command } from '../../../src/runtime/types'
const { navigation } = useContent() const navigation: Ref<NavItem[]> = inject('navigation')
const router = useRouter() const router = useRouter()
const { usingInput } = useShortcuts() const { usingInput } = useShortcuts()
const { isSearchModalOpen } = useDocs() const { isSearchModalOpen } = useDocs()

View File

@@ -0,0 +1,33 @@
<template>
<UButton
color="white"
variant="outline"
icon="i-heroicons-magnifying-glass-20-solid"
label="Search..."
truncate
:ui="{
color: {
white: {
outline: 'ring-1 ring-inset ring-gray-200 dark:ring-gray-800 hover:ring-gray-300 dark:hover:ring-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/50 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
}
}
}"
@click="isSearchModalOpen = true"
>
<template #trailing>
<div class="hidden lg:flex items-center gap-0.5 ml-auto -my-1 flex-shrink-0">
<UKbd class="!text-inherit">
{{ metaSymbol }}
</UKbd>
<UKbd class="!text-inherit">
K
</UKbd>
</div>
</template>
</UButton>
</template>
<script setup lang="ts">
const { isSearchModalOpen } = useDocs()
const { metaSymbol } = useShortcuts()
</script>

View File

@@ -13,7 +13,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { toc } = useContent() defineProps({
toc: {
type: Object,
default: null
}
})
const isTocOpen = ref(false) const isTocOpen = ref(false)
</script> </script>

View File

@@ -17,6 +17,7 @@ This module has been developed by the [NuxtLabs](https://nuxtlabs.com/) team for
- Built with [Headless UI](https://headlessui.dev/) and [Tailwind CSS](https://tailwindcss.com/) - Built with [Headless UI](https://headlessui.dev/) and [Tailwind CSS](https://tailwindcss.com/)
- HMR support through Nuxt App Config - HMR support through Nuxt App Config
- Dark mode support - Dark mode support
- Support for LTR and RTL languages
- Keyboard shortcuts - Keyboard shortcuts
- Bundled icons - Bundled icons
- Fully typed - Fully typed

View File

@@ -38,6 +38,53 @@ As this module installs [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/) a
:u-button{icon="i-simple-icons-stackblitz" label="Play on StackBlitz" size="lg" to="https://stackblitz.com/edit/nuxtlabs-ui?file=app.config.ts,app.vue" target="_blank"} :u-button{icon="i-simple-icons-stackblitz" label="Play on StackBlitz" size="lg" to="https://stackblitz.com/edit/nuxtlabs-ui?file=app.config.ts,app.vue" target="_blank"}
## IntelliSense
If you're using VSCode, you can install the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension to get autocompletion for the classes.
You can read more on how to set it up on the [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/tailwind/editor-support) module documentation, but to summarize, you'll need to add the following to your `settings.json`:
```json [settings.json]
{
"files.associations": {
"*.css": "tailwindcss"
},
"editor.quickSuggestions": {
"strings": true
}
}
```
You can write your `tailwind.config` in TypeScript as such:
```ts [tailwind.config.ts]
import type { Config } from 'tailwindcss'
export default <Partial<Config>> {
content: [
'docs/content/**/*.md'
]
}
```
If you do so, you'll need to add the following to your `settings.json`:
```json [settings.json]
{
"tailwindCSS.experimental.configFile": "tailwind.config.ts"
}
```
Also, the extension won't work when writing classes in your `app.config.ts` by default. You can add the following to your `settings.json` to fix this:
```json [settings.json]
{
"tailwindCSS.experimental.classRegex": [
["ui:\\s*{([^)]*)\\s*}", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
```
## Options ## Options
| Key | Default | Description | | Key | Default | Description |

View File

@@ -20,7 +20,7 @@ export default defineAppConfig({
``` ```
::alert{icon="i-heroicons-light-bulb"} ::alert{icon="i-heroicons-light-bulb"}
Try to change the `primary` and `gray` colors in the navbar and see the documentation change live. Try to change the `primary` and `gray` colors by clicking on the :u-icon{name="i-heroicons-swatch-20-solid" class="w-4 h-4 align-middle text-primary-500 dark:text-primary-400"} button in the header.
:: ::
As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors. By default, the `primary` color is `green` and the `gray` color is `cool`. As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors. By default, the `primary` color is `green` and the `gray` color is `cool`.
@@ -33,7 +33,7 @@ Likewise, you can't define a `primary` color in your `tailwind.config.ts` as it
We'd advise you to use those colors in your components and pages, e.g. `text-primary-500 dark:text-primary-400`, `bg-gray-100 dark:bg-gray-900`, etc. so your app automatically adapts when changing your `app.config.ts`. We'd advise you to use those colors in your components and pages, e.g. `text-primary-500 dark:text-primary-400`, `bg-gray-100 dark:bg-gray-900`, etc. so your app automatically adapts when changing your `app.config.ts`.
:: ::
Components having a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style), [Input](/elements/input#style) (inherited in [Select](/forms/select) and [SelectMenu](/forms/select-menu)) and [Notification](/overlays/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors. Components having a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style), [Input](/elements/input#style) (inherited in [Select](/forms/select) and [SelectMenu](/forms/select-menu)), [Radio](/forms/radio), [Checkbox](/forms/checkbox), [Toggle](/forms/toggle), [Range](/forms/range) and [Notification](/overlays/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors.
Variant classes of those components are defined with a syntax like `bg-{color}-500 dark:bg-{color}-400` so they can be used with any color. However, this means that Tailwind will not find those classes and therefore will not generate the corresponding CSS. Variant classes of those components are defined with a syntax like `bg-{color}-500 dark:bg-{color}-400` so they can be used with any color. However, this means that Tailwind will not find those classes and therefore will not generate the corresponding CSS.
@@ -73,6 +73,24 @@ All the components are styled with dark mode in mind.
Thanks to [Tailwind CSS dark mode](https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually) class strategy and the [@nuxtjs/color-mode](https://github.com/nuxt-modules/color-mode) module, you literally have nothing to do. Thanks to [Tailwind CSS dark mode](https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually) class strategy and the [@nuxtjs/color-mode](https://github.com/nuxt-modules/color-mode) module, you literally have nothing to do.
::alert{icon="i-heroicons-puzzle-piece"}
Learn how to build a color mode button in the [Examples](/getting-started/examples#color-mode-button) page.
::
You can disable dark mode by setting the `preference` to `light` instead of `system` in your `nuxt.config.ts`.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
colorMode: {
preference: 'light'
}
})
```
::alert{icon="i-heroicons-light-bulb"}
If you're stuck in dark mode even after changing this setting, you might need to remove the `nuxt-color-mode` entry from your browser's local storage.
::
## Components ## Components
Components are styled with Tailwind CSS but classes are all defined in the default [app.config.ts](https://github.com/nuxtlabs/ui/blob/dev/src/runtime/app.config.ts) file. You can override them in your `app.config.ts`. Components are styled with Tailwind CSS but classes are all defined in the default [app.config.ts](https://github.com/nuxtlabs/ui/blob/dev/src/runtime/app.config.ts) file. You can override them in your `app.config.ts`.
@@ -231,10 +249,23 @@ export default defineAppConfig({
sortButton: { sortButton: {
icon: 'i-octicon-arrow-switch-24' icon: 'i-octicon-arrow-switch-24'
}, },
loadingState: {
icon: 'i-octicon-sync-24'
},
emptyState: { emptyState: {
icon: 'i-octicon-database-24' icon: 'i-octicon-database-24'
} }
} }
},
pagination: {
default: {
prevButton: {
icon: 'i-octicon-arrow-left-24'
},
nextButton: {
icon: 'i-octicon-arrow-right-24'
}
}
} }
} }
}) })

View File

@@ -48,6 +48,21 @@ defineShortcuts({
</script> </script>
``` ```
Shortcuts keys are written as the literal keyboard key value. Combinations are made with `_` separator. Chained shortcuts are made with `-` separator.
Modifiers are also available:
- `meta`: acts as `Command` for MacOS and `Control` for others
- `ctrl`: acts as `Control`
- `shift`: acts as `Shift` and is only necessary for alphabetic keys
Examples of keys:
- `escape`: will trigger by hitting `Esc`
- `meta_k`: will trigger by hitting `⌘` and `K` at the same time on MacOS, and `Ctrl` and `K` on Windows and Linux
- `ctrl_k`: will trigger by hitting `Ctrl` and `K` at the same time on MacOS, Windows and Linux
- `shift_e`: will trigger by hitting `Shift` and `E` at the same time on MacOS, Windows and Linux
- `?`: will trigger by hitting `?` on some keyboard layouts, or for example `Shift` and `/`, which results in `?` on US Mac keyboards
- `g-d`: will trigger by hitting `g` then `d` with a maximum delay of 800ms by default
### `usingInput` ### `usingInput`
Prop: `usingInput?: string | boolean` Prop: `usingInput?: string | boolean`

View File

@@ -0,0 +1,258 @@
---
title: Examples
description: Discover some real-life examples of components you can build.
---
::alert{icon="i-heroicons-wrench-screwdriver"}
If you have any ideas of examples you'd like to see, please comment on [this issue](https://github.com/nuxtlabs/ui/issues/297).
::
## Components
You can mix and match components to build your own UI.
### ColorModeButton
You can easily build a color mode button by using the `useColorMode` composable from `@nuxtjs/color-mode`.
::component-example
#default
:color-mode-button
#code
```vue [components/ColorModeButton.vue]
<script setup>
const colorMode = useColorMode()
const isDark = computed({
get () {
return colorMode.value === 'dark'
},
set () {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
</script>
<template>
<ClientOnly>
<UButton
:icon="isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'"
color="gray"
variant="ghost"
aria-label="Theme"
@click="isDark = !isDark"
/>
<template #fallback>
<div class="w-8 h-8" />
</template>
</ClientOnly>
</template>
```
::
### DatePicker
Here is an example of a date picker component built with [v-calendar](https://github.com/nathanreyes/v-calendar).
```vue [components/DatePicker.vue]
<script setup lang="ts">
import { DatePicker as VCalendarDatePicker } from 'v-calendar'
import 'v-calendar/dist/style.css'
const props = defineProps({
modelValue: {
type: Date,
default: null
}
})
const emit = defineEmits(['update:model-value', 'close'])
const colorMode = useColorMode()
const isDark = computed(() => colorMode.value === 'dark')
const date = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:model-value', value)
emit('close')
}
})
const attrs = [{
key: 'today',
highlight: {
color: 'blue',
fillMode: 'outline',
class: '!bg-gray-100 dark:!bg-gray-800'
},
dates: new Date()
}]
</script>
<template>
<VCalendarDatePicker
v-model="date"
transparent
borderless
:attributes="attrs"
:is-dark="isDark"
title-position="left"
trim-weeks
:first-day-of-week="2"
/>
</template>
```
You can use it inside a [Popover](/overlays/popover) component to display it when clicking on a [Button](/elements/button).
::component-example
#default
:date-picker-example
#code
```vue
<script setup>
const date = ref(new Date())
const label = computed(() => date.value.toLocaleDateString('en-us', { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric' })
)
</script>
<template>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton icon="i-heroicons-calendar-days-20-solid" :label="label" />
<template #panel="{ close }">
<DatePicker v-model="date" @close="close" />
</template>
</UPopover>
</template>
```
::
## Theming
Our theming system provides a lot of flexibility to customize the components.
### CommandPalette
Here is some examples of what you can do with the [CommandPalette](/navigation/command-palette).
#### Algolia
::component-example
---
padding: false
---
#default
:command-palette-theme-algolia{class="max-h-[480px] rounded-md"}
::
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeAlgolia.vue#L23"}
Take a look at the component!
::
#### Raycast
::component-example
---
padding: false
---
#default
:command-palette-theme-raycast{class="max-h-[480px] rounded-md"}
::
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeRaycast.vue#L30"}
Take a look at the component!
::
### VerticalNavigation
::component-example
#default
:vertical-navigation-theme-tailwind
#code
```vue
<script setup>
const links = [{
label: 'Introduction',
to: '/getting-started'
}, {
label: 'Installation',
to: '/getting-started/installation'
}, {
label: 'Theming',
to: '/getting-started/theming'
}, {
label: 'Shortcuts',
to: '/getting-started/shortcuts'
}, {
label: 'Examples',
to: '/getting-started/examples'
}, {
label: 'Roadmap',
to: '/getting-started/roadmap'
}]
</script>
<template>
<UVerticalNavigation
:links="links"
:ui="{
wrapper: 'border-s border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-s -ms-px lg:leading-6',
padding: 'ps-4',
rounded: '',
font: '',
ring: '',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}"
/>
</template>
```
::
### Pagination
::component-example
#default
:pagination-theme-rounded
#code
```vue
<script setup>
const page = ref(1)
const items = ref(Array(55))
</script>
<template>
<UPagination
v-model="page"
:total="items.length"
:ui="{
wrapper: 'flex items-center gap-1',
rounded: 'rounded-full min-w-[32px] justify-center'
}"
:prev-button="null"
:next-button="{
icon: 'i-heroicons-arrow-small-right-20-solid',
color: 'primary',
variant: 'outline'
}"
/>
</template>
```
::
### LTR and RTL
::component-example
#default
:l-t-r-and-r-t-l-theme
::

View File

@@ -29,13 +29,15 @@ baseProps:
### Chip ### Chip
Use the `chip-color` and `chip-position` props to display a chip on the Avatar. Use the `chip-color`, `chip-text` :u-badge{label="Edge" class="!rounded-full"} and `chip-position` props to display a chip on the Avatar.
::component-card ::component-card
--- ---
props: props:
chipColor: 'primary' chipColor: 'primary'
chipText: ''
chipPosition: 'top-right' chipPosition: 'top-right'
size : 'sm'
extraColors: extraColors:
- gray - gray
baseProps: baseProps:

View File

@@ -14,7 +14,7 @@ Use a `v-model` to make the Checkbox reactive.
#code #code
```vue ```vue
<script setup> <script setup>
const selected = ref(false) const selected = ref(true)
</script> </script>
<template> <template>
@@ -36,6 +36,20 @@ props:
--- ---
:: ::
### Style
Use the `color` prop to change the style of the Checkbox.
::component-card
---
baseProps:
name: 'checkbox2'
label: 'Label'
props:
color: 'primary'
---
::
### Required ### Required
Use the `required` prop to display a red star next to the label. Use the `required` prop to display a red star next to the label.
@@ -43,7 +57,7 @@ Use the `required` prop to display a red star next to the label.
::component-card ::component-card
--- ---
baseProps: baseProps:
name: 'checkbox2' name: 'checkbox3'
props: props:
label: 'Label' label: 'Label'
required: true required: true
@@ -57,7 +71,7 @@ Use the `help` prop to display some text under the Checkbox.
::component-card ::component-card
--- ---
baseProps: baseProps:
name: 'checkbox3' name: 'checkbox4'
props: props:
label: 'Label' label: 'Label'
help: 'Please check this box' help: 'Please check this box'

View File

@@ -50,6 +50,20 @@ props:
--- ---
:: ::
### Style
Use the `color` prop to change the style of the Radio.
::component-card
---
baseProps:
name: 'radio2'
label: 'Label'
props:
color: 'primary'
---
::
### Required ### Required
Use the `required` prop to display a red star next to the label. Use the `required` prop to display a red star next to the label.
@@ -57,7 +71,7 @@ Use the `required` prop to display a red star next to the label.
::component-card ::component-card
--- ---
baseProps: baseProps:
name: 'radio2' name: 'radio3'
props: props:
label: 'Label' label: 'Label'
required: true required: true
@@ -71,7 +85,7 @@ Use the `help` prop to display some text under the Radio.
::component-card ::component-card
--- ---
baseProps: baseProps:
name: 'radio3' name: 'radio4'
props: props:
label: 'Label' label: 'Label'
help: 'Please choose one' help: 'Please choose one'
@@ -85,7 +99,7 @@ Use the `disabled` prop to disable the Radio.
::component-card ::component-card
--- ---
baseProps: baseProps:
name: 'radio4' name: 'radio5'
value: true value: true
props: props:
disabled: true disabled: true

View File

@@ -26,6 +26,17 @@ const selected = ref(false)
``` ```
:: ::
### Style
Use the `color` prop to change the style of the Toggle.
::component-card
---
props:
color: 'primary'
---
::
### Icon ### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `on-icon` and `off-icon` props by using this pattern: `i-{collection_name}-{icon_name}` or change it globally in `ui.toggle.default.onIcon` and `ui.toggle.default.offIcon`. Use any icon from [Iconify](https://icones.js.org) by setting the `on-icon` and `off-icon` props by using this pattern: `i-{collection_name}-{icon_name}` or change it globally in `ui.toggle.default.onIcon` and `ui.toggle.default.offIcon`.

View File

@@ -0,0 +1,101 @@
---
github: true
description: Display a range field
navigation:
badge: "Edge"
---
## Usage
Use a `v-model` to make the Range reactive.
::component-example
#default
:range-example
#code
```vue
<script setup>
const value = ref(50)
</script>
<template>
<URange v-model="value" />
</template>
```
::
### Style
Use the `color` prop to change the visual style of the Range.
::component-card
---
baseProps:
name: range'
placeholder: 'Search...'
props:
color: 'primary'
---
::
### Size
Use the `size` prop to change the size of the Range.
::component-card
---
baseProps:
name: 'range'
props:
size: 'md'
---
::
### Disabled
Use the `disabled` prop to disable the Range.
::component-card
---
baseProps:
name: 'range'
props:
disabled: true
---
::
### Min and Max
Use the `min` and `max` prop to configure the Range.
::component-card
---
baseProps:
name: 'range'
props:
min: 0
max: 100
---
::
### Step
Use the `step` prop to change the step increment.
::component-card
---
baseProps:
name: 'range'
props:
step: 20
---
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -401,7 +401,7 @@ const rows = computed(() => {
``` ```
:: ::
### Loading :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full"} ### Loading :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
Use the `loading` prop to display a loading state. Use the `loading` prop to display a loading state.
@@ -566,7 +566,7 @@ const selected = ref([people[1]])
``` ```
:: ::
### `loading-state` :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full"} ### `loading-state` :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
Use the `#loading-state` slot to customize the loading state. Use the `#loading-state` slot to customize the loading state.
@@ -605,7 +605,7 @@ const pending = ref(true)
``` ```
:: ::
### `empty-state` :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full"} ### `empty-state` :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
Use the `#empty-state` slot to customize the empty state. Use the `#empty-state` slot to customize the empty state.

View File

@@ -39,46 +39,6 @@ const links = [{
``` ```
:: ::
## Theme
Our theming system provides a lot of flexibility to customize the component. Here is an example of what you can do.
::component-example
#default
:vertical-navigation-theme-tailwind
#code
```vue
<script setup>
const links = [{
label: 'Installation',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
to: '/navigation/vertical-navigation'
}, {
label: 'Command Palette',
to: '/navigation/command-palette'
}]
</script>
<template>
<UVerticalNavigation
:links="links"
:ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-l -ml-px lg:leading-6',
padding: 'pl-4',
rounded: '',
font: '',
ring: '',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}"
/>
</template>
```
::
## Props ## Props
:component-props :component-props

View File

@@ -323,43 +323,9 @@ const groups = computed(() => {
The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behavior by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`. The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behavior by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`.
:: ::
## Themes
Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do.
### Algolia
::component-example
---
padding: false
---
#default
:command-palette-theme-algolia{class="max-h-[480px] rounded-md"}
::
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeAlgolia.vue#L23"}
Take a look at the component!
::
### Raycast
::component-example
---
padding: false
---
#default
:command-palette-theme-raycast{class="max-h-[480px] rounded-md"}
::
::alert{icon="i-simple-icons-github" to="https://github.com/nuxtlabs/ui/blob/dev/docs/components/content/themes/CommandPaletteThemeRaycast.vue#L30"}
Take a look at the component!
::
## Slots ## Slots
### `empty-state` :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full"} ### `empty-state` :u-badge{label="New" class="ml-2 align-text-bottom !rounded-full"}
Use the `#empty-state` slot to customize the empty state. Use the `#empty-state` slot to customize the empty state.

View File

@@ -2,7 +2,7 @@
github: true github: true
description: Add a pagination to handle pages. description: Add a pagination to handle pages.
navigation: navigation:
badge: 'Edge' badge: 'New'
--- ---
## Usage ## Usage
@@ -111,39 +111,6 @@ excludedProps:
--- ---
:: ::
## Theme
Our theming system provides a lot of flexibility to customize the component. Here is an example of what you can do.
::component-example
#default
:pagination-theme-rounded
#code
```vue
<script setup>
const page = ref(1)
const items = ref(Array(55))
</script>
<template>
<UPagination
v-model="page"
:total="items.length"
:ui="{
wrapper: 'flex items-center gap-1',
rounded: 'rounded-full min-w-[32px] justify-center'
}"
:prev-button="null"
:next-button="{
icon: 'i-heroicons-arrow-small-right-20-solid',
color: 'primary',
variant: 'outline'
}"
/>
</template>
```
::
## Slots ## Slots
### `prev` / `next` ### `prev` / `next`
@@ -162,16 +129,16 @@ const items = ref(Array(55));
</script> </script>
<template> <template>
<UPagination v-model="page" :total="items.length" :ui="{ rounded: 'first-of-type:rounded-l-md last-of-type:rounded-r-md' }"> <UPagination v-model="page" :total="items.length" :ui="{ rounded: 'first-of-type:rounded-s-md last-of-type:rounded-e-md' }">
<template #prev="{ onClick }"> <template #prev="{ onClick }">
<UTooltip text="Previous page"> <UTooltip text="Previous page">
<UButton icon="i-heroicons-arrow-small-left-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="mr-2" @click="onClick" /> <UButton icon="i-heroicons-arrow-small-left-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="rtl:[&_span:first-child]:rotate-180 me-2" @click="onClick" />
</UTooltip> </UTooltip>
</template> </template>
<template #next="{ onClick }"> <template #next="{ onClick }">
<UTooltip text="Next page"> <UTooltip text="Next page">
<UButton icon="i-heroicons-arrow-small-right-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="ml-2" @click="onClick" /> <UButton icon="i-heroicons-arrow-small-right-20-solid" color="primary" :ui="{ rounded: 'rounded-full' }" class="rtl:[&_span:last-child]:rotate-180 ms-2" @click="onClick" />
</UTooltip> </UTooltip>
</template> </template>
</UPagination> </UPagination>

View File

@@ -19,6 +19,19 @@ First of all, add the `Notifications` component to your app, preferably inside `
</template> </template>
``` ```
This component will render the notifications at the bottom right of the screen by default. You can configure its behavior in the `app.config.ts` through `ui.notifications`:
```ts [app.config.ts]
export default defineAppConfig({
ui: {
notifications: {
// Show toasts at the top right of the screen
position: 'top-0 right-0'
}
}
})
```
Then, you can use the `useToast` composable to add notifications to your app: Then, you can use the `useToast` composable to add notifications to your app:
::component-example ::component-example
@@ -36,19 +49,46 @@ const toast = useToast()
``` ```
:: ::
This component will render by default the notifications at the bottom right of the screen. You can configure its behavior in the `app.config.ts` through `ui.notifications`: When using `toast.add`, this will push a new notification to the stack displayed in `<UNotifications />`. All the props of the `Notification` component can be passed to `toast.add`.
```ts [app.config.ts] ```vue
export default defineAppConfig({ <script setup>
ui: { const toast = useToast()
notifications: {
// Show toasts at the top right of the screen onMounted(() => {
position: 'top-0 right-0' toast.add({
} id: 'update_downloaded',
} title: 'Update downloaded.',
description: 'It will be installed on restart. Restart now?',
icon: 'i-octicon-desktop-download-24',
timeout: 0,
actions: [{
label: 'Restart',
click: () => {
}
}]
})
}) })
</script>
``` ```
You can also use the `Notification` component directly in your app as an alert for example.
### Title
Pass a `title` to your Notification.
::component-card
---
baseProps:
id: 1
timeout: 0
props:
title: 'Notification'
---
::
### Description ### Description
You can add a `description` in addition of the `title`. You can add a `description` in addition of the `title`.
@@ -58,8 +98,8 @@ You can add a `description` in addition of the `title`.
baseProps: baseProps:
id: 2 id: 2
timeout: 0 timeout: 0
props:
title: 'Notification' title: 'Notification'
props:
description: 'This is a notification.' description: 'This is a notification.'
--- ---
:: ::
@@ -118,14 +158,14 @@ props:
--- ---
:: ::
### Color ### Style
Use the `color` prop to change the progress and icon color of the Notification. Use the `color` prop to change the progress and icon color of the Notification.
::component-card ::component-card
--- ---
baseProps: baseProps:
id: 5 id: 6
title: 'Notification' title: 'Notification'
description: 'This is a notification.' description: 'This is a notification.'
timeout: 600000 timeout: 600000
@@ -194,7 +234,7 @@ You can pass all the props of the [Button](/elements/button) component to custom
::component-card ::component-card
--- ---
baseProps: baseProps:
id: 6 id: 7
title: 'Notification' title: 'Notification'
timeout: 0 timeout: 0
props: props:
@@ -235,7 +275,7 @@ Like for `closeButton`, you can pass all the props of the [Button](/elements/but
::component-card ::component-card
--- ---
baseProps: baseProps:
id: 6 id: 8
title: 'Notification' title: 'Notification'
timeout: 0 timeout: 0
props: props:
@@ -256,7 +296,7 @@ Actions will render differently whether you have a `description` set.
::component-card ::component-card
--- ---
baseProps: baseProps:
id: 6 id: 9
title: 'Notification' title: 'Notification'
description: 'This is a notification.' description: 'This is a notification.'
timeout: 0 timeout: 0

View File

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

View File

@@ -15,7 +15,6 @@ export default defineNuxtConfig({
'nuxt-component-meta' 'nuxt-component-meta'
], ],
content: { content: {
documentDriven: true,
highlight: { highlight: {
theme: { theme: {
light: 'material-lighter', light: 'material-lighter',

53
docs/pages/[...slug].vue Normal file
View File

@@ -0,0 +1,53 @@
<template>
<div v-if="page" class="grid lg:grid-cols-10 lg:gap-8">
<div class="pt-8 pb-16" :class="page.body?.toc ? 'lg:col-span-8' : 'lg:col-span-10'">
<DocsPageHeader :page="page" />
<ContentRenderer v-if="page.body" :value="page" class="prose prose-primary dark:prose-invert max-w-none" />
<DocsPageFooter :page="page" class="mt-12" />
<hr class="border-gray-200 dark:border-gray-800 my-6">
<DocsPrevNext :prev="prev" :next="next" />
<DocsFooter class="mt-16" />
</div>
<DocsToc v-if="page.body?.toc" :toc="page.body.toc" class="lg:col-span-2" />
</div>
<div v-else class="flex-1 flex flex-col items-center justify-center">
<div class="text-center">
<p class="text-base font-semibold text-primary-500 dark:text-primary-400">
404
</p>
<h1 class="mt-2 text-4xl tracking-tight font-extrabold u-text-gray-900 sm:text-5xl">
Page not found
</h1>
<p class="mt-2 text-base u-text-gray-500">
Sorry, we couldnt find the page youre looking for.
</p>
<div class="mt-6">
<NuxtLink to="/" class="text-base font-medium text-primary-500 dark:text-primary-400 hover:u-text-gray-900">
Go back home
<span aria-hidden="true"> &rarr;</span>
</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: page } = await useAsyncData(`docs-${route.path}`, () => queryContent(route.path).findOne())
const { data: surround } = await useAsyncData(`docs-${route.path}-surround`, () => queryContent()
.only(['_path', 'title', 'navigation', 'description'])
.where({ _extension: 'md', navigation: { $ne: false } })
.findSurround(route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
)
const [prev, next] = surround.value
useContentHead(page)
</script>

42
docs/plugins/ui.ts Normal file
View File

@@ -0,0 +1,42 @@
import { hexToRgb } from "../../src/runtime/utils"
import colors from '#tailwind-config/theme/colors'
export default defineNuxtPlugin({
enforce: 'post',
setup () {
const appConfig = useAppConfig()
const root = computed(() => {
const primary: Record<string, string> | undefined = colors[appConfig.ui.primary]
const gray: Record<string, string> | undefined = colors[appConfig.ui.gray]
return `:root {
${Object.entries(primary || colors.green).map(([key, value]) => `--color-primary-${key}: ${hexToRgb(value)};`).join('\n')}
${Object.entries(gray || colors.cool).map(([key, value]) => `--color-gray-${key}: ${hexToRgb(value)};`).join('\n')}
}`
})
if (process.client) {
watch(root, () => {
window.localStorage.setItem('nuxt-ui-root', root.value)
})
appConfig.ui.primary = window.localStorage.getItem('nuxt-ui-primary') || appConfig.ui.primary
appConfig.ui.gray = window.localStorage.getItem('nuxt-ui-gray') || appConfig.ui.gray
}
if (process.server) {
useHead({
script: [
{
innerHTML: `
if (localStorage.getItem('nuxt-ui-root')) {
document.querySelector('style#nuxt-ui-colors').innerHTML = localStorage.getItem('nuxt-ui-root')
}`.replace(/\s+/g, ' '),
type: 'text/javascript',
tagPriority: -1
}
]
})
}
}
})

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nuxthq/ui", "name": "@nuxthq/ui",
"version": "2.4.0", "version": "2.5.0",
"repository": "https://github.com/nuxtlabs/ui", "repository": "https://github.com/nuxtlabs/ui",
"license": "MIT", "license": "MIT",
"exports": { "exports": {
@@ -15,7 +15,7 @@
"dist" "dist"
], ],
"engines": { "engines": {
"node": ">=16.14.0" "node": ">=v16.14.0"
}, },
"scripts": { "scripts": {
"build": "nuxt-module-build", "build": "nuxt-module-build",
@@ -25,45 +25,47 @@
"lint": "eslint .", "lint": "eslint .",
"typecheck": "nuxi typecheck", "typecheck": "nuxi typecheck",
"prepare": "nuxi prepare docs", "prepare": "nuxi prepare docs",
"release": "pnpm lint && standard-version && git push --follow-tags" "release": "release-it"
}, },
"dependencies": { "dependencies": {
"@egoist/tailwindcss-icons": "^1.1.0", "@egoist/tailwindcss-icons": "^1.1.0",
"@headlessui/vue": "1.7.10", "@headlessui/vue": "^1.7.14",
"@iconify-json/heroicons": "^1.1.11", "@iconify-json/heroicons": "^1.1.11",
"@nuxt/kit": "^3.5.3", "@nuxt/kit": "^3.6.1",
"@nuxtjs/color-mode": "^3.2.0", "@nuxtjs/color-mode": "^3.3.0",
"@nuxtjs/tailwindcss": "^6.7.2", "@nuxtjs/tailwindcss": "^6.8.0",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@vueuse/core": "^10.1.2", "@vueuse/core": "^10.2.0",
"@vueuse/integrations": "^10.1.2", "@vueuse/integrations": "^10.2.0",
"@vueuse/math": "^10.1.2", "@vueuse/math": "^10.2.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",
"tailwindcss": "^3.3.2" "tailwindcss": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/simple-icons": "^1.1.56", "@iconify-json/simple-icons": "^1.1.58",
"@nuxt/content": "^2.6.0", "@nuxt/content": "^2.7.0",
"@nuxt/devtools": "^0.5.5", "@nuxt/devtools": "^0.6.4",
"@nuxt/eslint-config": "^0.1.1", "@nuxt/eslint-config": "^0.1.1",
"@nuxt/module-builder": "^0.4.0", "@nuxt/module-builder": "^0.4.0",
"@nuxthq/studio": "^0.13.2", "@nuxthq/studio": "^0.13.2",
"@nuxtjs/plausible": "^0.2.1", "@nuxtjs/plausible": "^0.2.1",
"@release-it/conventional-changelog": "^5.1.1",
"@types/lodash-es": "^4.17.7", "@types/lodash-es": "^4.17.7",
"@types/node": "^20.3.1", "@types/node": "^20.3.2",
"@vueuse/nuxt": "^10.1.2", "@vueuse/nuxt": "^10.2.0",
"eslint": "^8.42.0", "eslint": "^8.43.0",
"nuxt": "^3.5.3", "nuxt": "^3.6.1",
"nuxt-component-meta": "^0.5.3", "nuxt-component-meta": "^0.5.3",
"nuxt-lodash": "^2.4.1", "nuxt-lodash": "^2.5.0",
"standard-version": "^9.5.0", "release-it": "^15.11.0",
"unbuild": "^1.2.1", "unbuild": "^1.2.1",
"vue-tsc": "1.6.3" "v-calendar": "^3.0.3",
"vue-tsc": "^1.8.2"
} }
} }

2853
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,26 +27,26 @@ const kebabCase = (str: string) => {
const safelistByComponent = { const safelistByComponent = {
avatar: (colorsAsRegex) => [{ avatar: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-400`), pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark'] variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}], }],
badge: (colorsAsRegex) => [{ badge: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-50`) pattern: new RegExp(`bg-(${colorsAsRegex})-50`)
}, { }, {
pattern: new RegExp(`bg-(${colorsAsRegex})-400`), pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark'] variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, { }, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`), pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark'] variants: ['dark']
}, { }, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`) pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, { }, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`), pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark'] variants: ['dark']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`)
}], }],
button: (colorsAsRegex) => [{ button: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-50`), pattern: new RegExp(`bg-(${colorsAsRegex})-50`),
@@ -92,31 +92,100 @@ const safelistByComponent = {
variants: ['focus-visible'] variants: ['focus-visible']
}], }],
input: (colorsAsRegex) => [{ input: (colorsAsRegex) => [{
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`), pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark', 'dark:focus'] variants: ['dark', 'dark:focus']
}, { }, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`), pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus'] variants: ['focus']
}], }],
notification: (colorsAsRegex) => [{ radio: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-500`) pattern: new RegExp(`text-(${colorsAsRegex})-400`),
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark'] variants: ['dark']
}, { }, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`) pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, { }, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
checkbox: (colorsAsRegex) => [{
pattern: new RegExp(`text-(${colorsAsRegex})-400`), pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark'] variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
toggle: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
range: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus-visible']
}],
notification: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}] }]
} }
const safelistComponentAliasesMap = {
'USelect': 'UInput',
'USelectMenu': 'UInput',
'UTextarea': 'UInput'
}
const colorsAsRegex = (colors: string[]): string => colors.join('|') const colorsAsRegex = (colors: string[]): string => colors.join('|')
export const excludeColors = (colors: object) => Object.keys(omit(colors, colorsToExclude)).map(color => kebabCase(color)) as string[] export const excludeColors = (colors: object) => Object.keys(omit(colors, colorsToExclude)).map(color => kebabCase(color)) as string[]
export const generateSafelist = (colors: string[]) => { export const generateSafelist = (colors: string[]) => {
const safelist = ['avatar', 'badge', 'button', 'input', 'notification'].flatMap(component => safelistByComponent[component](colorsAsRegex(colors))) const safelist = Object.keys(safelistByComponent).flatMap(component => safelistByComponent[component](colorsAsRegex(colors)))
return [ return [
...safelist, ...safelist,
@@ -128,34 +197,41 @@ export const generateSafelist = (colors: string[]) => {
] ]
} }
export const customSafelistExtractor = (prefix, content: string, colors: string[]) => { export const customSafelistExtractor = (prefix, content: string, colors: string[], safelistColors: string[]) => {
const classes = [] const classes = []
const regex = /<(\w+)\s+[^>:]*color=["']([^"']+)["'][^>]*>/gs const regex = /<(\w+)\s+(?![^>]*:color\b)[^>]*\bcolor=["']([^"']+)["'][^>]*>/gs
const matches = content.matchAll(regex) const matches = content.matchAll(regex)
const components = Object.keys(safelistByComponent).map(component => `${prefix}${component.charAt(0).toUpperCase() + component.slice(1)}`)
for (const match of matches) { for (const match of matches) {
const [, component, color] = match const [, component, color] = match
if (!colors.includes(color)) { if (!colors.includes(color) || safelistColors.includes(color)) {
continue continue
} }
if (Object.keys(safelistByComponent).map(component => `${prefix}${component.charAt(0).toUpperCase() + component.slice(1)}`).includes(component)) { let name = safelistComponentAliasesMap[component] ? safelistComponentAliasesMap[component] : component
const name = component.replace(prefix, '').toLowerCase()
const matchClasses = safelistByComponent[name](color).flatMap(group => { if (!components.includes(name)) {
return ['', ...(group.variants || [])].flatMap(variant => { continue
const matches = group.pattern.source.match(/\(([^)]+)\)/g)
return matches.map(match => {
const colorOptions = match.substring(1, match.length - 1).split('|')
return colorOptions.map(color => `${variant ? variant + ':' : ''}` + group.pattern.source.replace(match, color))
}).flat()
})
})
classes.push(...matchClasses)
} }
name = name.replace(prefix, '').toLowerCase()
const matchClasses = safelistByComponent[name](color).flatMap(group => {
return ['', ...(group.variants || [])].flatMap(variant => {
const matches = group.pattern.source.match(/\(([^)]+)\)/g)
return matches.map(match => {
const colorOptions = match.substring(1, match.length - 1).split('|')
return colorOptions.map(color => `${variant ? variant + ':' : ''}` + group.pattern.source.replace(match, color))
}).flat()
})
})
classes.push(...matchClasses)
} }
return classes return classes

View File

@@ -133,7 +133,6 @@ export default defineNuxtModule<ModuleOptions>({
await installModule('@nuxtjs/color-mode', { classSuffix: '' }) await installModule('@nuxtjs/color-mode', { classSuffix: '' })
await installModule('@nuxtjs/tailwindcss', { await installModule('@nuxtjs/tailwindcss', {
viewer: false,
exposeConfig: true, exposeConfig: true,
config: { config: {
darkMode: 'class', darkMode: 'class',
@@ -157,7 +156,7 @@ export default defineNuxtModule<ModuleOptions>({
vue: (content) => { vue: (content) => {
return [ return [
...defaultExtractor(content), ...defaultExtractor(content),
...customSafelistExtractor(options.prefix, content, nuxt.options.appConfig.ui.colors) ...customSafelistExtractor(options.prefix, content, nuxt.options.appConfig.ui.colors, options.safelistColors)
] ]
} }
} }

View File

@@ -11,7 +11,7 @@ const table = {
selected: 'bg-gray-50 dark:bg-gray-800/50' selected: 'bg-gray-50 dark:bg-gray-800/50'
}, },
th: { th: {
base: 'text-left', base: 'text-left rtl:text-right',
padding: 'px-3 py-3.5', padding: 'px-3 py-3.5',
color: 'text-gray-900 dark:text-white', color: 'text-gray-900 dark:text-white',
font: 'font-semibold', font: 'font-semibold',
@@ -75,7 +75,7 @@ const avatar = {
'3xl': 'h-20 w-20 text-2xl' '3xl': 'h-20 w-20 text-2xl'
}, },
chip: { chip: {
base: 'absolute block rounded-full ring-1 ring-white dark:ring-gray-900', base: 'absolute rounded-full ring-1 ring-white dark:ring-gray-900 flex items-center justify-center text-white dark:text-gray-900 font-medium',
background: 'bg-{color}-500 dark:bg-{color}-400', background: 'bg-{color}-500 dark:bg-{color}-400',
position: { position: {
'top-right': 'top-0 right-0', 'top-right': 'top-0 right-0',
@@ -84,15 +84,15 @@ const avatar = {
'bottom-left': 'bottom-0 left-0' 'bottom-left': 'bottom-0 left-0'
}, },
size: { size: {
'3xs': 'h-1 w-1', '3xs': 'h-[4px] min-w-[4px] text-[4px] p-px',
'2xs': 'h-1 w-1', '2xs': 'h-[5px] min-w-[5px] text-[5px] p-px',
xs: 'h-1.5 w-1.5', xs: 'h-1.5 min-w-[0.375rem] text-[6px] p-px',
sm: 'h-2 w-2', sm: 'h-2 min-w-[0.5rem] text-[7px] p-0.5',
md: 'h-2.5 w-2.5', md: 'h-2.5 min-w-[0.625rem] text-[8px] p-0.5',
lg: 'h-3 w-3', lg: 'h-3 min-w-[0.75rem] text-[10px] p-0.5',
xl: 'h-3.5 w-3.5', xl: 'h-3.5 min-w-[0.875rem] text-[11px] p-1',
'2xl': 'h-3.5 w-3.5', '2xl': 'h-4 min-w-[1rem] text-[12px] p-1',
'3xl': 'h-4 w-4' '3xl': 'h-5 min-w-[1.25rem] text-[14px] p-1'
} }
}, },
default: { default: {
@@ -105,7 +105,7 @@ const avatar = {
const avatarGroup = { const avatarGroup = {
wrapper: 'flex flex-row-reverse', wrapper: 'flex flex-row-reverse',
ring: 'ring-2 ring-white dark:ring-gray-900', ring: 'ring-2 ring-white dark:ring-gray-900',
margin: '-mr-1.5 first:mr-0' margin: '-me-1.5 first:me-0'
} }
const badge = { const badge = {
@@ -213,7 +213,7 @@ const buttonGroup = {
} }
const dropdown = { const dropdown = {
wrapper: 'relative inline-flex text-left', wrapper: 'relative inline-flex text-left rtl:text-right',
container: 'z-20', container: 'z-20',
width: 'w-48', width: 'w-48',
height: '', height: '',
@@ -241,8 +241,9 @@ const dropdown = {
base: 'flex-shrink-0', base: 'flex-shrink-0',
size: '3xs' size: '3xs'
}, },
shortcuts: 'hidden md:inline-flex flex-shrink-0 gap-0.5 ml-auto' shortcuts: 'hidden md:inline-flex flex-shrink-0 gap-0.5 ms-auto'
}, },
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
transition: { transition: {
enterActiveClass: 'transition duration-100 ease-out', enterActiveClass: 'transition duration-100 ease-out',
enterFromClass: 'transform scale-95 opacity-0', enterFromClass: 'transform scale-95 opacity-0',
@@ -307,30 +308,30 @@ const input = {
}, },
leading: { leading: {
padding: { padding: {
'2xs': 'pl-7', '2xs': 'ps-7',
xs: 'pl-8', xs: 'ps-8',
sm: 'pl-9', sm: 'ps-9',
md: 'pl-10', md: 'ps-10',
lg: 'pl-11', lg: 'ps-11',
xl: 'pl-12' xl: 'ps-12'
} }
}, },
trailing: { trailing: {
padding: { padding: {
'2xs': 'pr-7', '2xs': 'pe-7',
xs: 'pr-8', xs: 'pe-8',
sm: 'pr-9', sm: 'pe-9',
md: 'pr-10', md: 'pe-10',
lg: 'pr-11', lg: 'pe-11',
xl: 'pr-12' xl: 'pe-12'
} }
}, },
color: { color: {
white: { white: {
outline: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400', outline: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
}, },
gray: { gray: {
outline: 'shadow-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400', outline: 'shadow-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
} }
}, },
variant: { variant: {
@@ -349,27 +350,27 @@ const input = {
xl: 'h-6 w-6' xl: 'h-6 w-6'
}, },
leading: { leading: {
wrapper: 'absolute inset-y-0 left-0 flex items-center', wrapper: 'absolute inset-y-0 start-0 flex items-center',
pointer: 'pointer-events-none', pointer: 'pointer-events-none',
padding: { padding: {
'2xs': 'pl-2', '2xs': 'ps-2',
xs: 'pl-2.5', xs: 'ps-2.5',
sm: 'pl-2.5', sm: 'ps-2.5',
md: 'pl-3', md: 'ps-3',
lg: 'pl-3.5', lg: 'ps-3.5',
xl: 'pl-3.5' xl: 'ps-3.5'
} }
}, },
trailing: { trailing: {
wrapper: 'absolute inset-y-0 right-0 flex items-center', wrapper: 'absolute inset-y-0 end-0 flex items-center',
pointer: 'pointer-events-none', pointer: 'pointer-events-none',
padding: { padding: {
'2xs': 'pr-2', '2xs': 'pe-2',
xs: 'pr-2.5', xs: 'pe-2.5',
sm: 'pr-2.5', sm: 'pe-2.5',
md: 'pr-3', md: 'pe-3',
lg: 'pr-3.5', lg: 'pe-3.5',
xl: 'pr-3.5' xl: 'pe-3.5'
} }
} }
}, },
@@ -386,7 +387,7 @@ const formGroup = {
label: { label: {
wrapper: 'flex content-center justify-between', wrapper: 'flex content-center justify-between',
base: 'block text-sm font-medium text-gray-700 dark:text-gray-200', base: 'block text-sm font-medium text-gray-700 dark:text-gray-200',
required: `after:content-['*'] after:ml-0.5 after:text-red-500 dark:after:text-red-400` required: `after:content-['*'] after:ms-0.5 after:text-red-500 dark:after:text-red-400`
}, },
description: 'text-sm text-gray-500 dark:text-gray-400', description: 'text-sm text-gray-500 dark:text-gray-400',
container: 'mt-1 relative', container: 'mt-1 relative',
@@ -400,7 +401,7 @@ const textarea = {
default: { default: {
size: 'sm', size: 'sm',
color: 'white', color: 'white',
variant: 'outline', variant: 'outline'
} }
} }
@@ -437,7 +438,7 @@ const selectMenu = {
container: 'flex items-center gap-2 min-w-0', container: 'flex items-center gap-2 min-w-0',
active: 'bg-gray-100 dark:bg-gray-900', active: 'bg-gray-100 dark:bg-gray-900',
inactive: '', inactive: '',
selected: 'pr-7', selected: 'pe-7',
disabled: 'cursor-not-allowed opacity-50', disabled: 'cursor-not-allowed opacity-50',
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5', empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
icon: { icon: {
@@ -446,8 +447,8 @@ const selectMenu = {
inactive: 'text-gray-400 dark:text-gray-500' inactive: 'text-gray-400 dark:text-gray-500'
}, },
selectedIcon: { selectedIcon: {
wrapper: 'absolute inset-y-0 right-0 flex items-center', wrapper: 'absolute inset-y-0 end-0 flex items-center',
padding: 'pr-2', padding: 'pe-2',
base: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0' base: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0'
}, },
avatar: { avatar: {
@@ -458,6 +459,7 @@ const selectMenu = {
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full' base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
} }
}, },
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
transition: { transition: {
leaveActiveClass: 'transition ease-in duration-100', leaveActiveClass: 'transition ease-in duration-100',
leaveFromClass: 'opacity-100', leaveFromClass: 'opacity-100',
@@ -473,40 +475,90 @@ const selectMenu = {
const radio = { const radio = {
wrapper: 'relative flex items-start', wrapper: 'relative flex items-start',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent', base: 'h-4 w-4 dark:checked:bg-current dark:checked:border-transparent disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
color: 'text-{color}-500 dark:text-{color}-400',
background: 'bg-white dark:bg-gray-900',
border: 'border border-gray-300 dark:border-gray-700',
ring: 'focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
label: 'font-medium text-gray-700 dark:text-gray-200', label: 'font-medium text-gray-700 dark:text-gray-200',
required: 'text-red-500 dark:text-red-400', required: 'text-red-500 dark:text-red-400',
help: 'text-gray-500 dark:text-gray-400' help: 'text-gray-500 dark:text-gray-400',
default: {
color: 'primary'
}
} }
const checkbox = { const checkbox = {
wrapper: 'relative flex items-start', wrapper: 'relative flex items-start',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent dark:indeterminate:bg-current dark:indeterminate:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent', base: 'h-4 w-4 dark:checked:bg-current dark:checked:border-transparent dark:indeterminate:bg-current dark:indeterminate:border-transparent disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
rounded: 'rounded', rounded: 'rounded',
color: 'text-{color}-500 dark:text-{color}-400',
background: 'bg-white dark:bg-gray-900',
border: 'border border-gray-300 dark:border-gray-700',
ring: 'focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
label: 'font-medium text-gray-700 dark:text-gray-200', label: 'font-medium text-gray-700 dark:text-gray-200',
required: 'text-red-500 dark:text-red-400', required: 'text-red-500 dark:text-red-400',
help: 'text-gray-500 dark:text-gray-400' help: 'text-gray-500 dark:text-gray-400',
default: {
color: 'primary'
}
} }
const toggle = { const toggle = {
base: 'relative inline-flex flex-shrink-0 h-5 w-9 border-2 border-transparent rounded-full cursor-pointer disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900', base: 'relative inline-flex h-5 w-9 flex-shrink-0 border-2 border-transparent disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none',
active: 'bg-primary-500 dark:bg-primary-400', rounded: 'rounded-full',
ring: 'focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
active: 'bg-{color}-500 dark:bg-{color}-400',
inactive: 'bg-gray-200 dark:bg-gray-700', inactive: 'bg-gray-200 dark:bg-gray-700',
container: { container: {
base: 'pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white dark:bg-gray-900 shadow transform ring-0 transition ease-in-out duration-200', base: 'pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white dark:bg-gray-900 shadow transform ring-0 transition ease-in-out duration-200',
active: 'translate-x-4', active: 'translate-x-4 rtl:-translate-x-4',
inactive: 'translate-x-0' inactive: 'translate-x-0 rtl:-translate-x-0'
}, },
icon: { icon: {
base: 'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity', base: 'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity',
active: 'opacity-100 ease-in duration-200', active: 'opacity-100 ease-in duration-200',
inactive: 'opacity-0 ease-out duration-100', inactive: 'opacity-0 ease-out duration-100',
on: 'h-3 w-3 text-primary-500 dark:text-primary-400', on: 'h-3 w-3 text-{color}-500 dark:text-{color}-400',
off: 'h-3 w-3 text-gray-400 dark:text-gray-500' off: 'h-3 w-3 text-gray-400 dark:text-gray-500'
}, },
default: { default: {
onIcon: null, onIcon: null,
offIcon: null offIcon: null,
color: 'primary'
}
}
const range = {
wrapper: 'relative w-full',
base: 'w-full absolute appearance-none cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none [&::-webkit-slider-runnable-track]:h-full [&::-moz-slider-runnable-track]:h-full',
background: 'bg-gray-200 dark:bg-gray-700',
rounded: 'rounded-lg',
ring: 'focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
progress: {
base: 'absolute inset-0 h-full pointer-events-none',
rounded: 'rounded-s-lg',
background: 'bg-{color}-500 dark:bg-{color}-400'
},
thumb: {
base: `[&::-webkit-slider-thumb]:relative [&::-moz-range-thumb]:relative [&::-webkit-slider-thumb]:z-[1] [&::-moz-range-thumb]:z-[1] [&::-webkit-slider-thumb]:appearance-none [&::-moz-range-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0`,
color: 'text-{color}-500 dark:text-{color}-400',
background: '[&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:dark:bg-gray-900 [&::-moz-range-thumb]:bg-current',
ring: '[&::-webkit-slider-thumb]:ring-2 [&::-webkit-slider-thumb]:ring-current',
size: {
sm: '[&::-webkit-slider-thumb]:h-3 [&::-moz-range-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-moz-range-thumb]:w-3 [&::-webkit-slider-thumb]:-mt-1 [&::-moz-range-thumb]:-mt-1',
md: '[&::-webkit-slider-thumb]:h-4 [&::-moz-range-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-moz-range-thumb]:w-4 [&::-webkit-slider-thumb]:-mt-1 [&::-moz-range-thumb]:-mt-1',
lg: '[&::-webkit-slider-thumb]:h-5 [&::-moz-range-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-moz-range-thumb]:w-5 [&::-webkit-slider-thumb]:-mt-1 [&::-moz-range-thumb]:-mt-1'
}
},
size: {
sm: 'h-1',
md: 'h-2',
lg: 'h-3'
},
default: {
size: 'md',
color: 'primary'
} }
} }
@@ -572,7 +624,7 @@ const verticalNavigation = {
size: '3xs' size: '3xs'
}, },
badge: { badge: {
base: 'relative ml-auto inline-block py-0.5 px-2 text-xs rounded-md -mr-1 -my-0.5', base: 'relative ms-auto inline-block py-0.5 px-2 text-xs rounded-md -me-1 -my-0.5',
active: 'bg-white dark:bg-gray-900', active: 'bg-white dark:bg-gray-900',
inactive: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white group-hover:bg-white dark:group-hover:bg-gray-900' inactive: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white group-hover:bg-white dark:group-hover:bg-gray-900'
} }
@@ -588,11 +640,11 @@ const commandPalette = {
height: 'h-12', height: 'h-12',
size: 'sm:text-sm', size: 'sm:text-sm',
icon: { icon: {
base: 'pointer-events-none absolute left-4 text-gray-400 dark:text-gray-500', base: 'pointer-events-none absolute start-4 text-gray-400 dark:text-gray-500',
size: 'h-4 w-4', size: 'h-4 w-4',
padding: 'pl-10' padding: 'ps-10'
}, },
closeButton: 'absolute right-4' closeButton: 'absolute end-4'
}, },
emptyState: { emptyState: {
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14', wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
@@ -649,7 +701,7 @@ const commandPalette = {
const pagination = { const pagination = {
wrapper: 'flex items-center -space-x-px', wrapper: 'flex items-center -space-x-px',
base: '', base: '',
rounded: 'first:rounded-l-md last:rounded-r-md', rounded: 'first:rounded-s-md last:rounded-e-md',
default: { default: {
size: 'sm', size: 'sm',
activeButton: { activeButton: {
@@ -660,11 +712,13 @@ const pagination = {
}, },
prevButton: { prevButton: {
color: 'white', color: 'white',
class: 'rtl:[&_span:first-child]:rotate-180',
icon: 'i-heroicons-chevron-left-20-solid' icon: 'i-heroicons-chevron-left-20-solid'
}, },
nextButton: { nextButton: {
color: 'white', color: 'white',
icon: 'i-heroicons-chevron-right-20-solid' class: 'rtl:[&_span:last-child]:rotate-180',
icon: 'i-heroicons-chevron-right-20-solid '
} }
} }
} }
@@ -676,10 +730,11 @@ const modal = {
inner: 'fixed inset-0 overflow-y-auto', inner: 'fixed inset-0 overflow-y-auto',
container: 'flex min-h-full items-end sm:items-center justify-center text-center', container: 'flex min-h-full items-end sm:items-center justify-center text-center',
padding: 'p-4 sm:p-0', padding: 'p-4 sm:p-0',
base: 'relative text-left overflow-hidden sm:my-8 w-full flex flex-col', base: 'relative text-left rtl:text-right overflow-hidden sm:my-8 w-full flex flex-col',
overlay: { overlay: {
base: 'fixed inset-0 transition-opacity', base: 'fixed inset-0 transition-opacity',
background: 'bg-gray-200/75 dark:bg-gray-800/75', background: 'bg-gray-200/75 dark:bg-gray-800/75',
// Syntax for `<TransitionRoot>` component https://headlessui.com/vue/transition#basic-example
transition: { transition: {
enter: 'ease-out duration-300', enter: 'ease-out duration-300',
enterFrom: 'opacity-0', enterFrom: 'opacity-0',
@@ -695,6 +750,7 @@ const modal = {
shadow: 'shadow-xl', shadow: 'shadow-xl',
width: 'sm:max-w-lg', width: 'sm:max-w-lg',
height: '', height: '',
// Syntax for `<TransitionRoot>` component https://headlessui.com/vue/transition#basic-example
transition: { transition: {
enter: 'ease-out duration-300', enter: 'ease-out duration-300',
enterFrom: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95', enterFrom: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95',
@@ -710,6 +766,7 @@ const slideover = {
overlay: { overlay: {
base: 'fixed inset-0 transition-opacity', base: 'fixed inset-0 transition-opacity',
background: 'bg-gray-200/75 dark:bg-gray-800/75', background: 'bg-gray-200/75 dark:bg-gray-800/75',
// Syntax for `<TransitionRoot>` component https://headlessui.com/vue/transition#basic-example
transition: { transition: {
enter: 'ease-in-out duration-500', enter: 'ease-in-out duration-500',
enterFrom: 'opacity-0', enterFrom: 'opacity-0',
@@ -726,6 +783,7 @@ const slideover = {
padding: '', padding: '',
shadow: 'shadow-xl', shadow: 'shadow-xl',
width: 'w-screen max-w-md', width: 'w-screen max-w-md',
// Syntax for `<TransitionRoot>` component https://headlessui.com/vue/transition#basic-example
transition: { transition: {
enter: 'transform transition ease-in-out duration-300', enter: 'transform transition ease-in-out duration-300',
leave: 'transform transition ease-in-out duration-200' leave: 'transform transition ease-in-out duration-200'
@@ -737,11 +795,13 @@ const tooltip = {
container: 'z-20', container: 'z-20',
width: 'max-w-xs', width: 'max-w-xs',
background: 'bg-white dark:bg-gray-900', background: 'bg-white dark:bg-gray-900',
color: 'text-gray-900 dark:text-white',
shadow: 'shadow', shadow: 'shadow',
rounded: 'rounded', rounded: 'rounded',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800', ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
base: 'invisible lg:visible h-6 px-2 py-1 text-xs font-normal truncate', base: 'invisible lg:visible h-6 px-2 py-1 text-xs font-normal truncate',
shortcuts: 'hidden md:inline-flex flex-shrink-0 gap-0.5', shortcuts: 'hidden md:inline-flex flex-shrink-0 gap-0.5',
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
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',
@@ -764,6 +824,7 @@ const popover = {
rounded: 'rounded-md', rounded: 'rounded-md',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800', ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
base: 'overflow-hidden focus:outline-none', base: 'overflow-hidden focus:outline-none',
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
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',
@@ -786,6 +847,7 @@ const contextMenu = {
rounded: 'rounded-md', rounded: 'rounded-md',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800', ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
base: 'overflow-hidden focus:outline-none', base: 'overflow-hidden focus:outline-none',
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
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',
@@ -819,9 +881,10 @@ const notification = {
size: 'md' size: 'md'
}, },
progress: { progress: {
base: 'absolute bottom-0 left-0 right-0 h-1', base: 'absolute bottom-0 end-0 start-0 h-1',
background: 'bg-{color}-500 dark:bg-{color}-400' background: 'bg-{color}-500 dark:bg-{color}-400'
}, },
// Syntax for `<Transition>` component https://vuejs.org/guide/built-ins/transition.html#css-based-transitions
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',
@@ -848,7 +911,7 @@ const notification = {
const notifications = { const notifications = {
wrapper: 'fixed flex flex-col justify-end z-[55]', wrapper: 'fixed flex flex-col justify-end z-[55]',
position: 'bottom-0 right-0', position: 'bottom-0 end-0',
width: 'w-full sm:w-96', width: 'w-full sm:w-96',
container: 'px-4 sm:px-6 py-6 space-y-3 overflow-y-auto' container: 'px-4 sm:px-6 py-6 space-y-3 overflow-y-auto'
} }
@@ -871,6 +934,7 @@ export default {
checkbox, checkbox,
radio, radio,
toggle, toggle,
range,
card, card,
container, container,
skeleton, skeleton,

View File

@@ -3,7 +3,7 @@
<table :class="[ui.base, ui.divide]"> <table :class="[ui.base, ui.divide]">
<thead :class="ui.thead"> <thead :class="ui.thead">
<tr :class="ui.tr.base"> <tr :class="ui.tr.base">
<th v-if="modelValue" scope="col" class="pl-4"> <th v-if="modelValue" scope="col" class="ps-4">
<UCheckbox :checked="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" @change="selected = $event.target.checked ? rows : []" /> <UCheckbox :checked="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" @change="selected = $event.target.checked ? rows : []" />
</th> </th>
@@ -12,7 +12,7 @@
<UButton <UButton
v-if="column.sortable" v-if="column.sortable"
v-bind="{ ...ui.default.sortButton, ...sortButton }" v-bind="{ ...ui.default.sortButton, ...sortButton }"
:icon="(!sort.column || sort.column !== column.key) ? sortButton.icon : sort.direction === 'asc' ? sortAscIcon : sortDescIcon" :icon="(!sort.column || sort.column !== column.key) ? (sortButton.icon || ui.default.sortButton.icon) : sort.direction === 'asc' ? sortAscIcon : sortDescIcon"
:label="column[columnAttribute]" :label="column[columnAttribute]"
@click="onSort(column)" @click="onSort(column)"
/> />
@@ -23,12 +23,12 @@
</thead> </thead>
<tbody :class="ui.tbody"> <tbody :class="ui.tbody">
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected]"> <tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected]">
<td v-if="modelValue" class="pl-4"> <td v-if="modelValue" class="ps-4">
<UCheckbox v-model="selected" :value="row" /> <UCheckbox v-model="selected" :value="row" />
</td> </td>
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]"> <td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]">
<slot :name="`${column.key}-data`" :column="column" :row="row"> <slot :name="`${column.key}-data`" :column="column" :row="row" :index="index">
{{ row[column.key] }} {{ row[column.key] }}
</slot> </slot>
</td> </td>
@@ -77,7 +77,7 @@ import appConfig from '#build/app.config'
// const appConfig = useAppConfig() // const appConfig = useAppConfig()
function defaultComparator<T>(a: T, z: T): boolean { function defaultComparator<T> (a: T, z: T): boolean {
return a === z return a === z
} }
@@ -188,7 +188,13 @@ export default defineComponent({
function onSort (column) { function onSort (column) {
if (sort.value.column === column.key) { if (sort.value.column === column.key) {
sort.value.direction = sort.value.direction === 'asc' ? 'desc' : 'asc' const direction = !column.direction || column.direction === 'asc' ? 'desc' : 'asc'
if (sort.value.direction === direction) {
sort.value = defu({}, props.sort, { column: null, direction: 'asc' })
} else {
sort.value.direction = sort.value.direction === 'asc' ? 'desc' : 'asc'
}
} else { } else {
sort.value = { column: column.key, direction: column.direction || 'asc' } sort.value = { column: column.key, direction: column.direction || 'asc' }
} }

View File

@@ -3,7 +3,9 @@
<img v-if="url && !error" :class="avatarClass" :src="url" :alt="alt" :onerror="() => onError()"> <img v-if="url && !error" :class="avatarClass" :src="url" :alt="alt" :onerror="() => onError()">
<span v-else-if="text || placeholder" :class="ui.placeholder">{{ text || placeholder }}</span> <span v-else-if="text || placeholder" :class="ui.placeholder">{{ text || placeholder }}</span>
<span v-if="chipColor" :class="chipClass" /> <span v-if="chipColor" :class="chipClass">
{{ chipText }}
</span>
<slot /> <slot />
</span> </span>
</template> </template>
@@ -55,6 +57,10 @@ export default defineComponent({
return Object.keys(appConfig.ui.avatar.chip.position).includes(value) return Object.keys(appConfig.ui.avatar.chip.position).includes(value)
} }
}, },
chipText: {
type: [String, Number],
default: null
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.avatar>>, type: Object as PropType<Partial<typeof appConfig.ui.avatar>>,
default: () => appConfig.ui.avatar default: () => appConfig.ui.avatar

View File

@@ -32,15 +32,15 @@ export default defineComponent({
const children = computed(() => getSlotsChildren(slots)) const children = computed(() => getSlotsChildren(slots))
const rounded = computed(() => ({ const rounded = computed(() => ({
'rounded-none': { left: 'rounded-l-none', right: 'rounded-r-none' }, 'rounded-none': { left: 'rounded-s-none', right: 'rounded-e-none' },
'rounded-sm': { left: 'rounded-l-sm', right: 'rounded-r-sm' }, 'rounded-sm': { left: 'rounded-s-sm', right: 'rounded-e-sm' },
rounded: { left: 'rounded-l', right: 'rounded-r' }, rounded: { left: 'rounded-s', right: 'rounded-e' },
'rounded-md': { left: 'rounded-l-md', right: 'rounded-r-md' }, 'rounded-md': { left: 'rounded-s-md', right: 'rounded-e-md' },
'rounded-lg': { left: 'rounded-l-lg', right: 'rounded-r-lg' }, 'rounded-lg': { left: 'rounded-s-lg', right: 'rounded-e-lg' },
'rounded-xl': { left: 'rounded-l-xl', right: 'rounded-r-xl' }, 'rounded-xl': { left: 'rounded-s-xl', right: 'rounded-e-xl' },
'rounded-2xl': { left: 'rounded-l-2xl', right: 'rounded-r-2xl' }, 'rounded-2xl': { left: 'rounded-s-2xl', right: 'rounded-e-2xl' },
'rounded-3xl': { left: 'rounded-l-3xl', right: 'rounded-r-3xl' }, 'rounded-3xl': { left: 'rounded-s-3xl', right: 'rounded-e-3xl' },
'rounded-full': { left: 'rounded-l-full', right: 'rounded-r-full' } 'rounded-full': { left: 'rounded-s-full', right: 'rounded-e-full' }
}[ui.value.rounded])) }[ui.value.rounded]))
const clones = computed(() => children.value.map((node, index) => { const clones = computed(() => children.value.map((node, index) => {

View File

@@ -1,6 +1,6 @@
<template> <template>
<Menu v-slot="{ open }" as="div" :class="ui.wrapper" @mouseleave="onMouseLeave"> <HMenu v-slot="{ open }" as="div" :class="ui.wrapper" @mouseleave="onMouseLeave">
<MenuButton <HMenuButton
ref="trigger" ref="trigger"
as="div" as="div"
:disabled="disabled" :disabled="disabled"
@@ -13,13 +13,13 @@
Open Open
</button> </button>
</slot> </slot>
</MenuButton> </HMenuButton>
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseover="onMouseOver"> <div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseover="onMouseOver">
<transition appear v-bind="ui.transition"> <Transition appear v-bind="ui.transition">
<MenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.height]" static> <HMenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.height]" static>
<div v-for="(subItems, index) of items" :key="index" :class="ui.padding"> <div v-for="(subItems, index) of items" :key="index" :class="ui.padding">
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled"> <HMenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
<ULinkCustom <ULinkCustom
v-bind="omit(item, ['label', 'icon', 'iconClass', 'avatar', 'shortcuts', 'click'])" v-bind="omit(item, ['label', 'icon', 'iconClass', 'avatar', 'shortcuts', 'click'])"
:class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]" :class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]"
@@ -36,19 +36,19 @@
</span> </span>
</slot> </slot>
</ULinkCustom> </ULinkCustom>
</MenuItem> </HMenuItem>
</div> </div>
</MenuItems> </HMenuItems>
</transition> </Transition>
</div> </div>
</Menu> </HMenu>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' import { defineComponent, ref, computed, onMounted } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { RouteLocationRaw } from 'vue-router' import type { RouteLocationRaw } from 'vue-router'
import { defineComponent, ref, computed, onMounted } from 'vue' import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue'
import { defu } from 'defu' import { defu } from 'defu'
import { omit } from 'lodash-es' import { omit } from 'lodash-es'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
@@ -67,11 +67,10 @@ import appConfig from '#build/app.config'
export default defineComponent({ export default defineComponent({
components: { components: {
// eslint-disable-next-line vue/no-reserved-component-names HMenu,
Menu, HMenuButton,
MenuButton, HMenuItems,
MenuItems, HMenuItem,
MenuItem,
UIcon, UIcon,
UAvatar, UAvatar,
UKbd, UKbd,

View File

@@ -3,7 +3,7 @@
<div class="flex items-center h-5"> <div class="flex items-center h-5">
<input <input
:id="name" :id="name"
v-model="isChecked" v-model="toggle"
:name="name" :name="name"
:required="required" :required="required"
:value="value" :value="value"
@@ -12,13 +12,11 @@
:indeterminate="indeterminate" :indeterminate="indeterminate"
type="checkbox" type="checkbox"
class="form-checkbox" class="form-checkbox"
:class="[ui.base, ui.rounded, ui.custom]" :class="inputClass"
v-bind="$attrs" v-bind="$attrs"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
> >
</div> </div>
<div v-if="label || $slots.label" class="ml-3 text-sm"> <div v-if="label || $slots.label" class="ms-3 text-sm">
<label :for="name" :class="ui.label"> <label :for="name" :class="ui.label">
<slot name="label">{{ label }}</slot> <slot name="label">{{ label }}</slot>
<span v-if="required" :class="ui.required">*</span> <span v-if="required" :class="ui.required">*</span>
@@ -34,6 +32,7 @@
import { computed, defineComponent } from 'vue' import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -80,19 +79,26 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
color: {
type: String,
default: () => appConfig.ui.checkbox.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
}
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.checkbox>>, type: Object as PropType<Partial<typeof appConfig.ui.checkbox>>,
default: () => appConfig.ui.checkbox default: () => appConfig.ui.checkbox
} }
}, },
emits: ['update:modelValue', 'focus', 'blur'], emits: ['update:modelValue'],
setup (props, { emit }) { setup (props, { emit }) {
// TODO: Remove // TODO: Remove
const appConfig = useAppConfig() const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.checkbox>>(() => defu({}, props.ui, appConfig.ui.checkbox)) const ui = computed<Partial<typeof appConfig.ui.checkbox>>(() => defu({}, props.ui, appConfig.ui.checkbox))
const isChecked = computed({ const toggle = computed({
get () { get () {
return props.modelValue return props.modelValue
}, },
@@ -101,10 +107,22 @@ export default defineComponent({
} }
}) })
const inputClass = computed(() => {
return classNames(
ui.value.base,
ui.value.rounded,
ui.value.background,
ui.value.border,
ui.value.ring.replaceAll('{color}', props.color),
ui.value.color.replaceAll('{color}', props.color)
)
})
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
isChecked toggle,
inputClass
} }
} }
}) })

View File

@@ -58,7 +58,7 @@ export default defineComponent({
if (props.error) { if (props.error) {
vProps.oldColor = node.props.color vProps.oldColor = node.props.color
vProps.color = 'red' vProps.color = 'red'
} else { } else if (vProps.oldColor) {
vProps.color = vProps.oldColor vProps.color = vProps.oldColor
} }

View File

@@ -13,8 +13,6 @@
:class="inputClass" :class="inputClass"
v-bind="$attrs" v-bind="$attrs"
@input="onInput" @input="onInput"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
> >
<slot /> <slot />
@@ -140,7 +138,7 @@ export default defineComponent({
default: () => appConfig.ui.input default: () => appConfig.ui.input
} }
}, },
emits: ['update:modelValue', 'focus', 'blur'], emits: ['update:modelValue'],
setup (props, { emit, slots }) { setup (props, { emit, slots }) {
// TODO: Remove // TODO: Remove
const appConfig = useAppConfig() const appConfig = useAppConfig()
@@ -156,7 +154,7 @@ export default defineComponent({
} }
const onInput = (event: InputEvent) => { const onInput = (event: InputEvent) => {
emit('update:modelValue', (event.target as any).value) emit('update:modelValue', (event.target as HTMLInputElement).value)
} }
onMounted(() => { onMounted(() => {

View File

@@ -3,20 +3,18 @@
<div class="flex items-center h-5"> <div class="flex items-center h-5">
<input <input
:id="`${name}-${value}`" :id="`${name}-${value}`"
v-model="isChecked" v-model="pick"
:name="name" :name="name"
:required="required" :required="required"
:value="value" :value="value"
:disabled="disabled" :disabled="disabled"
type="radio" type="radio"
class="form-radio" class="form-radio"
:class="[ui.base, ui.custom]" :class="inputClass"
v-bind="$attrs" v-bind="$attrs"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
> >
</div> </div>
<div v-if="label || $slots.label" class="ml-3 text-sm"> <div v-if="label || $slots.label" class="ms-3 text-sm">
<label :for="`${name}-${value}`" :class="ui.label"> <label :for="`${name}-${value}`" :class="ui.label">
<slot name="label">{{ label }}</slot> <slot name="label">{{ label }}</slot>
<span v-if="required" :class="ui.required">*</span> <span v-if="required" :class="ui.required">*</span>
@@ -32,6 +30,7 @@
import { computed, defineComponent } from 'vue' import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -70,19 +69,26 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
color: {
type: String,
default: () => appConfig.ui.radio.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
}
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.radio>>, type: Object as PropType<Partial<typeof appConfig.ui.radio>>,
default: () => appConfig.ui.radio default: () => appConfig.ui.radio
} }
}, },
emits: ['update:modelValue', 'focus', 'blur'], emits: ['update:modelValue'],
setup (props, { emit }) { setup (props, { emit }) {
// TODO: Remove // TODO: Remove
const appConfig = useAppConfig() const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.radio>>(() => defu({}, props.ui, appConfig.ui.radio)) const ui = computed<Partial<typeof appConfig.ui.radio>>(() => defu({}, props.ui, appConfig.ui.radio))
const isChecked = computed({ const pick = computed({
get () { get () {
return props.modelValue return props.modelValue
}, },
@@ -91,10 +97,21 @@ export default defineComponent({
} }
}) })
const inputClass = computed(() => {
return classNames(
ui.value.base,
ui.value.background,
ui.value.border,
ui.value.ring.replaceAll('{color}', props.color),
ui.value.color.replaceAll('{color}', props.color)
)
})
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
isChecked pick,
inputClass
} }
} }
}) })

View File

@@ -0,0 +1,148 @@
<template>
<div :class="wrapperClass">
<input
:id="name"
ref="input"
v-model.number="value"
:name="name"
:min="min"
:max="max"
:disabled="disabled"
:step="step"
type="range"
:class="[inputClass, thumbClass]"
v-bind="$attrs"
>
<span :class="progressClass" :style="progressStyle" />
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
export default defineComponent({
inheritAttrs: false,
props: {
modelValue: {
type: Number,
default: 0
},
name: {
type: String,
default: null
},
disabled: {
type: Boolean,
default: false
},
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 100
},
step: {
type: Number,
default: 1
},
size: {
type: String,
default: () => appConfig.ui.range.default.size,
validator (value: string) {
return Object.keys(appConfig.ui.range.size).includes(value)
}
},
color: {
type: String,
default: () => appConfig.ui.range.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
}
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.range>>,
default: () => appConfig.ui.range
}
},
emits: ['update:modelValue'],
setup (props, { emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.range>>(() => defu({}, props.ui, appConfig.ui.range))
const value = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const wrapperClass = computed(() => {
return classNames(
ui.value.wrapper,
ui.value.size[props.size]
)
})
const inputClass = computed(() => {
return classNames(
ui.value.base,
ui.value.background,
ui.value.rounded,
ui.value.ring.replaceAll('{color}', props.color),
ui.value.size[props.size]
)
})
const thumbClass = computed(() => {
return classNames(
ui.value.thumb.base,
// Intermediate class to allow thumb ring or background color (set to `current`) as it's impossible to safelist with arbitrary values
ui.value.thumb.color.replaceAll('{color}', props.color),
ui.value.thumb.ring,
ui.value.thumb.background,
ui.value.thumb.size[props.size]
)
})
const progressClass = computed(() => {
return classNames(
ui.value.progress.base,
ui.value.progress.rounded,
ui.value.progress.background.replaceAll('{color}', props.color),
ui.value.size[props.size]
)
})
const progressStyle = computed(() => {
return {
width: `${(props.modelValue / props.max) * 100}%`
}
})
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
value,
wrapperClass,
inputClass,
thumbClass,
progressClass,
progressStyle
}
}
})
</script>

View File

@@ -165,7 +165,7 @@ export default defineComponent({
default: () => appConfig.ui.select default: () => appConfig.ui.select
} }
}, },
emits: ['update:modelValue', 'focus', 'blur'], emits: ['update:modelValue'],
setup (props, { emit, slots }) { setup (props, { emit, slots }) {
// TODO: Remove // TODO: Remove
const appConfig = useAppConfig() const appConfig = useAppConfig()
@@ -173,7 +173,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.ui, appConfig.ui.select)) const ui = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.ui, appConfig.ui.select))
const onInput = (event: InputEvent) => { const onInput = (event: InputEvent) => {
emit('update:modelValue', (event.target as any).value) emit('update:modelValue', (event.target as HTMLInputElement).value)
} }
const guessOptionValue = (option: any) => { const guessOptionValue = (option: any) => {

View File

@@ -1,6 +1,6 @@
<template> <template>
<component <component
:is="searchable ? 'Combobox' : 'Listbox'" :is="searchable ? 'HCombobox' : 'HListbox'"
v-slot="{ open }" v-slot="{ open }"
:by="by" :by="by"
:name="name" :name="name"
@@ -21,7 +21,7 @@
> >
<component <component
:is="searchable ? 'ComboboxButton' : 'ListboxButton'" :is="searchable ? 'HComboboxButton' : 'HListboxButton'"
ref="trigger" ref="trigger"
as="div" as="div"
role="button" role="button"
@@ -51,9 +51,9 @@
</component> </component>
<div v-if="open" ref="container" :class="[ui.container, ui.width]"> <div v-if="open" ref="container" :class="[ui.container, ui.width]">
<transition v-bind="ui.transition"> <Transition v-bind="ui.transition">
<component :is="searchable ? 'ComboboxOptions' : 'ListboxOptions'" static :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.padding, ui.height]"> <component :is="searchable ? 'HComboboxOptions' : 'HListboxOptions'" static :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.padding, ui.height]">
<ComboboxInput <HComboboxInput
v-if="searchable" v-if="searchable"
ref="searchInput" ref="searchInput"
:display-value="() => query" :display-value="() => query"
@@ -65,7 +65,7 @@
@change="query = $event.target.value" @change="query = $event.target.value"
/> />
<component <component
:is="searchable ? 'ComboboxOption' : 'ListboxOption'" :is="searchable ? 'HComboboxOption' : 'HListboxOption'"
v-for="(option, index) in filteredOptions" v-for="(option, index) in filteredOptions"
v-slot="{ active, selected, disabled: optionDisabled }" v-slot="{ active, selected, disabled: optionDisabled }"
:key="index" :key="index"
@@ -95,7 +95,7 @@
</li> </li>
</component> </component>
<component :is="searchable ? 'ComboboxOption' : 'ListboxOption'" v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" as="template"> <component :is="searchable ? 'HComboboxOption' : 'HListboxOption'" v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" as="template">
<li :class="[ui.option.base, ui.option.rounded, ui.option.padding, ui.option.size, ui.option.color, active ? ui.option.active : ui.option.inactive]"> <li :class="[ui.option.base, ui.option.rounded, ui.option.padding, ui.option.size, ui.option.color, active ? ui.option.active : ui.option.inactive]">
<div :class="ui.option.container"> <div :class="ui.option.container">
<slot name="option-create" :option="queryOption" :active="active" :selected="selected"> <slot name="option-create" :option="queryOption" :active="active" :selected="selected">
@@ -110,7 +110,7 @@
</slot> </slot>
</p> </p>
</component> </component>
</transition> </Transition>
</div> </div>
</component> </component>
</template> </template>
@@ -118,8 +118,18 @@
<script lang="ts"> <script lang="ts">
import { ref, computed, watch, defineComponent } from 'vue' import { ref, computed, watch, defineComponent } from 'vue'
import type { PropType, ComponentPublicInstance } from 'vue' import type { PropType, ComponentPublicInstance } from 'vue'
import {
Combobox as HCombobox,
ComboboxButton as HComboboxButton,
ComboboxOptions as HComboboxOptions,
ComboboxOption as HComboboxOption,
ComboboxInput as HComboboxInput,
Listbox as HListbox,
ListboxButton as HListboxButton,
ListboxOptions as HListboxOptions,
ListboxOption as HListboxOption
} from '@headlessui/vue'
import { defu } from 'defu' import { defu } from 'defu'
import { Combobox, ComboboxButton, ComboboxOptions, ComboboxOption, ComboboxInput, Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/vue'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import { classNames } from '../../utils' import { classNames } from '../../utils'
@@ -134,15 +144,15 @@ import appConfig from '#build/app.config'
export default defineComponent({ export default defineComponent({
components: { components: {
Combobox, HCombobox,
ComboboxButton, HComboboxButton,
ComboboxOptions, HComboboxOptions,
ComboboxOption, HComboboxOption,
ComboboxInput, HComboboxInput,
Listbox, HListbox,
ListboxButton, HListboxButton,
ListboxOptions, HListboxOptions,
ListboxOption, HListboxOption,
UIcon, UIcon,
UAvatar UAvatar
}, },

View File

@@ -13,8 +13,6 @@
:class="textareaClass" :class="textareaClass"
v-bind="$attrs" v-bind="$attrs"
@input="onInput" @input="onInput"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
/> />
</div> </div>
</template> </template>
@@ -103,7 +101,7 @@ export default defineComponent({
default: () => appConfig.ui.textarea default: () => appConfig.ui.textarea
} }
}, },
emits: ['update:modelValue', 'focus', 'blur'], emits: ['update:modelValue'],
setup (props, { emit }) { setup (props, { emit }) {
const textarea = ref<HTMLTextAreaElement | null>(null) const textarea = ref<HTMLTextAreaElement | null>(null)
@@ -143,7 +141,7 @@ export default defineComponent({
const onInput = (event: InputEvent) => { const onInput = (event: InputEvent) => {
autoResize() autoResize()
emit('update:modelValue', (event.target as any).value) emit('update:modelValue', (event.target as HTMLInputElement).value)
} }
watch(() => props.modelValue, () => { watch(() => props.modelValue, () => {

View File

@@ -1,27 +1,28 @@
<template> <template>
<Switch <HSwitch
v-model="active" v-model="active"
:name="name" :name="name"
:disabled="disabled" :disabled="disabled"
:class="[active ? ui.active : ui.inactive, ui.base]" :class="switchClass"
> >
<span :class="[active ? ui.container.active : ui.container.inactive, ui.container.base]"> <span :class="[active ? ui.container.active : ui.container.inactive, ui.container.base]">
<span v-if="onIcon" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true"> <span v-if="onIcon" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
<UIcon :name="onIcon" :class="ui.icon.on" /> <UIcon :name="onIcon" :class="onIconClass" />
</span> </span>
<span v-if="offIcon" :class="[active ? ui.icon.inactive : ui.icon.active, ui.icon.base]" aria-hidden="true"> <span v-if="offIcon" :class="[active ? ui.icon.inactive : ui.icon.active, ui.icon.base]" aria-hidden="true">
<UIcon :name="offIcon" :class="ui.icon.off" /> <UIcon :name="offIcon" :class="offIconClass" />
</span> </span>
</span> </span>
</Switch> </HSwitch>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue' import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { Switch } from '@headlessui/vue' import { Switch as HSwitch } from '@headlessui/vue'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -31,8 +32,7 @@ import appConfig from '#build/app.config'
export default defineComponent({ export default defineComponent({
components: { components: {
// eslint-disable-next-line vue/no-reserved-component-names HSwitch,
Switch,
UIcon UIcon
}, },
props: { props: {
@@ -56,6 +56,13 @@ export default defineComponent({
type: String, type: String,
default: () => appConfig.ui.toggle.default.offIcon default: () => appConfig.ui.toggle.default.offIcon
}, },
color: {
type: String,
default: () => appConfig.ui.toggle.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
}
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.toggle>>, type: Object as PropType<Partial<typeof appConfig.ui.toggle>>,
default: () => appConfig.ui.toggle default: () => appConfig.ui.toggle
@@ -77,10 +84,34 @@ export default defineComponent({
} }
}) })
const switchClass = computed(()=>{
return classNames(
ui.value.base,
ui.value.rounded,
ui.value.ring.replaceAll('{color}', props.color),
(active.value ? ui.value.active : ui.value.inactive).replaceAll('{color}', props.color)
)
})
const onIconClass = computed(()=>{
return classNames(
ui.value.icon.on.replaceAll('{color}', props.color)
)
})
const offIconClass = computed(()=>{
return classNames(
ui.value.icon.off.replaceAll('{color}', props.color)
)
})
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
active active,
switchClass,
onIconClass,
offIconClass
} }
} }
}) })

View File

@@ -1,5 +1,5 @@
<template> <template>
<Combobox <HCombobox
:by="by" :by="by"
:model-value="modelValue" :model-value="modelValue"
:multiple="multiple" :multiple="multiple"
@@ -9,7 +9,7 @@
<div :class="ui.wrapper"> <div :class="ui.wrapper">
<div v-show="searchable" :class="ui.input.wrapper"> <div v-show="searchable" :class="ui.input.wrapper">
<UIcon v-if="iconName" :name="iconName" :class="iconClass" aria-hidden="true" /> <UIcon v-if="iconName" :name="iconName" :class="iconClass" aria-hidden="true" />
<ComboboxInput <HComboboxInput
ref="comboboxInput" ref="comboboxInput"
:value="query" :value="query"
:class="[ui.input.base, ui.input.size, ui.input.height, ui.input.padding, icon && ui.input.icon.padding]" :class="[ui.input.base, ui.input.size, ui.input.height, ui.input.padding, icon && ui.input.icon.padding]"
@@ -27,7 +27,7 @@
/> />
</div> </div>
<ComboboxOptions <HComboboxOptions
v-if="groups.length" v-if="groups.length"
static static
hold hold
@@ -49,7 +49,7 @@
<slot :name="name" v-bind="slotData" /> <slot :name="name" v-bind="slotData" />
</template> </template>
</CommandPaletteGroup> </CommandPaletteGroup>
</ComboboxOptions> </HComboboxOptions>
<template v-else-if="emptyState"> <template v-else-if="emptyState">
<slot name="empty-state"> <slot name="empty-state">
@@ -62,12 +62,12 @@
</slot> </slot>
</template> </template>
</div> </div>
</Combobox> </HCombobox>
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, computed, watch, onMounted, defineComponent } from 'vue' import { ref, computed, watch, onMounted, defineComponent } from 'vue'
import { Combobox, ComboboxInput, ComboboxOptions } from '@headlessui/vue' import { Combobox as HCombobox, ComboboxInput as HComboboxInput, ComboboxOptions as HComboboxOptions } from '@headlessui/vue'
import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue' import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue'
import { useDebounceFn } from '@vueuse/core' import { useDebounceFn } from '@vueuse/core'
import { useFuse } from '@vueuse/integrations/useFuse' import { useFuse } from '@vueuse/integrations/useFuse'
@@ -78,8 +78,8 @@ import type { Group, Command } from '../../types/command-palette'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import type { Button } from '../../types/button' import type { Button } from '../../types/button'
import { classNames } from '../../utils'
import CommandPaletteGroup from './CommandPaletteGroup.vue' import CommandPaletteGroup from './CommandPaletteGroup.vue'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -89,9 +89,9 @@ import appConfig from '#build/app.config'
export default defineComponent({ export default defineComponent({
components: { components: {
Combobox, HCombobox,
ComboboxInput, HComboboxInput,
ComboboxOptions, HComboboxOptions,
UIcon, UIcon,
UButton, UButton,
CommandPaletteGroup CommandPaletteGroup

View File

@@ -5,7 +5,7 @@
</h2> </h2>
<div :class="ui.group.container" role="listbox" :aria-label="group[groupAttribute]"> <div :class="ui.group.container" role="listbox" :aria-label="group[groupAttribute]">
<ComboboxOption <HComboboxOption
v-for="(command, index) of group.commands" v-for="(command, index) of group.commands"
:key="`${group.key}-${index}`" :key="`${group.key}-${index}`"
v-slot="{ active, selected }" v-slot="{ active, selected }"
@@ -50,7 +50,7 @@
<span v-else-if="!command.disabled && group.inactive" :class="ui.group.inactive">{{ group.inactive }}</span> <span v-else-if="!command.disabled && group.inactive" :class="ui.group.inactive">{{ group.inactive }}</span>
</slot> </slot>
</div> </div>
</ComboboxOption> </HComboboxOption>
</div> </div>
</div> </div>
</template> </template>
@@ -58,7 +58,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue' import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { ComboboxOption } from '@headlessui/vue' import { ComboboxOption as HComboboxOption } from '@headlessui/vue'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue' import UKbd from '../elements/Kbd.vue'
@@ -71,7 +71,7 @@ import appConfig from '#build/app.config'
export default defineComponent({ export default defineComponent({
components: { components: {
ComboboxOption, HComboboxOption,
UIcon, UIcon,
UAvatar, UAvatar,
UKbd UKbd

View File

@@ -1,10 +1,10 @@
<template> <template>
<div v-if="isOpen" ref="container" :class="[ui.container, ui.width]"> <div v-if="isOpen" ref="container" :class="[ui.container, ui.width]">
<transition appear v-bind="ui.transition"> <Transition appear v-bind="ui.transition">
<div :class="[ui.base, ui.ring, ui.rounded, ui.shadow, ui.background]"> <div :class="[ui.base, ui.ring, ui.rounded, ui.shadow, ui.background]">
<slot /> <slot />
</div> </div>
</transition> </Transition>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,6 @@
<template> <template>
<TransitionRoot :appear="appear" :show="isOpen" as="template"> <TransitionRoot :appear="appear" :show="isOpen" as="template">
<Dialog :class="ui.wrapper" @close="close"> <HDialog :class="ui.wrapper" @close="close">
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition"> <TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
<div :class="[ui.overlay.base, ui.overlay.background]" /> <div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild> </TransitionChild>
@@ -8,13 +8,13 @@
<div :class="ui.inner"> <div :class="ui.inner">
<div :class="[ui.container, ui.padding]"> <div :class="[ui.container, ui.padding]">
<TransitionChild as="template" :appear="appear" v-bind="ui.transition"> <TransitionChild as="template" :appear="appear" v-bind="ui.transition">
<DialogPanel :class="[ui.base, ui.width, ui.height, ui.background, ui.ring, ui.rounded, ui.shadow]"> <HDialogPanel :class="[ui.base, ui.width, ui.height, ui.background, ui.ring, ui.rounded, ui.shadow]">
<slot /> <slot />
</DialogPanel> </HDialogPanel>
</TransitionChild> </TransitionChild>
</div> </div>
</div> </div>
</Dialog> </HDialog>
</TransitionRoot> </TransitionRoot>
</template> </template>
@@ -22,7 +22,7 @@
import { computed, defineComponent } from 'vue' import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue' import { Dialog as HDialog, DialogPanel as HDialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -32,9 +32,8 @@ import appConfig from '#build/app.config'
export default defineComponent({ export default defineComponent({
components: { components: {
// eslint-disable-next-line vue/no-reserved-component-names HDialog,
Dialog, HDialogPanel,
DialogPanel,
TransitionRoot, TransitionRoot,
TransitionChild TransitionChild
}, },

View File

@@ -1,5 +1,5 @@
<template> <template>
<transition appear v-bind="ui.transition"> <Transition appear v-bind="ui.transition">
<div :class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]" @mouseover="onMouseover" @mouseleave="onMouseleave"> <div :class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]" @mouseover="onMouseover" @mouseleave="onMouseleave">
<div :class="[ui.container, ui.rounded, ui.ring]"> <div :class="[ui.container, ui.rounded, ui.ring]">
<div :class="ui.padding"> <div :class="ui.padding">
@@ -31,7 +31,7 @@
<div v-if="timeout" :class="progressClass" :style="progressStyle" /> <div v-if="timeout" :class="progressClass" :style="progressStyle" />
</div> </div>
</div> </div>
</transition> </Transition>
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@@ -1,6 +1,6 @@
<template> <template>
<Popover v-slot="{ open, close }" :class="ui.wrapper" @mouseleave="onMouseLeave"> <HPopover v-slot="{ open, close }" :class="ui.wrapper" @mouseleave="onMouseLeave">
<PopoverButton <HPopoverButton
ref="trigger" ref="trigger"
as="div" as="div"
:disabled="disabled" :disabled="disabled"
@@ -13,23 +13,23 @@
Open Open
</button> </button>
</slot> </slot>
</PopoverButton> </HPopoverButton>
<div v-if="open" ref="container" :class="[ui.container, ui.width]" @mouseover="onMouseOver"> <div v-if="open" ref="container" :class="[ui.container, ui.width]" @mouseover="onMouseOver">
<transition appear v-bind="ui.transition"> <Transition appear v-bind="ui.transition">
<PopoverPanel :class="[ui.base, ui.background, ui.ring, ui.rounded, ui.shadow]" static> <HPopoverPanel :class="[ui.base, ui.background, ui.ring, ui.rounded, ui.shadow]" static>
<slot name="panel" :open="open" :close="close" /> <slot name="panel" :open="open" :close="close" />
</PopoverPanel> </HPopoverPanel>
</transition> </Transition>
</div> </div>
</Popover> </HPopover>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, ref, onMounted, defineComponent } from 'vue' import { computed, ref, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue' import { Popover as HPopover, PopoverButton as HPopoverButton, PopoverPanel as HPopoverPanel } from '@headlessui/vue'
import { usePopper } from '../../composables/usePopper' import { usePopper } from '../../composables/usePopper'
import type { PopperOptions } from '../../types' import type { PopperOptions } from '../../types'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
@@ -41,9 +41,9 @@ import appConfig from '#build/app.config'
export default defineComponent({ export default defineComponent({
components: { components: {
Popover, HPopover,
PopoverButton, HPopoverButton,
PopoverPanel HPopoverPanel
}, },
props: { props: {
mode: { mode: {

View File

@@ -1,16 +1,16 @@
<template> <template>
<TransitionRoot as="template" :appear="appear" :show="isOpen"> <TransitionRoot as="template" :appear="appear" :show="isOpen">
<Dialog :class="[ui.wrapper, { 'justify-end': side === 'right' }]" @close="close"> <HDialog :class="[ui.wrapper, { 'justify-end': side === 'right' }]" @close="close">
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition"> <TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
<div :class="[ui.overlay.base, ui.overlay.background]" /> <div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild> </TransitionChild>
<TransitionChild as="template" :appear="appear" v-bind="transitionClass"> <TransitionChild as="template" :appear="appear" v-bind="transitionClass">
<DialogPanel :class="[ui.base, ui.width, ui.background, ui.ring, ui.padding]"> <HDialogPanel :class="[ui.base, ui.width, ui.background, ui.ring, ui.padding]">
<slot /> <slot />
</DialogPanel> </HDialogPanel>
</TransitionChild> </TransitionChild>
</Dialog> </HDialog>
</TransitionRoot> </TransitionRoot>
</template> </template>
@@ -18,7 +18,7 @@
import { computed, defineComponent } from 'vue' import { computed, defineComponent } from 'vue'
import type { WritableComputedRef, PropType } from 'vue' import type { WritableComputedRef, PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue' import { Dialog as HDialog, DialogPanel as HDialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -28,9 +28,8 @@ import appConfig from '#build/app.config'
export default defineComponent({ export default defineComponent({
components: { components: {
// eslint-disable-next-line vue/no-reserved-component-names HDialog,
Dialog, HDialogPanel,
DialogPanel,
TransitionRoot, TransitionRoot,
TransitionChild TransitionChild
}, },

View File

@@ -1,12 +1,12 @@
<template> <template>
<div ref="trigger" :class="ui.wrapper" @mouseover="onMouseOver" @mouseleave="onMouseLeave"> <div ref="trigger" :class="ui.wrapper" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
<slot :open="open"> <slot :open="open">
hover Hover
</slot> </slot>
<div v-if="open && !prevent" ref="container" :class="[ui.container, ui.width]"> <div v-if="open && !prevent" ref="container" :class="[ui.container, ui.width]">
<transition appear v-bind="ui.transition"> <Transition appear v-bind="ui.transition">
<div :class="[ui.base, ui.background, ui.rounded, ui.shadow, ui.ring]"> <div :class="[ui.base, ui.background, ui.color, ui.rounded, ui.shadow, ui.ring]">
<slot name="text"> <slot name="text">
{{ text }} {{ text }}
</slot> </slot>
@@ -18,7 +18,7 @@
</Ukbd> </Ukbd>
</span> </span>
</div> </div>
</transition> </Transition>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,7 +1,7 @@
import { ref, computed } from 'vue'
import type { ComputedRef, WatchSource } from 'vue' import type { ComputedRef, WatchSource } from 'vue'
import { logicAnd, logicNot } from '@vueuse/math' import { logicAnd, logicNot } from '@vueuse/math'
import { useEventListener } from '@vueuse/core' import { useEventListener, useDebounceFn } from '@vueuse/core'
import { computed } from 'vue'
import { useShortcuts } from './useShortcuts' import { useShortcuts } from './useShortcuts'
export interface ShortcutConfig { export interface ShortcutConfig {
@@ -14,9 +14,14 @@ export interface ShortcutsConfig {
[key: string]: ShortcutConfig | Function [key: string]: ShortcutConfig | Function
} }
export interface ShortcutsOptions {
chainDelay?: number
}
interface Shortcut { interface Shortcut {
handler: Function handler: Function
condition: ComputedRef<Boolean> condition: ComputedRef<Boolean>
chained: boolean
// KeyboardEvent attributes // KeyboardEvent attributes
key: string key: string
ctrlKey: boolean ctrlKey: boolean
@@ -27,18 +32,43 @@ interface Shortcut {
// keyCode?: number // keyCode?: number
} }
export const defineShortcuts = (config: ShortcutsConfig) => { export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptions = {}) => {
const { macOS, usingInput } = useShortcuts() const { macOS, usingInput } = useShortcuts()
let shortcuts: Shortcut[] = [] let shortcuts: Shortcut[] = []
const chainedInputs = ref([])
const clearChainedInput = () => {
chainedInputs.value.splice(0, chainedInputs.value.length)
}
const debouncedClearChainedInput = useDebounceFn(clearChainedInput, options.chainDelay ?? 800)
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
// Input autocomplete triggers a keydown event // Input autocomplete triggers a keydown event
if (!e.key) { return } if (!e.key) { return }
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key) const alphabeticalKey = /^[a-z]{1}$/i.test(e.key)
for (const shortcut of shortcuts) { let chainedKey
chainedInputs.value.push(e.key)
// try matching a chained shortcut
if (chainedInputs.value.length >= 2) {
chainedKey = chainedInputs.value.slice(-2).join('-')
for (const shortcut of shortcuts.filter(s => s.chained)) {
if (shortcut.key !== chainedKey) { continue }
if (shortcut.condition.value) {
e.preventDefault()
shortcut.handler()
}
clearChainedInput()
return
}
}
// try matching a standard shortcut
for (const shortcut of shortcuts.filter(s => !s.chained)) {
if (e.key.toLowerCase() !== shortcut.key) { continue } if (e.key.toLowerCase() !== shortcut.key) { continue }
if (e.metaKey !== shortcut.metaKey) { continue } if (e.metaKey !== shortcut.metaKey) { continue }
if (e.ctrlKey !== shortcut.ctrlKey) { continue } if (e.ctrlKey !== shortcut.ctrlKey) { continue }
@@ -52,8 +82,11 @@ export const defineShortcuts = (config: ShortcutsConfig) => {
e.preventDefault() e.preventDefault()
shortcut.handler() shortcut.handler()
} }
clearChainedInput()
return return
} }
debouncedClearChainedInput()
} }
// Map config to full detailled shortcuts // Map config to full detailled shortcuts
@@ -63,15 +96,34 @@ export const defineShortcuts = (config: ShortcutsConfig) => {
} }
// Parse key and modifiers // Parse key and modifiers
const keySplit = key.toLowerCase().split('_').map(k => k) let shortcut: Partial<Shortcut>
let shortcut: Partial<Shortcut> = {
key: keySplit.filter(k => !['meta', 'ctrl', 'shift', 'alt'].includes(k)).join('_'), if (key.includes('-') && key.includes('_')) {
metaKey: keySplit.includes('meta'), console.trace('[Shortcut] Invalid key')
ctrlKey: keySplit.includes('ctrl'), return null
shiftKey: keySplit.includes('shift'),
altKey: keySplit.includes('alt')
} }
const chained = key.includes('-')
if (chained) {
shortcut = {
key: key.toLowerCase(),
metaKey: false,
ctrlKey: false,
shiftKey: false,
altKey: false
}
} else {
const keySplit = key.toLowerCase().split('_').map(k => k)
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')
}
}
shortcut.chained = chained
// Convert Meta to Ctrl for non-MacOS // Convert Meta to Ctrl for non-MacOS
if (!macOS.value && shortcut.metaKey && !shortcut.ctrlKey) { if (!macOS.value && shortcut.metaKey && !shortcut.ctrlKey) {
shortcut.metaKey = false shortcut.metaKey = false

View File

@@ -8,8 +8,8 @@ export default defineNuxtPlugin(() => {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const root = computed(() => { const root = computed(() => {
const primary = colors[appConfig.ui.primary] const primary: Record<string, string> | undefined = colors[appConfig.ui.primary]
const gray = colors[appConfig.ui.gray] const gray: Record<string, string> | undefined = colors[appConfig.ui.gray]
if (!primary) { if (!primary) {
console.warn(`[@nuxthq/ui] Primary color '${appConfig.ui.primary}' not found in Tailwind config`) console.warn(`[@nuxthq/ui] Primary color '${appConfig.ui.primary}' not found in Tailwind config`)

View File

@@ -2,7 +2,7 @@ export function classNames (...classes: any[string]) {
return classes.filter(Boolean).join(' ') return classes.filter(Boolean).join(' ')
} }
export const hexToRgb = (hex) => { export const hexToRgb = (hex: string) => {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
hex = hex.replace(shorthandRegex, function (_, r, g, b) { hex = hex.replace(shorthandRegex, function (_, r, g, b) {