Compare commits

..

46 Commits

Author SHA1 Message Date
Benjamin Canac
0af5184c70 chore(release): 2.2.1 2023-05-27 12:27:53 +02:00
Benjamin Canac
44c3e2c46a chore(forms): remove required on Input, Select and Textarea name
Resolves #236
2023-05-27 12:03:29 +02:00
Benjamin Canac
a96dc19215 fix(FormGroup): missing h import from vue
Resolves #236
2023-05-27 12:02:51 +02:00
Benjamin Canac
aa881a8d00 chore(release): 2.2.0 2023-05-26 23:19:53 +02:00
Benjamin Canac
08413f198b scripts: update to pnpm 2023-05-26 22:46:17 +02:00
Benjamin Canac
75ab1d2ed5 chore(deps): bump 2023-05-26 22:25:58 +02:00
Sumit Kolhe
2d6ce654f4 docs: add close button to Slideover example (#211)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-05-26 22:20:34 +02:00
Benjamin Canac
9ce531a06f feat!: handle color states on form elements (#234) 2023-05-26 22:07:49 +02:00
Benjamin Canac
1a9dc5c980 fix(Notification): remove default color on icon 2023-05-26 18:28:52 +02:00
Benjamin Canac
589f86ef1b chore(Avatar): dark variant for chip background color 2023-05-26 18:28:32 +02:00
Benjamin Canac
1b61ec72e2 chore(Notification)!: rename progressColor to color and style icon
This also removes `progressVariant` prop
2023-05-26 18:03:54 +02:00
Benjamin Canac
1f22f84360 chore(Avatar)!: remove chipVariant prop 2023-05-26 18:02:48 +02:00
Benjamin Canac
2c6db975f9 chore(deps): switch to pnpm (#228) 2023-05-26 17:41:07 +02:00
Benjamin Canac
b7099aa0d3 chore(SelectMenu): add searchablePlaceholder prop
Resolves #231
2023-05-26 15:02:21 +02:00
Benjamin Canac
36b0869bc2 docs: fix prev card gap on first page 2023-05-23 16:57:42 +02:00
Benjamin Canac
28167e41ff docs: add VerticalNavigation tailwind example 2023-05-23 15:27:13 +02:00
Benjamin Canac
19923cbf1e chore(VerticalNavigation)!: split preset 2023-05-23 15:26:47 +02:00
Benjamin Canac
1210e99ec1 chore(VerticalNavigation): improve types import 2023-05-23 15:25:28 +02:00
Benjamin Canac
fc894bc1ae chore(Dropdown): improve types import 2023-05-23 15:25:12 +02:00
Benjamin Canac
9491ac7172 chore(CommandPalette): improve types import 2023-05-23 15:25:00 +02:00
Benjamin Canac
32dc2264d8 chore(types): export button 2023-05-23 15:24:41 +02:00
Benjamin Canac
45ba3b26da chore(Notification): improve types 2023-05-23 15:24:32 +02:00
Benjamin Canac
6d3309c42d chore(Notification): move padding to app.config 2023-05-23 11:25:56 +02:00
Benjamin Canac
530b85136d docs: handle color mode in volta embed 2023-05-23 11:11:19 +02:00
Benjamin Canac
cb9ed9ad3f chore(Button): inject NuxtLink in components 2023-05-22 19:05:39 +02:00
Benjamin Canac
524e220914 chore(VerticalNavigation): improve binds & types 2023-05-22 19:05:17 +02:00
Benjamin Canac
e3e6ef27a2 docs: improve Dropdown example with click and disabled 2023-05-22 19:04:39 +02:00
Benjamin Canac
55f115f9fe chore(Dropdown): use ULinkCustom + improve item binds & types
Fixes #215
2023-05-22 19:04:18 +02:00
Benjamin Canac
bdaf2dbbd4 chore(CommandPalette): handle loading state (#221) 2023-05-22 16:00:31 +02:00
Benjamin Canac
e7eea067b2 chore(Notification): add progressColor and progressVariant props (#219)
Co-authored-by: Sébastien Chopin <seb@nuxtjs.com>
2023-05-22 15:01:19 +02:00
Benjamin Canac
a56dbeab35 fix(Radio/Checkbox): remove ring offset on focus 2023-05-22 13:41:56 +02:00
Benjamin Canac
570b82d1e7 chore(Avatar): allow default value for chipColor through app.config.ts 2023-05-22 12:24:17 +02:00
Harry Yep
b5189c0c07 docs: LogoLabs not shown (#216) 2023-05-21 23:01:08 +02:00
Sébastien Chopin
8a0a5d8ba0 docs: pre-render component-meta routes 2023-05-20 19:13:58 +02:00
Sébastien Chopin
d3e5f4e15d docs: remove console.log 2023-05-20 18:53:32 +02:00
Sébastien Chopin
5a592b7ee0 docs: use CF rules for redirect 2023-05-20 18:49:39 +02:00
Sébastien Chopin
43787eca74 docs: move vercel.json to public dir 2023-05-20 18:41:07 +02:00
Sébastien Chopin
595ed9fb46 docs: add vercel redirect 2023-05-20 18:38:19 +02:00
Sébastien Chopin
5c4ab26d25 docs: support ssg 2023-05-20 18:31:56 +02:00
Sébastien Chopin
2030f24a47 docs: update logo on aside on mobile 2023-05-20 13:18:19 +02:00
Benjamin Canac
6eda322496 chore(VerticalNavigation): links badge type as number
Resolves #206
2023-05-19 15:55:18 +02:00
Benjamin Canac
318f8b2f08 docs: improve theming colors section 2023-05-19 15:00:39 +02:00
Benjamin Canac
dfab900562 docs: add badge in VerticalNavigation example 2023-05-19 14:51:31 +02:00
Benjamin Canac
d2ee5058f8 fix(VerticalNavigation): badge display
Resolves #205
2023-05-19 14:51:16 +02:00
Benjamin Canac
e358183165 docs: getting started title on index 2023-05-19 13:01:34 +02:00
Benjamin Canac
26579538f5 docs: prevent Alert text hover without link 2023-05-19 13:00:50 +02:00
67 changed files with 11287 additions and 10222 deletions

View File

@@ -22,30 +22,41 @@ jobs:
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Checkout - name: checkout
uses: actions/checkout@master uses: actions/checkout@master
with:
persist-credentials: false
fetch-depth: 0
- name: Cache - uses: pnpm/action-setup@v2
uses: actions/cache@v3 name: Install pnpm
id: pnpm-install
with: with:
path: node_modules version: 7
key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }} run_install: false
- name: Dependencies - name: Get pnpm store directory
if: steps.cache.outputs.cache-hit != 'true' id: pnpm-cache
run: yarn shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Lint - name: Lint
run: yarn lint run: pnpm run lint
- name: Typecheck - name: Typecheck
run: yarn typecheck run: pnpm run typecheck
- name: Build - name: Build
run: yarn build run: pnpm run build
- name: Release Edge - name: Release Edge
if: github.event_name == 'push' if: github.event_name == 'push'

View File

@@ -22,30 +22,41 @@ jobs:
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Checkout - name: checkout
uses: actions/checkout@master uses: actions/checkout@master
with:
persist-credentials: false
fetch-depth: 0
- name: Cache - uses: pnpm/action-setup@v2
uses: actions/cache@v3 name: Install pnpm
id: pnpm-install
with: with:
path: node_modules version: 7
key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }} run_install: false
- name: Dependencies - name: Get pnpm store directory
if: steps.cache.outputs.cache-hit != 'true' id: pnpm-cache
run: yarn shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Lint - name: Lint
run: yarn lint run: pnpm run lint
- name: Typecheck - name: Typecheck
run: yarn typecheck run: pnpm run typecheck
- name: Build - name: Build
run: yarn build run: pnpm run build
- name: Version Check - name: Version Check
id: check id: check

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ nuxt.d.ts
dist dist
.DS_Store .DS_Store
.history .history
.vercel

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
shamefully-hoist=true

View File

@@ -2,6 +2,39 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [2.2.1](https://github.com/nuxtlabs/ui/compare/v2.2.0...v2.2.1) (2023-05-27)
### Bug Fixes
* **FormGroup:** missing `h` import from `vue` ([a96dc19](https://github.com/nuxtlabs/ui/commit/a96dc192157725143503b1a5e4b404cb48dc9d3f)), closes [#236](https://github.com/nuxtlabs/ui/issues/236)
## [2.2.0](https://github.com/nuxtlabs/ui/compare/v2.1.0...v2.2.0) (2023-05-26)
### ⚠ BREAKING CHANGES
* handle color states on form elements (#234)
* **Notification:** rename `progressColor` to `color` and style icon
* **Avatar:** remove `chipVariant` prop
* **VerticalNavigation:** split preset
### Features
* handle color states on form elements ([#234](https://github.com/nuxtlabs/ui/issues/234)) ([9ce531a](https://github.com/nuxtlabs/ui/commit/9ce531a06f1a972bc003876162e0503c1bbbdbd8))
### Bug Fixes
* **Notification:** remove default color on icon ([1a9dc5c](https://github.com/nuxtlabs/ui/commit/1a9dc5c980d8477cdf9386a17e20fc9fec0d883e))
* **Radio/Checkbox:** remove ring offset on focus ([a56dbea](https://github.com/nuxtlabs/ui/commit/a56dbeab351a5c58e5bb49f5762669e2884c6483))
* **VerticalNavigation:** badge display ([d2ee505](https://github.com/nuxtlabs/ui/commit/d2ee5058f819fc17f281f323dab2f0b3d80cf7bd)), closes [#205](https://github.com/nuxtlabs/ui/issues/205)
* **Avatar:** remove `chipVariant` prop ([1f22f84](https://github.com/nuxtlabs/ui/commit/1f22f84360c20498eea8971b21db9293a4c9c3dc))
* **Notification:** rename `progressColor` to `color` and style icon ([1b61ec7](https://github.com/nuxtlabs/ui/commit/1b61ec72e292325d7776a4719f14a75bdb18e110))
* **VerticalNavigation:** split preset ([19923cb](https://github.com/nuxtlabs/ui/commit/19923cbf1edc6c6d4aefb9ffab9f908b116e1c69))
## [2.1.0](https://github.com/nuxtlabs/ui/compare/v2.0.4...v2.1.0) (2023-05-19) ## [2.1.0](https://github.com/nuxtlabs/ui/compare/v2.0.4...v2.1.0) (2023-05-19)

View File

@@ -3,7 +3,7 @@
<UContainer> <UContainer>
<div class="flex items-center justify-between h-16"> <div class="flex items-center justify-between h-16">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<NuxtLink to="/" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white"> <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" /> <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> NuxtLabs<span class="text-primary-500 dark:text-primary-400">UI</span>
@@ -62,10 +62,9 @@
<div class="px-4 sm:px-6 sticky top-0 border-b border-gray-900/10 dark:border-gray-50/[0.06] bg-white/75 dark:bg-gray-900/75 backdrop-blur z-10"> <div class="px-4 sm:px-6 sticky top-0 border-b border-gray-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 justify-between h-16">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-gray-900 dark:text-white"> <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" /> <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>
nuxthq/ui
</NuxtLink> </NuxtLink>
</div> </div>

View File

@@ -1,48 +1,52 @@
<template> <template>
<div class="flex items-center shadow-sm"> <div class="flex items-center shadow-sm">
<USelectMenu <ClientOnly>
v-model="primary" <USelectMenu
name="primary" v-model="primary"
class="w-full [&>div>button]:!rounded-r-none" name="primary"
appearance="gray" class="w-full [&>div>button]:!rounded-r-none"
:ui="{ width: 'w-[194px]' }" color="gray"
:popper="{ placement: 'bottom-start' }" :ui="{ width: 'w-[194px]' }"
:options="primaryOptions" :popper="{ placement: 'bottom-start' }"
> :options="primaryOptions"
<template #label> >
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${primary.hex}`}" /> <template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${primary.hex}`}" />
{{ primary.text }} {{ primary.text }}
</template> </template>
<template #option="{ option }"> <template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" /> <span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
{{ option.text }} {{ option.text }}
</template> </template>
</USelectMenu> </USelectMenu>
</ClientOnly>
<USelectMenu <ClientOnly>
v-model="gray" <USelectMenu
name="gray" v-model="gray"
class="w-full [&>div>button]:!rounded-l-none [&>div>button]:-ml-px" name="gray"
appearance="gray" class="w-full [&>div>button]:!rounded-l-none [&>div>button]:-ml-px"
:ui="{ width: 'w-[194px]' }" color="gray"
:popper="{ placement: 'bottom-end' }" :ui="{ width: 'w-[194px]' }"
:options="grayOptions" :popper="{ placement: 'bottom-end' }"
> :options="grayOptions"
<template #label> >
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${gray.hex}`}" /> <template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${gray.hex}`}" />
{{ gray.text }} {{ gray.text }}
</template> </template>
<template #option="{ option }"> <template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" /> <span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
{{ option.text }} {{ option.text }}
</template> </template>
</USelectMenu> </USelectMenu>
</ClientOnly>
</div> </div>
</template> </template>
@@ -84,4 +88,43 @@ const gray = computed({
grayCookie.value = option.value 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> </script>

View File

@@ -2,8 +2,8 @@
<component <component
:is="to ? NuxtLink : 'div'" :is="to ? NuxtLink : 'div'"
:to="to" :to="to"
class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-200 text-sm leading-6 my-5 last:mb-0 font-normal group relative" class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm leading-6 my-5 last:mb-0 font-normal group relative"
:class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed' : '']" :class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed hover:text-gray-800 dark:hover:text-gray-200' : '']"
> >
<UIcon v-if="!!to" name="i-heroicons-link-20-solid" class="w-3 h-3 absolute right-2 top-2 text-gray-400 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400" /> <UIcon v-if="!!to" name="i-heroicons-link-20-solid" class="w-3 h-3 absolute right-2 top-2 text-gray-400 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400" />

View File

@@ -7,7 +7,7 @@
v-if="prop.type === 'boolean'" v-if="prop.type === 'boolean'"
v-model="componentProps[prop.name]" v-model="componentProps[prop.name]"
:name="prop.name" :name="prop.name"
appearance="none" variant="none"
class="justify-center" class="justify-center"
/> />
<USelectMenu <USelectMenu
@@ -16,7 +16,7 @@
:options="prop.options" :options="prop.options"
:name="prop.name" :name="prop.name"
:label="componentProps[prop.name]" :label="componentProps[prop.name]"
appearance="none" variant="none"
class="inline-flex" class="inline-flex"
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md' }" :ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md' }"
:ui-select="{ custom: '!py-0' }" :ui-select="{ custom: '!py-0' }"
@@ -27,7 +27,7 @@
:model-value="componentProps[prop.name]" :model-value="componentProps[prop.name]"
:type="prop.type === 'number' ? 'number' : 'text'" :type="prop.type === 'number' ? 'number' : 'text'"
:name="prop.name" :name="prop.name"
appearance="none" variant="none"
autocomplete="off" autocomplete="off"
:ui="{ custom: '!py-0' }" :ui="{ custom: '!py-0' }"
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val" @update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
@@ -49,6 +49,7 @@
// @ts-expect-error // @ts-expect-error
import { transformContent } from '@nuxt/content/transformers' import { transformContent } from '@nuxt/content/transformers'
// eslint-disable-next-line vue/no-dupe-keys
const props = defineProps({ const props = defineProps({
slug: { slug: {
type: String, type: String,
@@ -78,17 +79,23 @@ const props = defineProps({
type: Array, type: Array,
default: () => [] default: () => []
}, },
extraColors: {
type: Array,
default: () => []
},
backgroundClass: { backgroundClass: {
type: String, type: String,
default: 'bg-white dark:bg-gray-900' default: 'bg-white dark:bg-gray-900'
} }
}) })
// eslint-disable-next-line vue/no-dupe-keys
const baseProps = reactive({ ...props.baseProps }) const baseProps = reactive({ ...props.baseProps })
const componentProps = reactive({ ...props.props }) const componentProps = reactive({ ...props.props })
const appConfig = useAppConfig() const appConfig = useAppConfig()
const route = useRoute() const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1] const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug) const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}` const name = `U${useUpperFirst(camelName)}`
@@ -97,6 +104,7 @@ const meta = await fetchComponentMeta(name)
// Computed // Computed
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => ({ ...appConfig.ui[camelName], ...props.ui })) const ui = computed(() => ({ ...appConfig.ui[camelName], ...props.ui }))
const fullProps = computed(() => ({ ...props.baseProps, ...componentProps })) const fullProps = computed(() => ({ ...props.baseProps, ...componentProps }))
@@ -117,7 +125,8 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
const keys = useGet(ui.value, dottedKey, {}) const keys = useGet(ui.value, dottedKey, {})
let options = typeof keys === 'object' && Object.keys(keys) let options = typeof keys === 'object' && Object.keys(keys)
if (key.toLowerCase().endsWith('color')) { if (key.toLowerCase().endsWith('color')) {
options = appConfig.ui.colors // @ts-ignore
options = [...appConfig.ui.colors, ...props.extraColors]
} }
return { return {
@@ -128,6 +137,7 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
} }
}).filter(Boolean)) }).filter(Boolean))
// eslint-disable-next-line vue/no-dupe-keys
const code = computed(() => { const code = computed(() => {
let code = `\`\`\`html let code = `\`\`\`html
<${name}` <${name}`

View File

@@ -15,6 +15,7 @@ const props = defineProps({
const appConfig = useAppConfig() const appConfig = useAppConfig()
const route = useRoute() const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1] const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug) const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}` const name = `U${useUpperFirst(camelName)}`

View File

@@ -43,6 +43,7 @@ const props = defineProps({
}) })
const route = useRoute() const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1] const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug) const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}` const name = `U${useUpperFirst(camelName)}`

View File

@@ -26,6 +26,7 @@ const props = defineProps({
}) })
const route = useRoute() const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1] const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug) const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}` const name = `U${useUpperFirst(camelName)}`

View File

@@ -11,6 +11,7 @@ const props = defineProps({
}) })
const appConfig = useAppConfig() const appConfig = useAppConfig()
const colorMode = useColorMode()
const src = computed(() => `https://volta.net/embed/${props.token}?gray=${appConfig.ui.gray}&primary=${appConfig.ui.primary}`) const src = computed(() => `https://volta.net/embed/${props.token}?theme=${colorMode.value}&gray=${appConfig.ui.gray}&primary=${appConfig.ui.primary}`)
</script> </script>

View File

@@ -8,11 +8,15 @@ const items = [
}], [{ }], [{
label: 'Edit', label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid', icon: 'i-heroicons-pencil-square-20-solid',
shortcuts: ['E'] shortcuts: ['E'],
click: () => {
console.log('Edit')
}
}, { }, {
label: 'Duplicate', label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid', icon: 'i-heroicons-document-duplicate-20-solid',
shortcuts: ['D'] shortcuts: ['D'],
disabled: true
}], [{ }], [{
label: 'Archive', label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid' icon: 'i-heroicons-archive-box-20-solid'

View File

@@ -7,8 +7,23 @@ const isOpen = ref(false)
<UButton label="Open" @click="isOpen = true" /> <UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen"> <USlideover v-model="isOpen">
<div class="p-4 h-full"> <div class="p-4 sm:p-6 flex flex-col flex-1 gap-4 sm:gap-6">
<Placeholder class="w-full h-full" /> <div class="flex items-center justify-between">
<h2 class="font-semibold text-gray-900 dark:text-white">
Title
</h2>
<UButton
icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="link"
size="md"
:padded="false"
@click="isOpen = false"
/>
</div>
<Placeholder class="flex-1 w-full" />
</div> </div>
</USlideover> </USlideover>
</div> </div>

View File

@@ -3,7 +3,8 @@ const links = [{
label: 'Profile', label: 'Profile',
avatar: { avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4' src: 'https://avatars.githubusercontent.com/u/739984?v=4'
} },
badge: 100
}, { }, {
label: 'Installation', label: 'Installation',
icon: 'i-heroicons-home', icon: 'i-heroicons-home',

View File

@@ -0,0 +1,28 @@
<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>

View File

@@ -10,8 +10,11 @@
class="mt-1" class="mt-1"
:ui="{ :ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2', 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', padding: 'pl-4',
base: 'group text-sm block border-l -ml-px lg:leading-6', rounded: '',
font: '',
ring: '',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold', 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' 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'
}" }"

View File

@@ -3,7 +3,7 @@
<div class="flex items-baseline gap-1.5 text-sm text-center text-gray-500 dark:text-gray-400"> <div class="flex items-baseline gap-1.5 text-sm text-center text-gray-500 dark:text-gray-400">
Made by Made by
<NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs"> <NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs">
<LogoLabs class="text-white w-14 h-auto" /> <LogoLabs class="text-primary-500 w-14 h-auto dark:text-primary-400" />
</NuxtLink> </NuxtLink>
</div> </div>
</footer> </footer>

View File

@@ -1,7 +1,7 @@
<template> <template>
<header v-if="page" class="relative border-b border-gray-200 dark:border-gray-800 pb-8 mb-12"> <header v-if="page" class="relative border-b border-gray-200 dark:border-gray-800 pb-8 mb-12">
<p class="mb-4 text-sm leading-6 font-semibold text-primary-500 dark:text-primary-400 capitalize"> <p class="mb-4 text-sm leading-6 font-semibold text-primary-500 dark:text-primary-400 capitalize">
{{ useLowerCase(page._dir) }} {{ page._dir?.title ? page._dir.title : useLowerCase(page._dir) }}
</p> </p>
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between"> <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 tracking-tight dark:text-white"> <h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 tracking-tight dark:text-white">

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="grid gap-6 sm:grid-cols-2"> <div class="grid gap-6 sm:grid-cols-2">
<DocsPrevNextCard v-if="prev" :title="prev.navigation?.title || prev.title" :description="prev.navigation?.description || prev.description" :to="prev._path" icon="i-heroicons-arrow-left-20-solid" /> <DocsPrevNextCard v-if="prev" :title="prev.navigation?.title || prev.title" :description="prev.navigation?.description || prev.description" :to="prev._path" icon="i-heroicons-arrow-left-20-solid" />
<span v-else>&nbsp;</span> <span v-else class="hidden sm:block">&nbsp;</span>
<DocsPrevNextCard <DocsPrevNextCard
v-if="next" v-if="next"
:title="next.navigation?.title || next.title" :title="next.navigation?.title || next.title"

View File

@@ -10,7 +10,16 @@ export async function fetchComponentMeta (name: string) {
if (state.value[name]) { return state.value[name] } if (state.value[name]) { return state.value[name] }
// Store promise to avoid multiple calls // Store promise to avoid multiple calls
state.value[name] = $fetch(`/api/component-meta/${name}`).then((meta) => {
// add to nitro prerender
if (process.server) {
const event = useRequestEvent()
event.node.res.setHeader(
'x-nitro-prerender',
[event.node.res.getHeader('x-nitro-prerender'), `/api/component-meta/${name}.json`].filter(Boolean).join(',')
)
}
state.value[name] = $fetch(`/api/component-meta/${name}.json`).then((meta) => {
state.value[name] = meta state.value[name] = meta
}) })

View File

@@ -19,15 +19,21 @@ 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.
::
As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors. By default, the `primary` color is `green` and the `gray` color is `cool`. As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors. By default, the `primary` color is `green` and the `gray` color is `cool`.
To provide dynamic colors that can be changed at runtime, this module uses CSS variables. As Tailwind CSS already has a `gray` color, the module automatically renames it to `cool` to avoid conflicts (`coolGray` was renamed to `gray` when Tailwind CSS v3.0 was released). To provide dynamic colors that can be changed at runtime, this module uses CSS variables. As Tailwind CSS already has a `gray` color, the module automatically renames it to `cool` to avoid conflicts (`coolGray` was renamed to `gray` when Tailwind CSS v3.0 was released).
Likewise, you can't define a `primary` color in your `tailwind.config.ts` as it would conflict with the `primary` color defined by the module.
::alert{icon="i-heroicons-light-bulb"} ::alert{icon="i-heroicons-light-bulb"}
Try to change the `primary` and `gray` colors in the navbar and see the colors change live. 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 that have a `color` prop like [Avatar](/elements/avatar), [Badge](/elements/badge) and [Button](/elements/button) 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 that have a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style) 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.
## Dark mode ## Dark mode
@@ -178,6 +184,7 @@ export default defineAppConfig({
commandPalette: { commandPalette: {
default: { default: {
icon: 'i-octicon-search-24', icon: 'i-octicon-search-24',
loadingIcon: 'i-octicon-sync-24',
selectedIcon: 'i-octicon-check-24', selectedIcon: 'i-octicon-check-24',
empty: { empty: {
icon: 'i-octicon-search-24' icon: 'i-octicon-search-24'

View File

@@ -0,0 +1 @@
title: Getting Started

View File

@@ -29,14 +29,15 @@ baseProps:
### Chip ### Chip
Use the `chipColor`, `chipVariant` and `chipPosition` props to display a chip on the Avatar. Use the `chip-color` and `chip-position` props to display a chip on the Avatar.
::component-card ::component-card
--- ---
props: props:
chipColor: 'primary' chipColor: 'primary'
chipVariant: 'solid'
chipPosition: 'top-right' chipPosition: 'top-right'
extraColors:
- gray
baseProps: baseProps:
src: 'https://avatars.githubusercontent.com/u/739984?v=4' src: 'https://avatars.githubusercontent.com/u/739984?v=4'
alt: 'Avatar' alt: 'Avatar'

View File

@@ -113,7 +113,7 @@ Button
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`. Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `leading` and `trailing` props to set the icon position or the `leadingIcon` and `trailingIcon` props to set a different icon for each position. Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.
::component-card ::component-card
--- ---
@@ -163,7 +163,7 @@ Button
Use the `loading` prop to show a loading icon and disable the Button. Use the `loading` prop to show a loading icon and disable the Button.
Use the `loadingIcon` prop to set a different icon or change it globally in `ui.button.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`. Use the `loading-icon` prop to set a different icon or change it globally in `ui.button.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
::component-card ::component-card
--- ---

View File

@@ -24,11 +24,15 @@ const items = [
}], [{ }], [{
label: 'Edit', label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid', icon: 'i-heroicons-pencil-square-20-solid',
shortcuts: ['E'] shortcuts: ['E'],
click: () => {
console.log('Edit')
}
}, { }, {
label: 'Duplicate', label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid', icon: 'i-heroicons-document-duplicate-20-solid',
shortcuts: ['D'] shortcuts: ['D'],
disabled: true
}], [{ }], [{
label: 'Archive', label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid' icon: 'i-heroicons-archive-box-20-solid'

View File

@@ -12,6 +12,53 @@ baseProps:
--- ---
:: ::
### Style
Use the `color` and `variant` props to change the visual style of the Input.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size ### Size
Use the `size` prop to change the size of the Input. Use the `size` prop to change the size of the Input.
@@ -38,25 +85,11 @@ props:
--- ---
:: ::
### Appearance
Use the `appearance` prop to change the style of the Input.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
appearance: 'white'
---
::
### Icon ### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`. Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `leading` and `trailing` props to set the icon position or the `leadingIcon` and `trailingIcon` props to set a different icon for each position. Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.
::component-card ::component-card
--- ---
@@ -65,9 +98,12 @@ baseProps:
placeholder: 'Search...' placeholder: 'Search...'
props: props:
icon: 'i-heroicons-magnifying-glass-20-solid' icon: 'i-heroicons-magnifying-glass-20-solid'
appearance: 'white'
size: 'sm' size: 'sm'
color: 'white'
trailing: false trailing: false
extraColors:
- white
- gray
excludedProps: excludedProps:
- icon - icon
--- ---
@@ -81,12 +117,9 @@ Use the `disabled` prop to disable the Input.
--- ---
baseProps: baseProps:
name: 'input' name: 'input'
props:
placeholder: 'Search...' placeholder: 'Search...'
appearance: 'white' props:
disabled: true disabled: true
excludedProps:
- placeholder
--- ---
:: ::
@@ -94,7 +127,7 @@ excludedProps:
Use the `loading` prop to show a loading icon and disable the Input. Use the `loading` prop to show a loading icon and disable the Input.
Use the `loadingIcon` prop to set a different icon or change it globally in `ui.input.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`. Use the `loading-icon` prop to set a different icon or change it globally in `ui.input.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
::component-card ::component-card
--- ---
@@ -109,32 +142,6 @@ excludedProps:
--- ---
:: ::
### Group
You can use the `InputGroup` component to add a label and additional informations to a form element.
::component-card{slug="InputGroup"}
---
baseProps:
name: 'group'
props:
label: 'Email'
help: "We'll only use this for spam."
hint: 'Required'
required: true
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{name="group" placeholder="you@example.com" icon="i-heroicons-envelope"}
::
::alert{icon="i-heroicons-light-bulb"}
This also works with `Textarea`, `Select` and `SelectMenu` components.
::
## Props ## Props
:component-props :component-props

View File

@@ -12,6 +12,53 @@ baseProps:
--- ---
:: ::
### Style
Use the `color` and `variant` props to change the visual style of the Textarea.
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size ### Size
Use the `size` prop to change the size of the Textarea. Use the `size` prop to change the size of the Textarea.
@@ -38,20 +85,6 @@ props:
--- ---
:: ::
### Appearance
Use the `appearance` prop to change the style of the Textarea.
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
appearance: 'white'
---
::
### Disabled ### Disabled
Use the `disabled` prop to disable the Textarea. Use the `disabled` prop to disable the Textarea.
@@ -62,7 +95,6 @@ baseProps:
name: 'input' name: 'input'
placeholder: 'Search...' placeholder: 'Search...'
props: props:
appearance: 'white'
disabled: true disabled: true
--- ---
:: ::

View File

@@ -22,9 +22,65 @@ excludedProps:
--- ---
:: ::
### Style
Use the `color` and `variant` props to change the visual style of the Select.
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size ### Size
Use the `size` prop to change the size of the Input. Use the `size` prop to change the size of the Select.
::component-card ::component-card
--- ---
@@ -56,29 +112,11 @@ props:
--- ---
:: ::
### Appearance
Use the `appearance` prop to change the style of the Select.
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
placeholder: 'Search...'
props:
appearance: 'white'
---
::
### Icon ### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`. Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `trailingIcon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`. Use the `trailing-icon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
::component-card ::component-card
--- ---
@@ -91,8 +129,11 @@ baseProps:
placeholder: 'Search...' placeholder: 'Search...'
props: props:
icon: 'i-heroicons-magnifying-glass-20-solid' icon: 'i-heroicons-magnifying-glass-20-solid'
appearance: 'white' color: 'white'
size: 'sm' size: 'sm'
extraColors:
- white
- gray
excludedProps: excludedProps:
- icon - icon
--- ---
@@ -100,7 +141,7 @@ excludedProps:
### Disabled ### Disabled
Use the `disabled` prop to disable the Input. Use the `disabled` prop to disable the Select.
::component-card ::component-card
--- ---
@@ -112,7 +153,6 @@ baseProps:
- 'Mexico' - 'Mexico'
placeholder: 'Search...' placeholder: 'Search...'
props: props:
appearance: 'white'
disabled: true disabled: true
--- ---
:: ::

View File

@@ -8,7 +8,7 @@ headlessui:
## Usage ## Usage
The SelectMenu component renders by default a [Select](/forms/select) component and is based on the `ui.select` preset. You can use most of the Select props to configure the display if you don't want to override the default slot such as [size](/forms/select#size), [placeholder](/forms/select#placeholder), [appearance](/forms/select#appearance), [icon](/forms/select#icon), [disabled](/forms/select#disabled), etc. The SelectMenu component renders by default a [Select](/forms/select) component and is based on the `ui.select` preset. You can use most of the Select props to configure the display if you don't want to override the default slot such as [color](/forms/select#style), [variant](/forms/select#style), [size](/forms/select#size), [placeholder](/forms/select#placeholder), [icon](/forms/select#icon), [disabled](/forms/select#disabled), etc.
Like the Select component, you can use the `options` prop to pass an array of strings or objects. Like the Select component, you can use the `options` prop to pass an array of strings or objects.
@@ -81,7 +81,7 @@ const selected = ref(people[3])
``` ```
:: ::
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `optionAttribute` prop that defaults to `label`. You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `option-attribute` prop that defaults to `label`.
::component-example ::component-example
#default #default
@@ -136,9 +136,9 @@ const selected = ref(people[0])
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`. Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `trailingIcon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`. Use the `trailing-icon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
Use the `selectedIcon` prop to set a different icon or change it globally in `ui.selectMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`. Use the `selected-icon` prop to set a different icon or change it globally in `ui.selectMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
::component-card ::component-card
--- ---
@@ -148,6 +148,10 @@ baseProps:
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer'] options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props: props:
icon: 'i-heroicons-magnifying-glass-20-solid' icon: 'i-heroicons-magnifying-glass-20-solid'
color: 'white'
extraColors:
- white
- gray
excludedProps: excludedProps:
- icon - icon
--- ---
@@ -157,6 +161,8 @@ excludedProps:
Use the `searchable` prop to enable search. Use the `searchable` prop to enable search.
Use the `searchable-placeholder` prop to set a different placeholder.
This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) component instead of [Listbox](https://headlessui.com/vue/listbox). This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) component instead of [Listbox](https://headlessui.com/vue/listbox).
::component-card ::component-card
@@ -167,6 +173,7 @@ baseProps:
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer'] options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props: props:
searchable: true searchable: true
searchablePlaceholder: 'Search a person...'
--- ---
:: ::

View File

@@ -0,0 +1,140 @@
---
github: true
description: Display a label and additional informations around a form element.
---
## Usage
Use the FormGroup component around an [Input](/forms/input), [Textarea](/forms/textarea), [Select](/forms/select) or a [SelectMenu](/forms/select-menu) with the `name` prop to automatically associate a `<label>` element with the form element.
::component-card
---
props:
name: 'email'
label: 'Email'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Required
Use the `required` prop to indicate that the form element is required.
::component-card
---
baseProps:
name: 'group-required'
props:
label: 'Email'
required: true
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Description
Use the `description` prop to display a description below the label.
::component-card
---
baseProps:
name: 'group-description'
props:
label: 'Email'
description: "We'll only use this for spam."
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Hint
Use the `hint` prop to display a hint above the form element.
::component-card
---
baseProps:
name: 'group-hint'
props:
label: 'Email'
hint: 'Optional'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Help
Use the `help` prop to display an help message below the form element.
::component-card
---
baseProps:
name: 'group-help'
props:
label: 'Email'
help: 'We will never share your email with anyone else.'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Error
Use the `error` prop to display an error message below the form element.
When used together with the `help` prop, the `error` prop will take precedence.
::component-card
---
baseProps:
name: 'group-error'
props:
label: 'Email'
help: 'We will never share your email with anyone else.'
error: "Not a valid email address."
code: >-
<UInput placeholder="you@example.com" trailing-icon="i-heroicons-exclamation-triangle-20-solid" />
---
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com" trailing-icon="i-heroicons-exclamation-triangle-20-solid"}
::
You can also use the `error` prop as a boolean to mark the form element as invalid.
::alert{icon="i-heroicons-light-bulb"}
The `error` prop will automatically set the `color` prop of the form element to `red`.
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -16,7 +16,8 @@ const links = [{
label: 'Profile', label: 'Profile',
avatar: { avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4' src: 'https://avatars.githubusercontent.com/u/739984?v=4'
} },
badge: 100
}, { }, {
label: 'Installation', label: 'Installation',
icon: 'i-heroicons-home', icon: 'i-heroicons-home',
@@ -38,6 +39,48 @@ const links = [{
``` ```
:: ::
## Themes
Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do.
### Tailwind
::component-example
#default
:vertical-navigation-theme-tailwind
#code
```vue
<script setup>
const links = [{
label: 'Installation',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
to: '/navigation/vertical-navigation'
}, {
label: 'Command Palette',
to: '/navigation/command-palette'
}]
</script>
<template>
<UVerticalNavigation
:links="links"
:ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-l -ml-px lg:leading-6',
padding: 'pl-4',
rounded: '',
font: '',
ring: '',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}"
/>
</template>
```
::
## Props ## Props
:component-props :component-props

View File

@@ -160,7 +160,7 @@ function onSelect (option) {
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`. Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `selectedIcon` prop to set a different icon or change it globally in `ui.commandPalette.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`. Use the `selected-icon` prop to set a different icon or change it globally in `ui.commandPalette.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
::component-card ::component-card
--- ---
@@ -174,6 +174,24 @@ excludedProps:
--- ---
:: ::
### Loading
Use the `loading` prop to show a loading icon.
Use the `loading-icon` prop to set a different icon or change it globally in `ui.commandPalette.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
::component-card
---
padding: false
baseProps:
empty: null
props:
loading: true
excludedProps:
- icon
---
::
### Placeholder ### Placeholder
Use the `placeholder` prop to change the input placeholder Use the `placeholder` prop to change the input placeholder
@@ -218,6 +236,8 @@ Use the `empty` prop to display a message when there are no results.
You can pass an `object` through the `empty` prop or globally through `ui.commandPalette.default.empty`. Here is the default: You can pass an `object` through the `empty` prop or globally through `ui.commandPalette.default.empty`. Here is the default:
You can also set it to `null` to hide the empty label.
::component-card ::component-card
--- ---
padding: false padding: false
@@ -237,7 +257,7 @@ excludedProps:
The CommandPalette component takes care of the full-text search for you with [Fuse.js](https://fusejs.io). You can pass all the options of Fuse.js through the `fuse` prop. The CommandPalette component takes care of the full-text search for you with [Fuse.js](https://fusejs.io). You can pass all the options of Fuse.js through the `fuse` prop.
When searching for a command, the component will look for a `label` property on the command by default. You can customize this behaviour by overriding the `commandAttribute` prop. This will also affect the display of the command. When searching for a command, the component will look for a `label` property on the command by default. You can customize this behaviour by overriding the `command-attribute` prop. This will also affect the display of the command.
You can also highlight the matches in the command by setting the `fuse.fuseOptions.includeMatches` to `true`. The CommandPalette component automatically takes care of the highlighting for you. You can also highlight the matches in the command by setting the `fuse.fuseOptions.includeMatches` to `true`. The CommandPalette component automatically takes care of the highlighting for you.
@@ -299,6 +319,10 @@ const groups = computed(() => {
``` ```
:: ::
::alert{icon="i-heroicons-light-bulb"}
The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behaviour by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`.
::
## Themes ## Themes
Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do. Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do.

View File

@@ -92,8 +92,8 @@ baseProps:
id: 4 id: 4
timeout: 0 timeout: 0
title: 'Notification' title: 'Notification'
description: 'This is a notification.'
props: props:
description: 'This is a notification.'
avatar: avatar:
src: 'https://avatars.githubusercontent.com/u/739984?v=4' src: 'https://avatars.githubusercontent.com/u/739984?v=4'
excludedProps: excludedProps:
@@ -114,7 +114,28 @@ baseProps:
title: 'Notification' title: 'Notification'
description: 'This is a notification.' description: 'This is a notification.'
props: props:
timeout: 10000 timeout: 60000
---
::
### Color
Use the `color` prop to change the progress and icon color of the Notification.
::component-card
---
baseProps:
id: 5
title: 'Notification'
description: 'This is a notification.'
timeout: 600000
props:
icon: 'i-heroicons-x-circle'
color: 'red'
extraColors:
- gray
excludedProps:
- icon
--- ---
:: ::

View File

@@ -17,6 +17,7 @@ export default defineNuxtConfig({
highlight: { highlight: {
theme: { theme: {
light: 'material-lighter', light: 'material-lighter',
default: 'material-default',
dark: 'material-palenight' dark: 'material-palenight'
}, },
preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'diff', 'shell', 'markdown', 'yaml', 'bash', 'ini'] preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'diff', 'shell', 'markdown', 'yaml', 'bash', 'ini']
@@ -30,14 +31,18 @@ export default defineNuxtConfig({
strict: false, strict: false,
includeWorkspace: true includeWorkspace: true
}, },
// @ts-ignore
$production: {
routeRules: {
'/api/_content/**': { isr: true, static: true },
'/api/component-meta/**': { isr: true, static: true }
}
},
routeRules: { routeRules: {
// '/getting-started': { swr: 100000 } '/': { redirect: '/getting-started' }
},
generate: {
routes: ['/getting-started']
},
componentMeta: {
metaFields: {
props: true,
slots: false,
events: false,
exposed: false
}
} }
}) })

View File

@@ -1,7 +0,0 @@
<template>
<div />
</template>
<script setup lang="ts">
await navigateTo('/getting-started')
</script>

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nuxthq/ui", "name": "@nuxthq/ui",
"version": "2.1.0", "version": "2.2.1",
"repository": "https://github.com/nuxtlabs/ui", "repository": "https://github.com/nuxtlabs/ui",
"license": "MIT", "license": "MIT",
"exports": { "exports": {
@@ -18,23 +18,24 @@
"node": ">=16.14.0" "node": ">=16.14.0"
}, },
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm",
"build": "nuxt-module-build", "build": "nuxt-module-build",
"prepack": "yarn build", "prepack": "pnpm build",
"dev": "nuxi dev docs", "dev": "nuxi dev docs",
"build:docs": "nuxi build docs", "build:docs": "nuxi generate docs",
"lint": "eslint .", "lint": "eslint .",
"typecheck": "nuxi typecheck", "typecheck": "nuxi typecheck",
"prepare": "nuxi prepare docs", "prepare": "nuxi prepare docs",
"release": "yarn lint && standard-version && git push --follow-tags" "release": "pnpm lint && standard-version && git push --follow-tags"
}, },
"dependencies": { "dependencies": {
"@egoist/tailwindcss-icons": "^1.0.7", "@egoist/tailwindcss-icons": "^1.0.7",
"@headlessui/vue": "1.7.10", "@headlessui/vue": "1.7.10",
"@iconify-json/heroicons": "^1.1.10", "@iconify-json/heroicons": "^1.1.10",
"@nuxt/kit": "^3.4.3", "@nuxt/kit": "^3.5.1",
"@nuxtjs/color-mode": "^3.2.0", "@nuxtjs/color-mode": "^3.2.0",
"@nuxtjs/tailwindcss": "^6.7.0", "@nuxtjs/tailwindcss": "^6.7.0",
"@popperjs/core": "^2.11.7", "@popperjs/core": "^2.11.8",
"@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
@@ -47,19 +48,19 @@
"tailwindcss": "^3.3.2" "tailwindcss": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/simple-icons": "^1.1.53", "@iconify-json/simple-icons": "^1.1.54",
"@nuxt/content": "^2.6.0", "@nuxt/content": "^2.6.0",
"@nuxt/devtools": "^0.4.6", "@nuxt/devtools": "^0.5.5",
"@nuxt/eslint-config": "^0.1.1", "@nuxt/eslint-config": "^0.1.1",
"@nuxt/module-builder": "^0.3.1", "@nuxt/module-builder": "^0.4.0",
"@nuxthq/studio": "^0.12.1", "@nuxthq/studio": "^0.12.1",
"@nuxtjs/plausible": "^0.2.1", "@nuxtjs/plausible": "^0.2.1",
"@types/lodash-es": "^4.17.7", "@types/lodash-es": "^4.17.7",
"@types/node": "^20.1.7", "@types/node": "^20.2.4",
"@vueuse/nuxt": "^10.1.2", "@vueuse/nuxt": "^10.1.2",
"eslint": "^8.40.0", "eslint": "^8.41.0",
"nuxt": "^3.4.3", "nuxt": "^3.5.1",
"nuxt-component-meta": "^0.5.1", "nuxt-component-meta": "^0.5.3",
"nuxt-lodash": "^2.4.1", "nuxt-lodash": "^2.4.1",
"standard-version": "^9.5.0", "standard-version": "^9.5.0",
"unbuild": "^1.2.1", "unbuild": "^1.2.1",

10100
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
#!/bin/bash #!/bin/bash
# Restore all git changes # Restore all git changes
git restore -s@ -SW -- example src test git restore -s@ -SW -- .
# Bump versions to edge # Bump versions to edge
yarn jiti ./scripts/bump-edge pnpm jiti ./scripts/bump-edge
# Resolve yarn # Resolve pnpm
yarn pnpm install
# Update token # Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then

View File

@@ -1,10 +1,10 @@
#!/bin/bash #!/bin/bash
# Restore all git changes # Restore all git changes
git restore -s@ -SW -- example src test git restore -s@ -SW -- .
# Resolve yarn # Resolve pnpm
yarn pnpm install
# Update token # Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then

View File

@@ -121,45 +121,49 @@ export default defineNuxtModule<ModuleOptions>({
} }
tailwindConfig.safelist = tailwindConfig.safelist || [] tailwindConfig.safelist = tailwindConfig.safelist || []
tailwindConfig.safelist.push(...['bg-gray-400', { tailwindConfig.safelist.push(...[
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|400|500)`) 'bg-gray-500',
}, { 'dark:bg-gray-400',
pattern: new RegExp(`bg-(${safeColorsAsRegex})-500`), {
variants: ['disabled'] pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|400|500)`)
}, { }, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(400|950)`), pattern: new RegExp(`bg-(${safeColorsAsRegex})-500`),
variants: ['dark'] variants: ['disabled']
}, { }, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(500|900|950)`), pattern: new RegExp(`bg-(${safeColorsAsRegex})-(400|950)`),
variants: ['dark:hover'] variants: ['dark']
}, { }, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`), pattern: new RegExp(`bg-(${safeColorsAsRegex})-(500|900|950)`),
variants: ['dark:disabled'] variants: ['dark:hover']
}, { }, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|100|600)`), pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`),
variants: ['hover'] variants: ['dark:disabled']
}, { }, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-500`), pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|100|600)`),
variants: ['focus-visible'] variants: ['hover']
}, { }, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-400`), pattern: new RegExp(`outline-(${safeColorsAsRegex})-500`),
variants: ['dark:focus-visible'] variants: ['focus-visible']
}, { }, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-500`), pattern: new RegExp(`outline-(${safeColorsAsRegex})-400`),
variants: ['focus-visible'] variants: ['dark:focus-visible']
}, { }, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-400`), pattern: new RegExp(`ring-(${safeColorsAsRegex})-500`),
variants: ['dark', 'dark:focus-visible'] variants: ['focus', 'focus-visible']
}, { }, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-400`), pattern: new RegExp(`ring-(${safeColorsAsRegex})-400`),
variants: ['dark'] variants: ['dark', 'dark:focus', 'dark:focus-visible']
}, { }, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-600`), pattern: new RegExp(`text-(${safeColorsAsRegex})-400`),
variants: ['hover'] variants: ['dark']
}, { }, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-500`), pattern: new RegExp(`text-(${safeColorsAsRegex})-500`),
variants: ['dark:hover'] variants: ['dark:hover']
}]) }, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-600`),
variants: ['hover']
}
])
tailwindConfig.plugins = tailwindConfig.plugins || [] tailwindConfig.plugins = tailwindConfig.plugins || []
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) })) tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))

View File

@@ -18,15 +18,13 @@ const avatar = {
}, },
chip: { chip: {
base: 'absolute block rounded-full ring-1 ring-white dark:ring-gray-900', base: 'absolute block rounded-full ring-1 ring-white dark:ring-gray-900',
background: 'bg-{color}-500 dark:bg-{color}-400',
position: { position: {
'top-right': 'top-0 right-0', 'top-right': 'top-0 right-0',
'bottom-right': 'bottom-0 right-0', 'bottom-right': 'bottom-0 right-0',
'top-left': 'top-0 left-0', 'top-left': 'top-0 left-0',
'bottom-left': 'bottom-0 left-0' 'bottom-left': 'bottom-0 left-0'
}, },
variant: {
solid: 'bg-{color}-400'
},
size: { size: {
'3xs': 'h-1 w-1', '3xs': 'h-1 w-1',
'2xs': 'h-1 w-1', '2xs': 'h-1 w-1',
@@ -41,7 +39,7 @@ const avatar = {
}, },
default: { default: {
size: 'sm', size: 'sm',
chipVariant: 'solid', chipColor: null,
chipPosition: 'top-right' chipPosition: 'top-right'
} }
} }
@@ -220,7 +218,9 @@ const kbd = {
const input = { const input = {
wrapper: 'relative', wrapper: 'relative',
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none', base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0',
rounded: 'rounded-md',
placeholder: 'placeholder-gray-400 dark:placeholder-gray-500',
custom: '', custom: '',
size: { size: {
'2xs': 'text-xs', '2xs': 'text-xs',
@@ -266,13 +266,21 @@ const input = {
xl: 'pr-12' xl: 'pr-12'
} }
}, },
appearance: { color: {
white: 'border-0 bg-white dark:bg-gray-900 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500', white: {
gray: 'border-0 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500', 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',
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none placeholder:text-gray-400 dark:placeholder:text-gray-500' },
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',
}
},
variant: {
outline: 'shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 focus:ring-2 focus:ring-{color}-500 dark:focus:ring-{color}-400',
none: 'bg-transparent focus:ring-0 focus:shadow-none'
}, },
icon: { icon: {
base: 'text-gray-400 dark:text-gray-500', base: 'flex-shrink-0 text-gray-400 dark:text-gray-500',
color: 'text-{color}-500 dark:text-{color}-400',
size: { size: {
'2xs': 'h-3.5 w-3.5', '2xs': 'h-3.5 w-3.5',
xs: 'h-4 w-4', xs: 'h-4 w-4',
@@ -306,27 +314,32 @@ const input = {
}, },
default: { default: {
size: 'sm', size: 'sm',
appearance: 'white', color: 'white',
variant: 'outline',
loadingIcon: 'i-heroicons-arrow-path-20-solid' loadingIcon: 'i-heroicons-arrow-path-20-solid'
} }
} }
const inputGroup = { const formGroup = {
wrapper: '', wrapper: '',
label: 'block text-sm font-medium text-gray-700 dark:text-gray-200', label: {
labelWrapper: 'flex content-center justify-between', 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`
},
description: 'text-sm text-gray-500 dark:text-gray-400',
container: 'mt-1 relative', container: 'mt-1 relative',
required: 'text-red-500 dark:text-red-400 ml-0.5', hint: 'text-sm text-gray-500 dark:text-gray-400',
description: 'text-sm leading-5 text-gray-500 dark:text-gray-400', help: 'mt-2 text-sm text-gray-500 dark:text-gray-400',
hint: 'text-sm leading-5 text-gray-500 dark:text-gray-400', error: 'mt-2 text-sm text-red-500 dark:text-red-400'
help: 'mt-2 text-sm text-gray-500 dark:text-gray-400'
} }
const textarea = { const textarea = {
...input, ...input,
default: { default: {
size: 'sm', size: 'sm',
appearance: 'white' color: 'white',
variant: 'outline',
} }
} }
@@ -334,7 +347,8 @@ const select = {
...input, ...input,
default: { default: {
size: 'sm', size: 'sm',
appearance: 'white', color: 'white',
variant: 'outline',
trailingIcon: 'i-heroicons-chevron-down-20-solid' trailingIcon: 'i-heroicons-chevron-down-20-solid'
} }
} }
@@ -396,7 +410,7 @@ const selectMenu = {
const radio = { const radio = {
wrapper: 'relative flex items-start', wrapper: 'relative flex items-start',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus:ring-offset-white dark:focus:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent', 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',
label: 'font-medium text-gray-700 dark:text-gray-200', label: 'font-medium text-gray-700 dark:text-gray-200',
required: 'text-red-500 dark:text-red-400', required: 'text-red-500 dark:text-red-400',
help: 'text-gray-500 dark:text-gray-400' help: 'text-gray-500 dark:text-gray-400'
@@ -467,8 +481,13 @@ const skeleton = {
const verticalNavigation = { const verticalNavigation = {
wrapper: 'relative', wrapper: 'relative',
base: 'group flex items-center gap-2 text-sm font-medium rounded-md w-full relative focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:before:ring-inset focus-visible:before:ring-1 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 before:absolute before:inset-px before:rounded-md disabled:cursor-not-allowed disabled:opacity-75', base: 'group relative flex items-center gap-2 focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-1 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 before:absolute before:inset-px before:rounded-md disabled:cursor-not-allowed disabled:opacity-75',
ring: 'focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
padding: 'px-3 py-1.5', padding: 'px-3 py-1.5',
width: 'w-full',
rounded: 'rounded-md',
font: 'font-medium',
size: 'text-sm',
active: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800', active: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800',
inactive: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50', inactive: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50',
label: 'truncate relative', label: 'truncate relative',
@@ -482,9 +501,9 @@ const verticalNavigation = {
size: '3xs' size: '3xs'
}, },
badge: { badge: {
base: 'ml-auto inline-block py-0.5 px-2 text-xs rounded-md -mr-1 -my-0.5', base: 'relative ml-auto inline-block py-0.5 px-2 text-xs rounded-md -mr-1 -my-0.5',
active: 'bg-white dark:bg-gray-900', active: 'bg-white dark:bg-gray-900',
inactive: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 group-hover:bg-white dark:group-hover:bg-gray-900' inactive: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white group-hover:bg-white dark:group-hover:bg-gray-900'
} }
} }
@@ -545,6 +564,7 @@ const commandPalette = {
}, },
default: { default: {
icon: 'i-heroicons-magnifying-glass-20-solid', icon: 'i-heroicons-magnifying-glass-20-solid',
loadingIcon: 'i-heroicons-arrow-path-20-solid',
empty: { empty: {
icon: 'i-heroicons-magnifying-glass-20-solid', icon: 'i-heroicons-magnifying-glass-20-solid',
label: 'We couldn\'t find any items.', label: 'We couldn\'t find any items.',
@@ -694,10 +714,20 @@ const notification = {
background: 'bg-white dark:bg-gray-900', background: 'bg-white dark:bg-gray-900',
shadow: 'shadow-lg', shadow: 'shadow-lg',
rounded: 'rounded-lg', rounded: 'rounded-lg',
padding: 'p-4',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800', ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
icon: 'flex-shrink-0 w-5 h-5 text-gray-900 dark:text-white', icon: {
avatar: 'flex-shrink-0 pt-0.5', base: 'flex-shrink-0 w-5 h-5 text-gray-400 dark:text-gray-500',
progress: 'absolute bottom-0 left-0 right-0 h-1 bg-primary-500 dark:bg-primary-400', color: 'text-{color}-500 dark:text-{color}-400'
},
avatar: {
base: 'flex-shrink-0 self-center',
size: 'md'
},
progress: {
base: 'absolute bottom-0 left-0 right-0 h-1',
background: 'bg-{color}-500 dark:bg-{color}-400'
},
transition: { transition: {
enterActiveClass: 'transform ease-out duration-300 transition', enterActiveClass: 'transform ease-out duration-300 transition',
enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2', enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2',
@@ -707,6 +737,8 @@ const notification = {
leaveToClass: 'opacity-0' leaveToClass: 'opacity-0'
}, },
default: { default: {
color: 'primary',
icon: null,
close: { close: {
icon: 'i-heroicons-x-mark-20-solid', icon: 'i-heroicons-x-mark-20-solid',
color: 'gray', color: 'gray',
@@ -737,7 +769,7 @@ export default {
dropdown, dropdown,
kbd, kbd,
input, input,
inputGroup, formGroup,
textarea, textarea,
select, select,
selectMenu, selectMenu,

View File

@@ -43,18 +43,11 @@ export default defineComponent({
}, },
chipColor: { chipColor: {
type: String, type: String,
default: null, default: () => appConfig.ui.avatar.default.chipColor,
validator (value: string) { validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value) return ['gray', ...appConfig.ui.colors].includes(value)
} }
}, },
chipVariant: {
type: String,
default: () => appConfig.ui.avatar.default.chipVariant,
validator (value: string) {
return Object.keys(appConfig.ui.avatar.chip.variant).includes(value)
}
},
chipPosition: { chipPosition: {
type: String, type: String,
default: () => appConfig.ui.avatar.default.chipPosition, default: () => appConfig.ui.avatar.default.chipPosition,
@@ -94,7 +87,7 @@ export default defineComponent({
ui.value.chip.base, ui.value.chip.base,
ui.value.chip.size[props.size], ui.value.chip.size[props.size],
ui.value.chip.position[props.chipPosition], ui.value.chip.position[props.chipPosition],
ui.value.chip.variant[props.chipVariant]?.replaceAll('{color}', props.chipColor) ui.value.chip.background.replaceAll('{color}', props.chipColor)
) )
}) })

View File

@@ -1,7 +1,7 @@
import { h, computed, defineComponent } from 'vue' import { h, computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { classNames } from '../../utils' import { classNames, getSlotsChildren } from '../../utils'
import Avatar from './Avatar.vue' import Avatar from './Avatar.vue'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
@@ -34,20 +34,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defu({}, props.ui, appConfig.ui.avatarGroup)) const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defu({}, props.ui, appConfig.ui.avatarGroup))
const children = computed(() => { const children = computed(() => getSlotsChildren(slots))
let children = slots.default?.()
if (children.length) {
if (typeof children[0].type === 'symbol') {
// @ts-ignore-next
children = children[0].children
// @ts-ignore-next
} else if (children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
}
return children
})
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max) const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max)

View File

@@ -18,7 +18,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, useSlots } from 'vue' import { computed, defineComponent, useSlots } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { RouteLocationNormalized, RouteLocationRaw } from 'vue-router' import type { RouteLocationRaw } from 'vue-router'
import { defu } from 'defu' import { defu } from 'defu'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import { classNames } from '../../utils' import { classNames } from '../../utils'
@@ -32,7 +32,8 @@ import appConfig from '#build/app.config'
export default defineComponent({ export default defineComponent({
components: { components: {
UIcon UIcon,
NuxtLink
}, },
props: { props: {
type: { type: {
@@ -108,7 +109,7 @@ export default defineComponent({
default: false default: false
}, },
to: { to: {
type: [String, Object] as PropType<string | RouteLocationNormalized | RouteLocationRaw>, type: [String, Object] as PropType<string | RouteLocationRaw>,
default: null default: null
}, },
target: { target: {
@@ -142,7 +143,7 @@ export default defineComponent({
const buttonIs = computed(() => { const buttonIs = computed(() => {
if (props.to) { if (props.to) {
return NuxtLink return 'NuxtLink'
} }
return 'button' return 'button'

View File

@@ -1,6 +1,7 @@
import { h, computed, defineComponent } from 'vue' import { h, computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { getSlotsChildren } from '../../utils'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -28,20 +29,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defu({}, props.ui, appConfig.ui.buttonGroup)) const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defu({}, props.ui, appConfig.ui.buttonGroup))
const children = computed(() => { const children = computed(() => getSlotsChildren(slots))
let children = slots.default?.()
if (children.length) {
if (typeof children[0].type === 'symbol') {
// @ts-ignore-next
children = children[0].children
// @ts-ignore-next
} else if (children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
}
return children
})
const rounded = computed(() => ({ const rounded = computed(() => ({
'rounded-none': { left: 'rounded-l-none', right: 'rounded-r-none' }, 'rounded-none': { left: 'rounded-l-none', right: 'rounded-r-none' },

View File

@@ -20,9 +20,8 @@
<MenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background]" static> <MenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background]" static>
<div v-for="(subItems, index) of items" :key="index" :class="ui.padding"> <div v-for="(subItems, index) of items" :key="index" :class="ui.padding">
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled"> <MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
<Component <ULinkCustom
v-bind="omit(item, ['click'])" v-bind="omit(item, ['label', 'icon', 'iconClass', 'avatar', 'shortcuts', 'click'])"
:is="(item.to && NuxtLink) || (item.click && 'button') || 'div'"
:class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]" :class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]"
@click="item.click" @click="item.click"
> >
@@ -36,7 +35,7 @@
<UKbd v-for="shortcut of item.shortcuts" :key="shortcut">{{ shortcut }}</UKbd> <UKbd v-for="shortcut of item.shortcuts" :key="shortcut">{{ shortcut }}</UKbd>
</span> </span>
</slot> </slot>
</Component> </ULinkCustom>
</MenuItem> </MenuItem>
</div> </div>
</MenuItems> </MenuItems>
@@ -48,17 +47,17 @@
<script lang="ts"> <script lang="ts">
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router' import type { RouteLocationRaw } from 'vue-router'
import { defineComponent, ref, computed, onMounted } from 'vue' import { defineComponent, ref, computed, onMounted } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue' import UKbd from '../elements/Kbd.vue'
import ULinkCustom from '../elements/LinkCustom.vue'
import { omit } from '../../utils' import { omit } from '../../utils'
import { usePopper } from '../../composables/usePopper' import { usePopper } from '../../composables/usePopper'
import type { Avatar as AvatarType } from '../../types/avatar' import type { Avatar } from '../../types/avatar'
import type { PopperOptions } from '../../types' import type { PopperOptions } from '../../types'
import { NuxtLink } from '#components'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -75,22 +74,23 @@ export default defineComponent({
MenuItem, MenuItem,
UIcon, UIcon,
UAvatar, UAvatar,
UKbd UKbd,
ULinkCustom
}, },
props: { props: {
items: { items: {
type: Array as PropType<{ type: Array as PropType<{
to?: RouteLocationNormalized to?: string | RouteLocationRaw
exact?: boolean exact?: boolean
label: string label: string
disabled?: boolean slot?: string
slot?: string icon?: string
icon?: string iconClass?: string
iconClass?: string avatar?: Partial<Avatar>
avatar?: Partial<AvatarType> shortcuts?: string[]
click?: Function disabled?: boolean
shortcuts?: string[] click?: Function
}[][]>, }[][]>,
default: () => [] default: () => []
}, },
mode: { mode: {
@@ -196,8 +196,7 @@ export default defineComponent({
container, container,
onMouseOver, onMouseOver,
onMouseLeave, onMouseLeave,
omit, omit
NuxtLink
} }
} }
}) })

View File

@@ -0,0 +1,82 @@
import { h, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { getSlotsChildren } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
props: {
name: {
type: String,
default: null
},
label: {
type: String,
default: null
},
description: {
type: String,
default: null
},
required: {
type: Boolean,
default: false
},
help: {
type: String,
default: null
},
error: {
type: [String, Boolean],
default: null
},
hint: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.formGroup>>,
default: () => appConfig.ui.formGroup
}
},
setup (props, { slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.formGroup>>(() => defu({}, props.ui, appConfig.ui.formGroup))
const children = computed(() => getSlotsChildren(slots))
const clones = computed(() => children.value.map((node) => {
if (props.error) {
node.props.oldColor = node.props.color
node.props.color = 'red'
} else {
node.props.color = node.props.oldColor
}
if (props.name) {
node.props.name = props.name
}
return node
}))
return () => h('div', { class: [ui.value.wrapper] }, [
props.label && h('div', { class: [ui.value.label.wrapper] }, [
h('label', { for: props.name, class: [ui.value.label.base, props.required && ui.value.label.required] }, props.label),
props.hint && h('span', { class: [ui.value.hint] }, props.hint)
]),
props.description && h('p', { class: [ui.value.description] }, props.description),
h('div', { class: [!!props.label && ui.value.container] }, [
...clones.value,
props.error && typeof props.error === 'string' ? h('p', { class: [ui.value.error] }, props.error) : props.help ? h('p', { class: [ui.value.help] }, props.help) : null
])
])
}
})

View File

@@ -55,7 +55,7 @@ export default defineComponent({
}, },
name: { name: {
type: String, type: String,
required: true default: null
}, },
placeholder: { placeholder: {
type: String, type: String,
@@ -113,6 +113,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
padded: {
type: Boolean,
default: true
},
size: { size: {
type: String, type: String,
default: () => appConfig.ui.input.default.size, default: () => appConfig.ui.input.default.size,
@@ -120,11 +124,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.input.size).includes(value) return Object.keys(appConfig.ui.input.size).includes(value)
} }
}, },
appearance: { color: {
type: String, type: String,
default: () => appConfig.ui.input.default.appearance, default: () => appConfig.ui.input.default.color,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.input.appearance).includes(value) return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.input.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.input.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.input.variant),
...Object.values(appConfig.ui.input.color).flatMap(value => Object.keys(value))
].includes(value)
} }
}, },
ui: { ui: {
@@ -158,11 +172,15 @@ export default defineComponent({
}) })
const inputClass = computed(() => { const inputClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames( return classNames(
ui.value.base, ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size], ui.value.size[props.size],
ui.value.padding[props.size], props.padded && ui.value.padding[props.size],
ui.value.appearance[props.appearance], variant?.replaceAll('{color}', props.color),
isLeading.value && ui.value.leading.padding[props.size], isLeading.value && ui.value.leading.padding[props.size],
isTrailing.value && ui.value.trailing.padding[props.size], isTrailing.value && ui.value.trailing.padding[props.size],
ui.value.custom ui.value.custom
@@ -196,6 +214,7 @@ export default defineComponent({
const iconClass = computed(() => { const iconClass = computed(() => {
return classNames( return classNames(
ui.value.icon.base, ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size], ui.value.icon.size[props.size],
props.loading && 'animate-spin' props.loading && 'animate-spin'
) )

View File

@@ -1,78 +0,0 @@
<template>
<div :class="ui.wrapper">
<div v-if="label || $slots.label" :class="ui.labelWrapper">
<label :for="name" :class="ui.label">
<slot name="label">{{ label }}</slot>
<span v-if="required" :class="ui.required">*</span>
</label>
<span v-if="$slots.hint || hint" :class="ui.hint">
<slot name="hint">{{ hint }}</slot>
</span>
</div>
<p v-if="description" :class="ui.description">
{{ description }}
</p>
<div :class="!!label && ui.container">
<slot />
<p v-if="help" :class="ui.help">
{{ help }}
</p>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
props: {
name: {
type: String,
default: null
},
label: {
type: String,
default: null
},
description: {
type: String,
default: null
},
required: {
type: Boolean,
default: false
},
help: {
type: String,
default: null
},
hint: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.inputGroup>>,
default: () => appConfig.ui.inputGroup
}
},
setup (props) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.inputGroup>>(() => defu({}, props.ui, appConfig.ui.inputGroup))
return {
// eslint-disable-next-line vue/no-dupe-keys
ui
}
}
})
</script>

View File

@@ -71,7 +71,7 @@ export default defineComponent({
}, },
name: { name: {
type: String, type: String,
required: true default: null
}, },
placeholder: { placeholder: {
type: String, type: String,
@@ -97,6 +97,10 @@ export default defineComponent({
type: Array, type: Array,
default: () => [] default: () => []
}, },
padded: {
type: Boolean,
default: true
},
size: { size: {
type: String, type: String,
default: () => appConfig.ui.select.default.size, default: () => appConfig.ui.select.default.size,
@@ -104,11 +108,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.select.size).includes(value) return Object.keys(appConfig.ui.select.size).includes(value)
} }
}, },
appearance: { color: {
type: String, type: String,
default: () => appConfig.ui.select.default.appearance, default: () => appConfig.ui.select.default.color,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.select.appearance).includes(value) return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.select.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.select.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.select.variant),
...Object.values(appConfig.ui.select.color).flatMap(value => Object.keys(value))
].includes(value)
} }
}, },
textAttribute: { textAttribute: {
@@ -188,11 +202,15 @@ export default defineComponent({
}) })
const selectClass = computed(() => { const selectClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames( return classNames(
ui.value.base, ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size], ui.value.size[props.size],
ui.value.padding[props.size], props.padded && ui.value.padding[props.size],
ui.value.appearance[props.appearance], variant?.replaceAll('{color}', props.color),
!!props.icon && ui.value.leading.padding[props.size], !!props.icon && ui.value.leading.padding[props.size],
ui.value.trailing.padding[props.size], ui.value.trailing.padding[props.size],
ui.value.custom ui.value.custom
@@ -202,6 +220,7 @@ export default defineComponent({
const iconClass = computed(() => { const iconClass = computed(() => {
return classNames( return classNames(
ui.value.icon.base, ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size] ui.value.icon.size[props.size]
) )
}) })

View File

@@ -53,7 +53,7 @@
ref="searchInput" ref="searchInput"
:display-value="() => query" :display-value="() => query"
name="q" name="q"
placeholder="Search..." :placeholder="searchablePlaceholder"
autofocus autofocus
autocomplete="off" autocomplete="off"
:class="ui.input" :class="ui.input"
@@ -186,6 +186,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
searchablePlaceholder: {
type: String,
default: 'Search...'
},
creatable: { creatable: {
type: Boolean, type: Boolean,
default: false default: false
@@ -194,6 +198,10 @@ export default defineComponent({
type: String, type: String,
default: null default: null
}, },
padded: {
type: Boolean,
default: true
},
size: { size: {
type: String, type: String,
default: () => appConfig.ui.select.default.size, default: () => appConfig.ui.select.default.size,
@@ -201,11 +209,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.select.size).includes(value) return Object.keys(appConfig.ui.select.size).includes(value)
} }
}, },
appearance: { color: {
type: String, type: String,
default: () => appConfig.ui.select.default.appearance, default: () => appConfig.ui.select.default.color,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.select.appearance).includes(value) return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.select.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.select.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.select.variant),
...Object.values(appConfig.ui.select.color).flatMap(value => Object.keys(value))
].includes(value)
} }
}, },
optionAttribute: { optionAttribute: {
@@ -245,13 +263,17 @@ export default defineComponent({
const searchInput = ref<ComponentPublicInstance<HTMLElement>>() const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
const selectMenuClass = computed(() => { const selectMenuClass = computed(() => {
const variant = uiSelect.value.color?.[props.color as string]?.[props.variant as string] || uiSelect.value.variant[props.variant]
return classNames( return classNames(
uiSelect.value.base, uiSelect.value.base,
uiSelect.value.rounded,
uiSelect.value.placeholder,
'text-left cursor-default', 'text-left cursor-default',
uiSelect.value.size[props.size], uiSelect.value.size[props.size],
uiSelect.value.gap[props.size], uiSelect.value.gap[props.size],
uiSelect.value.padding[props.size], props.padded && uiSelect.value.padding[props.size],
uiSelect.value.appearance[props.appearance], variant?.replaceAll('{color}', props.color),
!!props.icon && uiSelect.value.leading.padding[props.size], !!props.icon && uiSelect.value.leading.padding[props.size],
uiSelect.value.trailing.padding[props.size], uiSelect.value.trailing.padding[props.size],
uiSelect.value.custom, uiSelect.value.custom,
@@ -262,6 +284,7 @@ export default defineComponent({
const iconClass = computed(() => { const iconClass = computed(() => {
return classNames( return classNames(
uiSelect.value.icon.base, uiSelect.value.icon.base,
appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
uiSelect.value.icon.size[props.size] uiSelect.value.icon.size[props.size]
) )
}) })

View File

@@ -38,7 +38,7 @@ export default defineComponent({
}, },
name: { name: {
type: String, type: String,
required: true default: null
}, },
placeholder: { placeholder: {
type: String, type: String,
@@ -72,6 +72,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
padded: {
type: Boolean,
default: true
},
size: { size: {
type: String, type: String,
default: () => appConfig.ui.textarea.default.size, default: () => appConfig.ui.textarea.default.size,
@@ -79,11 +83,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.textarea.size).includes(value) return Object.keys(appConfig.ui.textarea.size).includes(value)
} }
}, },
appearance: { color: {
type: String, type: String,
default: () => appConfig.ui.textarea.default.appearance, default: () => appConfig.ui.textarea.default.color,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.textarea.appearance).includes(value) return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.textarea.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.textarea.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.textarea.variant),
...Object.values(appConfig.ui.textarea.color).flatMap(value => Object.keys(value))
].includes(value)
} }
}, },
ui: { ui: {
@@ -146,11 +160,15 @@ export default defineComponent({
}) })
const textareaClass = computed(() => { const textareaClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames( return classNames(
ui.value.base, ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size], ui.value.size[props.size],
ui.value.padding[props.size], props.padded && ui.value.padding[props.size],
ui.value.appearance[props.appearance], variant?.replaceAll('{color}', props.color),
!props.resize && 'resize-none', !props.resize && 'resize-none',
ui.value.custom ui.value.custom
) )

View File

@@ -8,7 +8,7 @@
> >
<div :class="ui.wrapper"> <div :class="ui.wrapper">
<div v-show="searchable" :class="ui.input.wrapper"> <div v-show="searchable" :class="ui.input.wrapper">
<UIcon v-if="icon" :name="icon" :class="[ui.input.icon.base, ui.input.icon.size]" aria-hidden="true" /> <UIcon v-if="iconName" :name="iconName" :class="iconClass" aria-hidden="true" />
<ComboboxInput <ComboboxInput
ref="comboboxInput" ref="comboboxInput"
:value="query" :value="query"
@@ -73,7 +73,8 @@ import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import type { Group, Command } from '../../types/command-palette' import type { Group, Command } from '../../types/command-palette'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import type { Button as ButtonType } from '../../types/button' import type { Button } from '../../types/button'
import { classNames } from '../../utils'
import CommandPaletteGroup from './CommandPaletteGroup.vue' import CommandPaletteGroup from './CommandPaletteGroup.vue'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
@@ -112,6 +113,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: true default: true
}, },
loading: {
type: Boolean,
default: false
},
groups: { groups: {
type: Array as PropType<Group[]>, type: Array as PropType<Group[]>,
default: () => [] default: () => []
@@ -120,12 +125,16 @@ export default defineComponent({
type: String, type: String,
default: () => appConfig.ui.commandPalette.default.icon default: () => appConfig.ui.commandPalette.default.icon
}, },
loadingIcon: {
type: String,
default: () => appConfig.ui.commandPalette.default.loadingIcon
},
selectedIcon: { selectedIcon: {
type: String, type: String,
default: () => appConfig.ui.commandPalette.default.selectedIcon default: () => appConfig.ui.commandPalette.default.selectedIcon
}, },
close: { close: {
type: Object as PropType<Partial<ButtonType>>, type: Object as PropType<Partial<Button>>,
default: () => appConfig.ui.commandPalette.default.close default: () => appConfig.ui.commandPalette.default.close
}, },
empty: { empty: {
@@ -175,6 +184,7 @@ export default defineComponent({
const query = ref('') const query = ref('')
const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>() const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>()
const comboboxApi = ref(null) const comboboxApi = ref(null)
const isLoading = ref(false)
onMounted(() => { onMounted(() => {
if (props.autoselect) { if (props.autoselect) {
@@ -231,10 +241,17 @@ export default defineComponent({
const debouncedSearch = useDebounceFn(async () => { const debouncedSearch = useDebounceFn(async () => {
const searchableGroups = props.groups.filter(group => !!group.search) const searchableGroups = props.groups.filter(group => !!group.search)
if (!searchableGroups.length) {
return
}
isLoading.value = true
await Promise.all(searchableGroups.map(async (group) => { await Promise.all(searchableGroups.map(async (group) => {
searchResults.value[group.key] = await group.search(query.value) searchResults.value[group.key] = await group.search(query.value)
})) }))
isLoading.value = false
}, props.debounce) }, props.debounce)
watch(query, () => { watch(query, () => {
@@ -247,6 +264,22 @@ export default defineComponent({
}, 0) }, 0)
}) })
const iconName = computed(() => {
if ((props.loading || isLoading.value) && props.loadingIcon) {
return props.loadingIcon
}
return props.icon
})
const iconClass = computed(() => {
return classNames(
ui.value.input.icon.base,
ui.value.input.icon.size,
((props.loading || isLoading.value) && props.loadingIcon) && 'animate-spin'
)
})
// Methods // Methods
function activateFirstOption () { function activateFirstOption () {
@@ -292,6 +325,8 @@ export default defineComponent({
groups, groups,
comboboxInput, comboboxInput,
query, query,
iconName,
iconClass,
onSelect, onSelect,
onClear onClear
} }

View File

@@ -4,11 +4,11 @@
v-for="(link, index) of links" v-for="(link, index) of links"
v-slot="{ isActive }" v-slot="{ isActive }"
:key="index" :key="index"
v-bind="link" v-bind="omit(link, ['label', 'icon', 'iconClass', 'avatar', 'badge', 'click'])"
:class="[ui.base, ui.padding]" :class="[ui.base, ui.padding, ui.width, ui.ring, ui.rounded, ui.font, ui.size]"
:active-class="ui.active" :active-class="ui.active"
:inactive-class="ui.inactive" :inactive-class="ui.inactive"
@click="link.click && link.click()" @click="link.click"
@keyup.enter="$event.target.blur()" @keyup.enter="$event.target.blur()"
> >
<slot name="avatar" :link="link"> <slot name="avatar" :link="link">
@@ -29,7 +29,7 @@
<span v-if="link.label" :class="ui.label">{{ link.label }}</span> <span v-if="link.label" :class="ui.label">{{ link.label }}</span>
</slot> </slot>
<slot name="badge" :link="link" :is-active="isActive"> <slot name="badge" :link="link" :is-active="isActive">
<span v-if="link.badge" :class="[ui.badge.baseClass, isActive ? ui.badge.active : ui.badge.inactive]"> <span v-if="link.badge" :class="[ui.badge.base, isActive ? ui.badge.active : ui.badge.inactive]">
{{ link.badge }} {{ link.badge }}
</span> </span>
</slot> </slot>
@@ -40,12 +40,13 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue' import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router' import type { RouteLocationRaw } from 'vue-router'
import { defu } from 'defu' import { defu } from 'defu'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import ULinkCustom from '../elements/LinkCustom.vue' import ULinkCustom from '../elements/LinkCustom.vue'
import type { Avatar as AvatarType } from '../../types/avatar' import { omit } from '../../utils'
import type { Avatar } from '../../types/avatar'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -62,15 +63,15 @@ export default defineComponent({
props: { props: {
links: { links: {
type: Array as PropType<{ type: Array as PropType<{
to?: RouteLocationNormalized | string to?: string | RouteLocationRaw
exact?: boolean exact?: boolean
label: string label: string
icon?: string icon?: string
iconClass?: string iconClass?: string
avatar?: Partial<AvatarType> avatar?: Partial<Avatar>
click?: Function click?: Function
badge?: string badge?: string | number
}[]>, }[]>,
default: () => [] default: () => []
}, },
ui: { ui: {
@@ -86,7 +87,8 @@ export default defineComponent({
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui ui,
omit
} }
} }
}) })

View File

@@ -1,15 +1,11 @@
<template> <template>
<transition appear v-bind="ui.transition"> <transition appear v-bind="ui.transition">
<div <div :class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]" @mouseover="onMouseover" @mouseleave="onMouseleave">
:class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
>
<div :class="[ui.container, ui.rounded, ui.ring]"> <div :class="[ui.container, ui.rounded, ui.ring]">
<div class="p-4"> <div :class="ui.padding">
<div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }"> <div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }">
<UIcon v-if="icon" :name="icon" :class="ui.icon" /> <UIcon v-if="icon" :name="icon" :class="iconClass" />
<UAvatar v-if="avatar" v-bind="avatar" :class="ui.avatar" /> <UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
<div class="w-0 flex-1"> <div class="w-0 flex-1">
<p :class="ui.title"> <p :class="ui.title">
@@ -32,7 +28,7 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="timeout" :class="ui.progress" :style="progressBarStyle" /> <div v-if="timeout" :class="progressClass" :style="progressStyle" />
</div> </div>
</div> </div>
</transition> </transition>
@@ -46,9 +42,10 @@ import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import { useTimer } from '../../composables/useTimer' import { useTimer } from '../../composables/useTimer'
import type { ToastNotificationAction } from '../../types' import type { NotificationAction } from '../../types'
import type { Avatar as AvatarType } from '../../types/avatar' import type { Avatar} from '../../types/avatar'
import type { Button as ButtonType } from '../../types/button' import type { Button } from '../../types/button'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -77,14 +74,14 @@ export default defineComponent({
}, },
icon: { icon: {
type: String, type: String,
default: null default: () => appConfig.ui.notification.default.icon
}, },
avatar: { avatar: {
type: Object as PropType<Partial<AvatarType>>, type: Object as PropType<Partial<Avatar>>,
default: null default: null
}, },
close: { close: {
type: Object as PropType<Partial<ButtonType>>, type: Object as PropType<Partial<Button>>,
default: () => appConfig.ui.notification.default.close default: () => appConfig.ui.notification.default.close
}, },
timeout: { timeout: {
@@ -92,13 +89,20 @@ export default defineComponent({
default: 5000 default: 5000
}, },
actions: { actions: {
type: Array as PropType<ToastNotificationAction[]>, type: Array as PropType<NotificationAction[]>,
default: () => [] default: () => []
}, },
callback: { callback: {
type: Function, type: Function,
default: null default: null
}, },
color: {
type: String,
default: () => appConfig.ui.notification.default.color,
validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value)
}
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.notification>>, type: Object as PropType<Partial<typeof appConfig.ui.notification>>,
default: () => appConfig.ui.notification default: () => appConfig.ui.notification
@@ -114,12 +118,26 @@ export default defineComponent({
let timer: any = null let timer: any = null
const remaining = ref(props.timeout) const remaining = ref(props.timeout)
const progressBarStyle = computed(() => { const progressStyle = computed(() => {
const remainingPercent = remaining.value / props.timeout * 100 const remainingPercent = remaining.value / props.timeout * 100
return { width: `${remainingPercent || 0}%` } return { width: `${remainingPercent || 0}%` }
}) })
const progressClass = computed(() => {
return classNames(
ui.value.progress.base,
ui.value.progress.background?.replaceAll('{color}', props.color)
)
})
const iconClass = computed(() => {
return classNames(
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color?.replaceAll('{color}', props.color)
)
})
function onMouseover () { function onMouseover () {
if (timer) { if (timer) {
timer.pause() timer.pause()
@@ -144,7 +162,7 @@ export default defineComponent({
emit('close') emit('close')
} }
function onAction (action: ToastNotificationAction) { function onAction (action: NotificationAction) {
if (timer) { if (timer) {
timer.stop() timer.stop()
} }
@@ -179,7 +197,9 @@ export default defineComponent({
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
progressBarStyle, progressStyle,
progressClass,
iconClass,
onMouseover, onMouseover,
onMouseleave, onMouseleave,
onClose, onClose,

View File

@@ -17,7 +17,7 @@
import { computed, defineComponent } from 'vue' import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import type { ToastNotification } from '../../types' import type { Notification } from '../../types'
import { useToast } from '../../composables/useToast' import { useToast } from '../../composables/useToast'
import UNotification from './Notification.vue' import UNotification from './Notification.vue'
import { useState, useAppConfig } from '#imports' import { useState, useAppConfig } from '#imports'
@@ -44,7 +44,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.notifications>>(() => defu({}, props.ui, appConfig.ui.notifications)) const ui = computed<Partial<typeof appConfig.ui.notifications>>(() => defu({}, props.ui, appConfig.ui.notifications))
const toast = useToast() const toast = useToast()
const notifications = useState<ToastNotification[]>('notifications', () => []) const notifications = useState<Notification[]>('notifications', () => [])
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys

View File

@@ -1,8 +1,8 @@
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import type { ToastNotification } from '../types/toast' import type { Notification } from '../types/notification'
import { useToast } from './useToast' import { useToast } from './useToast'
export function useCopyToClipboard (options: Partial<ToastNotification> = {}) { export function useCopyToClipboard (options: Partial<Notification> = {}) {
const { copy: copyToClipboard, isSupported } = useClipboard() const { copy: copyToClipboard, isSupported } = useClipboard()
const toast = useToast() const toast = useToast()

View File

@@ -1,5 +1,6 @@
import { createSharedComposable, useActiveElement } from '@vueuse/core' import { createSharedComposable, useActiveElement } from '@vueuse/core'
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import type {} from '@vueuse/shared'
export const _useShortcuts = () => { export const _useShortcuts = () => {
const macOS = computed(() => process.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/)) const macOS = computed(() => process.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/))

View File

@@ -1,25 +1,25 @@
import type { ToastNotification } from '../types' import type { Notification } from '../types'
import { useState } from '#imports' import { useState } from '#imports'
export function useToast () { export function useToast () {
const notifications = useState<ToastNotification[]>('notifications', () => []) const notifications = useState<Notification[]>('notifications', () => [])
function add (notification: Partial<ToastNotification>) { function add (notification: Partial<Notification>) {
const body = { const body = {
id: new Date().getTime().toString(), id: new Date().getTime().toString(),
...notification ...notification
} }
const index = notifications.value.findIndex((n: ToastNotification) => n.id === body.id) const index = notifications.value.findIndex((n: Notification) => n.id === body.id)
if (index === -1) { if (index === -1) {
notifications.value.push(body as ToastNotification) notifications.value.push(body as Notification)
} }
return body return body
} }
function remove (id: string) { function remove (id: string) {
notifications.value = notifications.value.filter((n: ToastNotification) => n.id !== id) notifications.value = notifications.value.filter((n: Notification) => n.id !== id)
} }
return { return {

View File

@@ -28,7 +28,8 @@ ${Object.entries(gray || colors.cool).map(([key, value]) => `--color-gray-${key}
const headData: any = { const headData: any = {
style: [{ style: [{
innerHTML: () => root.value, innerHTML: () => root.value,
tagPriority: -2 tagPriority: -2,
id: 'nuxt-ui-colors'
}] }]
} }

View File

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

22
src/runtime/types/notification.d.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
import type { Avatar } from './avatar'
import type { Button } from './button'
import appConfig from '#build/app.config'
export interface NotificationAction extends Partial<Button> {
click: Function
}
export interface Notification {
id: string
title: string
description: string
icon?: string
avatar?: Partial<Avatar>
close?: Partial<Button>
timeout: number
actions?: NotificationAction[]
click?: Function
callback?: Function
color?: string
ui?: Partial<typeof appConfig.ui.notification>
}

View File

@@ -1,17 +0,0 @@
import type { Button } from './button'
export interface ToastNotificationAction extends Partial<Button> {
click: Function
}
export interface ToastNotification {
id: string
title: string
description: string
type: string
icon?: string
timeout: number
actions?: ToastNotificationAction[]
click?: Function
callback?: Function
}

View File

@@ -14,3 +14,18 @@ export const omit = (obj: object, keys: string[]) => {
Object.entries(obj).filter(([key]) => !keys.includes(key)) Object.entries(obj).filter(([key]) => !keys.includes(key))
) )
} }
export const getSlotsChildren = (slots: any) => {
let children = slots.default?.()
if (children.length) {
if (typeof children[0].type === 'symbol') {
// @ts-ignore-next
children = children[0].children
// @ts-ignore-next
} else if (children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
}
return children
}

9701
yarn.lock

File diff suppressed because it is too large Load Diff