Compare commits

...

36 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
85 changed files with 2811 additions and 1156 deletions

View File

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

View File

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

View File

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

1
.gitignore vendored
View File

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

6
.prettierrc.json Normal file
View File

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

View File

@@ -16,6 +16,7 @@
"@release-it/conventional-changelog": {
"preset": "conventionalcommits",
"infile": "CHANGELOG.md",
"header": "# Changelog",
"ignoreRecommendedBump": true
}
}

View File

@@ -1,4 +1,33 @@
# Changelog
## [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)
@@ -11,9 +40,6 @@
* **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))
# 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.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/)
- HMR support through Nuxt App Config
- Dark mode support
- Support for LTR and RTL languages
- Keyboard shortcuts
- Bundled icons
- Fully typed

View File

@@ -2,25 +2,11 @@
<div>
<Header />
<UContainer>
<div class="relative grid lg:grid-cols-10 lg:gap-8">
<DocsAside class="lg:col-span-2" />
<UContainer class="grid lg:grid-cols-10 lg:gap-8">
<DocsAside class="lg:col-span-2" />
<div class="relative pt-8 pb-16" :class="[toc ? 'lg:col-span-6' : 'lg:col-span-8']">
<DocsPageHeader />
<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 class="lg:col-span-8 min-h-0 flex flex-col">
<NuxtPage />
</div>
</UContainer>
@@ -33,9 +19,12 @@
</template>
<script setup lang="ts">
const { toc } = useContent()
const colorMode = useColorMode()
const { data: navigation } = await useAsyncData('navigation', () => fetchContentNavigation())
provide('navigation', navigation)
// Computed
const color = computed(() => colorMode.value === 'dark' ? '#18181b' : 'white')
@@ -56,7 +45,7 @@ useHead({
lang: 'en'
},
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>
<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>
<div class="flex items-center justify-between h-16">
<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>
<HeaderLinks v-model="isDialogOpen" :links="links" />
</UContainer>
<TransitionRoot :show="isDialogOpen" as="template">
<Dialog as="div" @close="isDialogOpen = false">
<DialogPanel class="fixed inset-0 z-50 overflow-y-auto bg-white dark:bg-gray-900 lg:hidden">
<div class="px-4 sm:px-6 sticky top-0 border-b border-gray-900/10 dark:border-gray-50/[0.06] bg-white/75 dark:bg-gray-900/75 backdrop-blur z-10">
<div class="flex items-center justify-between h-16">
<div class="flex items-center gap-3">
<NuxtLink to="/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 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">
<HeaderLinks v-model="isDialogOpen" :links="links" />
</div>
<div class="px-4 sm:px-6 py-4 sm:py-6">
<ThemeSelect class="mb-4 sm:mb-6 w-full" />
<DocsAsideLinks @click="isDialogOpen = false" />
</div>
</DialogPanel>
@@ -92,25 +22,11 @@
<script setup lang="ts">
import { Dialog, DialogPanel, TransitionRoot } from '@headlessui/vue'
const { isSearchModalOpen } = useDocs()
const colorMode = useColorMode()
const isDialogOpen = ref(false)
const isDark = computed({
get () {
return colorMode.value === 'dark'
},
set () {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
function openDocsSearch () {
isDialogOpen.value = false
setTimeout(() => {
isSearchModalOpen.value = true
}, 100)
}
const links = [
{ label: 'Documentation', to: '/getting-started' },
{ label: 'Components', to: '/elements/avatar' },
{ label: 'Examples', to: '/examples' }
]
</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-model="componentProps[prop.name]"
:name="`prop-${prop.name}`"
variant="none"
tabindex="-1"
:ui="{ wrapper: 'relative flex items-start justify-center' }"
/>
<USelectMenu
@@ -18,6 +18,7 @@
variant="none"
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md', wrapper: 'relative inline-flex' }"
class="!py-0"
tabindex="-1"
:popper="{ strategy: 'fixed', placement: 'bottom-start' }"
/>
<UInput
@@ -28,6 +29,7 @@
variant="none"
autocomplete="off"
class="!py-0"
tabindex="-1"
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
/>
</div>
@@ -45,7 +47,7 @@
</component>
</div>
<ContentRenderer :value="ast" class="[&>div>pre]:!rounded-t-none" />
<ContentRenderer v-if="!previewOnly" :value="ast" class="[&>div>pre]:!rounded-t-none" />
</div>
</template>
@@ -98,6 +100,10 @@ const props = defineProps({
overflowClass: {
type: String,
default: ''
},
previewOnly: {
type: Boolean,
default: false
}
})

View File

@@ -1,5 +1,5 @@
<script setup>
const selected = ref(false)
const selected = ref(true)
</script>
<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>
<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 }">
<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>
</template>
<template #next="{ onClick }">
<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>
</template>
</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>
const commandPaletteRef = ref()
const { navigation } = useContent()
const navigation = inject('navigation')
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',
container: 'relative flex-1 overflow-y-auto divide-y divide-gray-200 dark:divide-gray-700 scroll-py-2',
input: {
base: 'w-full h-14 px-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 text-gray-900 dark:text-white focus:ring-0'
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: {
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>
const links = [{
label: 'Introduction',
to: '/getting-started'
}, {
label: 'Installation',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
to: '/navigation/vertical-navigation'
label: 'Theming',
to: '/getting-started/theming'
}, {
label: 'Command Palette',
to: '/navigation/command-palette'
label: 'Shortcuts',
to: '/getting-started/shortcuts'
}, {
label: 'Examples',
to: '/getting-started/examples'
}, {
label: 'Roadmap',
to: '/getting-started/roadmap'
}]
</script>
@@ -15,9 +24,9 @@ const links = [{
<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',
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: '',

View File

@@ -1,32 +1,15 @@
<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="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="bg-white dark:bg-gray-900 relative pointer-events-auto">
<UButton
icon="i-heroicons-magnifying-glass-20-solid"
class="w-full"
color="gray"
@click="isSearchModalOpen = true"
>
Search
<div class="hidden lg:flex items-center gap-0.5 ml-auto -my-1">
<UKbd>{{ metaSymbol }}</UKbd>
<UKbd>K</UKbd>
</div>
</UButton>
<DocsSearchButton class="w-full" />
</div>
<div class="h-8 bg-gradient-to-b from-white dark:from-gray-900" />
</div>
</div> -->
<DocsAsideLinks />
</div>
</aside>
</template>
<script setup lang="ts">
const { isSearchModalOpen } = useDocs()
const { metaSymbol } = useShortcuts()
</script>

View File

@@ -1,9 +1,9 @@
<template>
<div class="space-y-8">
<div v-for="(group, index) in navigation" :key="index" class="space-y-3">
<div class="text-sm font-semibold text-gray-900 dark:text-gray-200">
<span class="truncate">{{ group.title }}</span>
</div>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-200 truncate leading-6">
{{ group.title }}
</p>
<UVerticalNavigation
:links="mapContentLinks(group.children)"
@@ -32,7 +32,7 @@
<script setup lang="ts">
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[]) {
return links?.map(link => ({ label: link.title, icon: link.icon, to: link._path, badge: link.badge })) || []

View File

@@ -1,10 +1,14 @@
<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">
Made by
<NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs">
<LogoLabs class="text-primary-500 w-14 h-auto dark:text-primary-400" />
</NuxtLink>
</div>
<NuxtLink to="https://github.com/nuxtlabs/ui/releases" target="_blank">
<UBadge label="v2.4.0" />
</NuxtLink>
</footer>
</template>

View File

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

View File

@@ -26,12 +26,17 @@
/>
</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 }}
</p>
</header>
</template>
<script setup lang="ts">
const { page } = useContent()
defineProps({
page: {
type: Object,
default: null
}
})
</script>

View File

@@ -1,11 +1,17 @@
<template>
<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>
<DocsPrevNextCard
v-if="next"
:title="next.navigation?.title || next.title"
:description="next.navigation?.description || next.description"
:title="next.title"
:description="next.description"
:to="next._path"
icon="i-heroicons-arrow-right-20-solid"
class="text-right"
@@ -14,5 +20,14 @@
</template>
<script setup lang="ts">
const { prev, next } = useContent()
defineProps({
prev: {
type: Object,
default: null
},
next: {
type: Object,
default: null
}
})
</script>

View File

@@ -1,5 +1,5 @@
<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">
<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>

View File

@@ -12,6 +12,8 @@
ref="commandPaletteRef"
:groups="groups"
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="{
fuseOptions: { ignoreLocation: true, includeMatches: true, threshold: 0, keys: ['title', 'description', 'children.children.value', 'children.children.children.value'] },
resultLimit: 10
@@ -23,9 +25,11 @@
</template>
<script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
import type { Command } from '../../../src/runtime/types'
const { navigation } = useContent()
const navigation: Ref<NavItem[]> = inject('navigation')
const router = useRouter()
const { usingInput } = useShortcuts()
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>
<script setup lang="ts">
const { toc } = useContent()
defineProps({
toc: {
type: Object,
default: null
}
})
const isTocOpen = ref(false)
</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/)
- HMR support through Nuxt App Config
- Dark mode support
- Support for LTR and RTL languages
- Keyboard shortcuts
- Bundled icons
- 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"}
## 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
| Key | Default | Description |

View File

@@ -20,7 +20,7 @@ export default defineAppConfig({
```
::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`.
@@ -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`.
::
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.
@@ -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.
::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 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: {
icon: 'i-octicon-arrow-switch-24'
},
loadingState: {
icon: 'i-octicon-sync-24'
},
emptyState: {
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>
```
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`
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
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
---
props:
chipColor: 'primary'
chipText: ''
chipPosition: 'top-right'
size : 'sm'
extraColors:
- gray
baseProps:

View File

@@ -14,7 +14,7 @@ Use a `v-model` to make the Checkbox reactive.
#code
```vue
<script setup>
const selected = ref(false)
const selected = ref(true)
</script>
<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
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
---
baseProps:
name: 'checkbox2'
name: 'checkbox3'
props:
label: 'Label'
required: true
@@ -57,7 +71,7 @@ Use the `help` prop to display some text under the Checkbox.
::component-card
---
baseProps:
name: 'checkbox3'
name: 'checkbox4'
props:
label: 'Label'
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
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
---
baseProps:
name: 'radio2'
name: 'radio3'
props:
label: 'Label'
required: true
@@ -71,7 +85,7 @@ Use the `help` prop to display some text under the Radio.
::component-card
---
baseProps:
name: 'radio3'
name: 'radio4'
props:
label: 'Label'
help: 'Please choose one'
@@ -85,7 +99,7 @@ Use the `disabled` prop to disable the Radio.
::component-card
---
baseProps:
name: 'radio4'
name: 'radio5'
value: true
props:
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
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.
@@ -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.
@@ -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.

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
: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`.
::
## 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
### `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.

View File

@@ -2,7 +2,7 @@
github: true
description: Add a pagination to handle pages.
navigation:
badge: 'Edge'
badge: 'New'
---
## 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
### `prev` / `next`
@@ -162,16 +129,16 @@ const items = ref(Array(55));
</script>
<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 }">
<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>
</template>
<template #next="{ onClick }">
<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>
</template>
</UPagination>

View File

@@ -19,6 +19,19 @@ First of all, add the `Notifications` component to your app, preferably inside `
</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:
::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]
export default defineAppConfig({
ui: {
notifications: {
// Show toasts at the top right of the screen
position: 'top-0 right-0'
}
}
```vue
<script setup>
const toast = useToast()
onMounted(() => {
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
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:
id: 2
timeout: 0
props:
title: 'Notification'
props:
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.
::component-card
---
baseProps:
id: 5
id: 6
title: 'Notification'
description: 'This is a notification.'
timeout: 600000
@@ -194,7 +234,7 @@ You can pass all the props of the [Button](/elements/button) component to custom
::component-card
---
baseProps:
id: 6
id: 7
title: 'Notification'
timeout: 0
props:
@@ -235,7 +275,7 @@ Like for `closeButton`, you can pass all the props of the [Button](/elements/but
::component-card
---
baseProps:
id: 6
id: 8
title: 'Notification'
timeout: 0
props:
@@ -256,7 +296,7 @@ Actions will render differently whether you have a `description` set.
::component-card
---
baseProps:
id: 6
id: 9
title: 'Notification'
description: 'This is a notification.'
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'
],
content: {
documentDriven: true,
highlight: {
theme: {
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",
"version": "2.4.1",
"version": "2.5.0",
"repository": "https://github.com/nuxtlabs/ui",
"license": "MIT",
"exports": {
@@ -15,7 +15,7 @@
"dist"
],
"engines": {
"node": ">=16.14.0"
"node": ">=v16.14.0"
},
"scripts": {
"build": "nuxt-module-build",
@@ -29,42 +29,43 @@
},
"dependencies": {
"@egoist/tailwindcss-icons": "^1.1.0",
"@headlessui/vue": "1.7.10",
"@headlessui/vue": "^1.7.14",
"@iconify-json/heroicons": "^1.1.11",
"@nuxt/kit": "^3.5.3",
"@nuxtjs/color-mode": "^3.2.0",
"@nuxtjs/tailwindcss": "^6.7.2",
"@nuxt/kit": "^3.6.1",
"@nuxtjs/color-mode": "^3.3.0",
"@nuxtjs/tailwindcss": "^6.8.0",
"@popperjs/core": "^2.11.8",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"@vueuse/core": "^10.1.2",
"@vueuse/integrations": "^10.1.2",
"@vueuse/math": "^10.1.2",
"@vueuse/core": "^10.2.0",
"@vueuse/integrations": "^10.2.0",
"@vueuse/math": "^10.2.0",
"defu": "^6.1.2",
"fuse.js": "^6.6.2",
"lodash-es": "^4.17.21",
"tailwindcss": "^3.3.2"
},
"devDependencies": {
"@iconify-json/simple-icons": "^1.1.56",
"@nuxt/content": "^2.6.0",
"@nuxt/devtools": "^0.5.5",
"@iconify-json/simple-icons": "^1.1.58",
"@nuxt/content": "^2.7.0",
"@nuxt/devtools": "^0.6.4",
"@nuxt/eslint-config": "^0.1.1",
"@nuxt/module-builder": "^0.4.0",
"@nuxthq/studio": "^0.13.2",
"@nuxtjs/plausible": "^0.2.1",
"@release-it/conventional-changelog": "^5.1.1",
"@types/lodash-es": "^4.17.7",
"@types/node": "^20.3.1",
"@vueuse/nuxt": "^10.1.2",
"eslint": "^8.42.0",
"nuxt": "^3.5.3",
"@types/node": "^20.3.2",
"@vueuse/nuxt": "^10.2.0",
"eslint": "^8.43.0",
"nuxt": "^3.6.1",
"nuxt-component-meta": "^0.5.3",
"nuxt-lodash": "^2.4.1",
"nuxt-lodash": "^2.5.0",
"release-it": "^15.11.0",
"unbuild": "^1.2.1",
"vue-tsc": "1.6.3"
"v-calendar": "^3.0.3",
"vue-tsc": "^1.8.2"
}
}

1450
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 = {
avatar: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}],
badge: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-50`)
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`)
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`)
}],
button: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-50`),
@@ -103,16 +103,74 @@ const safelistByComponent = {
pattern: new RegExp(`ring-(${colorsAsRegex})-500`),
variants: ['focus']
}],
notification: (colorsAsRegex) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
}, {
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
radio: (colorsAsRegex) => [{
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']
}],
checkbox: (colorsAsRegex) => [{
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']
}],
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`)
}]
}
@@ -127,7 +185,7 @@ 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 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 [
...safelist,

View File

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

View File

@@ -3,7 +3,7 @@
<table :class="[ui.base, ui.divide]">
<thead :class="ui.thead">
<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 : []" />
</th>
@@ -12,7 +12,7 @@
<UButton
v-if="column.sortable"
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]"
@click="onSort(column)"
/>
@@ -23,12 +23,12 @@
</thead>
<tbody :class="ui.tbody">
<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" />
</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]">
<slot :name="`${column.key}-data`" :column="column" :row="row">
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index">
{{ row[column.key] }}
</slot>
</td>
@@ -77,7 +77,7 @@ import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
function defaultComparator<T>(a: T, z: T): boolean {
function defaultComparator<T> (a: T, z: T): boolean {
return a === z
}
@@ -188,7 +188,13 @@ export default defineComponent({
function onSort (column) {
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 {
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()">
<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 />
</span>
</template>
@@ -55,6 +57,10 @@ export default defineComponent({
return Object.keys(appConfig.ui.avatar.chip.position).includes(value)
}
},
chipText: {
type: [String, Number],
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.avatar>>,
default: () => appConfig.ui.avatar

View File

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

View File

@@ -1,6 +1,6 @@
<template>
<Menu v-slot="{ open }" as="div" :class="ui.wrapper" @mouseleave="onMouseLeave">
<MenuButton
<HMenu v-slot="{ open }" as="div" :class="ui.wrapper" @mouseleave="onMouseLeave">
<HMenuButton
ref="trigger"
as="div"
:disabled="disabled"
@@ -13,13 +13,13 @@
Open
</button>
</slot>
</MenuButton>
</HMenuButton>
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseover="onMouseOver">
<transition appear v-bind="ui.transition">
<MenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.height]" static>
<Transition appear v-bind="ui.transition">
<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">
<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
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]"
@@ -36,19 +36,19 @@
</span>
</slot>
</ULinkCustom>
</MenuItem>
</HMenuItem>
</div>
</MenuItems>
</transition>
</HMenuItems>
</Transition>
</div>
</Menu>
</HMenu>
</template>
<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 { 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 { omit } from 'lodash-es'
import UIcon from '../elements/Icon.vue'
@@ -67,11 +67,10 @@ import appConfig from '#build/app.config'
export default defineComponent({
components: {
// eslint-disable-next-line vue/no-reserved-component-names
Menu,
MenuButton,
MenuItems,
MenuItem,
HMenu,
HMenuButton,
HMenuItems,
HMenuItem,
UIcon,
UAvatar,
UKbd,

View File

@@ -3,7 +3,7 @@
<div class="flex items-center h-5">
<input
:id="name"
v-model="isChecked"
v-model="toggle"
:name="name"
:required="required"
:value="value"
@@ -12,13 +12,11 @@
:indeterminate="indeterminate"
type="checkbox"
class="form-checkbox"
:class="[ui.base, ui.rounded]"
:class="inputClass"
v-bind="$attrs"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
>
</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">
<slot name="label">{{ label }}</slot>
<span v-if="required" :class="ui.required">*</span>
@@ -34,6 +32,7 @@
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
@@ -80,19 +79,26 @@ export default defineComponent({
type: Boolean,
default: false
},
color: {
type: String,
default: () => appConfig.ui.checkbox.default.color,
validator (value: string) {
return appConfig.ui.colors.includes(value)
}
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.checkbox>>,
default: () => appConfig.ui.checkbox
}
},
emits: ['update:modelValue', 'focus', 'blur'],
emits: ['update:modelValue'],
setup (props, { emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.checkbox>>(() => defu({}, props.ui, appConfig.ui.checkbox))
const isChecked = computed({
const toggle = computed({
get () {
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 {
// eslint-disable-next-line vue/no-dupe-keys
ui,
isChecked
toggle,
inputClass
}
}
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,28 @@
<template>
<Switch
<HSwitch
v-model="active"
:name="name"
: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 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 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>
</Switch>
</HSwitch>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { Switch } from '@headlessui/vue'
import { Switch as HSwitch } from '@headlessui/vue'
import UIcon from '../elements/Icon.vue'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -31,8 +32,7 @@ import appConfig from '#build/app.config'
export default defineComponent({
components: {
// eslint-disable-next-line vue/no-reserved-component-names
Switch,
HSwitch,
UIcon
},
props: {
@@ -56,6 +56,13 @@ export default defineComponent({
type: String,
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: {
type: Object as PropType<Partial<typeof 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 {
// eslint-disable-next-line vue/no-dupe-keys
ui,
active
active,
switchClass,
onIconClass,
offIconClass
}
}
})

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
<template>
<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]">
<slot />
</div>
</transition>
</Transition>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<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">
<div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild>
@@ -8,13 +8,13 @@
<div :class="ui.inner">
<div :class="[ui.container, ui.padding]">
<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 />
</DialogPanel>
</HDialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</HDialog>
</TransitionRoot>
</template>
@@ -22,7 +22,7 @@
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
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'
// TODO: Remove
// @ts-expect-error
@@ -32,9 +32,8 @@ import appConfig from '#build/app.config'
export default defineComponent({
components: {
// eslint-disable-next-line vue/no-reserved-component-names
Dialog,
DialogPanel,
HDialog,
HDialogPanel,
TransitionRoot,
TransitionChild
},

View File

@@ -1,5 +1,5 @@
<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.container, ui.rounded, ui.ring]">
<div :class="ui.padding">
@@ -31,7 +31,7 @@
<div v-if="timeout" :class="progressClass" :style="progressStyle" />
</div>
</div>
</transition>
</Transition>
</template>
<script lang="ts">

View File

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

View File

@@ -1,16 +1,16 @@
<template>
<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">
<div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild>
<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 />
</DialogPanel>
</HDialogPanel>
</TransitionChild>
</Dialog>
</HDialog>
</TransitionRoot>
</template>
@@ -18,7 +18,7 @@
import { computed, defineComponent } from 'vue'
import type { WritableComputedRef, PropType } from 'vue'
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'
// TODO: Remove
// @ts-expect-error
@@ -28,9 +28,8 @@ import appConfig from '#build/app.config'
export default defineComponent({
components: {
// eslint-disable-next-line vue/no-reserved-component-names
Dialog,
DialogPanel,
HDialog,
HDialogPanel,
TransitionRoot,
TransitionChild
},

View File

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

View File

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

View File

@@ -8,8 +8,8 @@ export default defineNuxtPlugin(() => {
const nuxtApp = useNuxtApp()
const root = computed(() => {
const primary = colors[appConfig.ui.primary]
const gray = colors[appConfig.ui.gray]
const primary: Record<string, string> | undefined = colors[appConfig.ui.primary]
const gray: Record<string, string> | undefined = colors[appConfig.ui.gray]
if (!primary) {
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(' ')
}
export const hexToRgb = (hex) => {
export const hexToRgb = (hex: string) => {
// 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) {