Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/app/components/content/ComponentSlots.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const meta = await fetchComponentMeta(name as any)
</ProseCode>
</ProseTd>
<ProseTd>
<HighlightInlineType v-if="slot.type" :type="slot.type.replace(/ui:\s*\{[^}]*\}/g, 'ui: {}')" />
<HighlightInlineType v-if="slot.type" :type="slot.type" />

<MDC v-if="slot.description" :value="slot.description" class="text-toned mt-1" :cache-key="`${kebabCase(route.path)}-${slot.name}-description`" />
</ProseTd>
Expand Down
22 changes: 22 additions & 0 deletions docs/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { NuxtComponentMeta } from 'nuxt-component-meta'
import { createResolver } from '@nuxt/kit'
import pkg from '../package.json'

Expand Down Expand Up @@ -186,7 +187,28 @@ export default defineNuxtConfig({
}
},

hooks: {
// @ts-expect-error - Hook is not typed correctly
'component-meta:schema': (schema: NuxtComponentMeta) => {
for (const componentName in schema) {
const component = schema[componentName]
// Delete schema from slots to reduce metadata file size
if (component?.meta?.slots) {
for (const slot of component.meta.slots) {
delete (slot as any).schema
}
}
}
}
},

componentMeta: {
transformers: [(component, code) => {
// Simplify ui in slot prop types: `leading(props: { ui: Button['ui'] })` -> `leading(props: { ui: object })`
code = code.replace(/ui:[^}]+(?=\})/g, 'ui: object')

return { component, code }
}],
exclude: [
'@nuxt/content',
'@nuxt/icon',
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"maska": "^3.2.0",
"motion-v": "^1.7.2",
"nuxt": "^4.1.2",
"nuxt-component-meta": "^0.14.0",
"nuxt-component-meta": "^0.14.1",
"nuxt-llms": "^0.1.3",
"nuxt-og-image": "^5.1.11",
"prettier": "^3.6.2",
Expand Down
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions src/runtime/components/Accordion.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ export interface AccordionProps<T extends AccordionItem = AccordionItem> extends

export interface AccordionEmits extends AccordionRootEmits {}

type SlotProps<T extends AccordionItem> = (props: { item: T, index: number, open: boolean }) => any
type SlotProps<T extends AccordionItem> = (props: { item: T, index: number, open: boolean, ui: Accordion['ui'] }) => any

export type AccordionSlots<T extends AccordionItem = AccordionItem> = {
leading: SlotProps<T>
default: SlotProps<T>
default(props: { item: T, index: number, open: boolean }): any
trailing: SlotProps<T>
content: SlotProps<T>
body: SlotProps<T>
} & DynamicSlots<T, 'body', { index: number, open: boolean }>
} & DynamicSlots<T, 'body', { index: number, open: boolean, ui: Accordion['ui'] }>

</script>

Expand Down Expand Up @@ -104,24 +104,24 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.accordion ||
>
<AccordionHeader as="div" :class="ui.header({ class: [props.ui?.header, item.ui?.header] })">
<AccordionTrigger :class="ui.trigger({ class: [props.ui?.trigger, item.ui?.trigger], disabled: item.disabled })">
<slot name="leading" :item="item" :index="index" :open="open">
<slot name="leading" :item="item" :index="index" :open="open" :ui="ui">
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: [props.ui?.leadingIcon, item?.ui?.leadingIcon] })" />
</slot>

<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: [props.ui?.label, item.ui?.label] })">
<slot :item="item" :index="index" :open="open">{{ get(item, props.labelKey as string) }}</slot>
</span>

<slot name="trailing" :item="item" :index="index" :open="open">
<slot name="trailing" :item="item" :index="index" :open="open" :ui="ui">
<UIcon :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.trailingIcon({ class: [props.ui?.trailingIcon, item.ui?.trailingIcon] })" />
</slot>
</AccordionTrigger>
</AccordionHeader>

<AccordionContent v-if="item.content || !!slots.content || (item.slot && !!slots[item.slot as keyof AccordionSlots<T>]) || !!slots.body || (item.slot && !!slots[`${item.slot}-body` as keyof AccordionSlots<T>])" :class="ui.content({ class: [props.ui?.content, item.ui?.content] })">
<slot :name="((item.slot || 'content') as keyof AccordionSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index" :open="open">
<slot :name="((item.slot || 'content') as keyof AccordionSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index" :open="open" :ui="ui">
<div :class="ui.body({ class: [props.ui?.body, item.ui?.body] })">
<slot :name="((item.slot ? `${item.slot}-body`: 'body') as keyof AccordionSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index" :open="open">
<slot :name="((item.slot ? `${item.slot}-body`: 'body') as keyof AccordionSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index" :open="open" :ui="ui">
{{ item.content }}
</slot>
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/components/Alert.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ export interface AlertEmits {
}

export interface AlertSlots {
leading(props?: {}): any
leading(props: { ui: Alert['ui'] }): any
title(props?: {}): any
description(props?: {}): any
actions(props?: {}): any
close(props: { ui: { [K in keyof Required<Alert['slots']>]: (props?: Record<string, any>) => string } }): any
close(props: { ui: Alert['ui'] }): any
}
</script>

Expand Down Expand Up @@ -98,7 +98,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.alert || {})

<template>
<Primitive :as="as" :data-orientation="orientation" :class="ui.root({ class: [props.ui?.root, props.class] })">
<slot name="leading">
<slot name="leading" :ui="ui">
<UAvatar v-if="avatar" :size="((props.ui?.avatarSize || ui.avatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.avatar({ class: props.ui?.avatar })" />
<UIcon v-else-if="icon" :name="icon" :class="ui.icon({ class: props.ui?.icon })" />
</slot>
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/components/AuthForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ type DynamicFormFieldSlots<T> = Record<string, (props?: {}) => any> & Record<`${

export type AuthFormSlots<T extends object = object, F extends AuthFormField = AuthFormField> = {
header(props?: {}): any
leading(props?: {}): any
leading(props: { ui: AuthForm['ui'] }): any
title(props?: {}): any
description(props?: {}): any
providers(props?: {}): any
Expand Down Expand Up @@ -192,7 +192,7 @@ function omitFieldProps(field: F) {
<div v-if="(icon || !!slots.icon) || (title || !!slots.title) || (description || !!slots.description) || !!slots.header" :class="ui.header({ class: props.ui?.header })">
<slot name="header">
<div v-if="icon || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
<slot name="leading">
<slot name="leading" :ui="ui">
<UIcon v-if="icon" :name="icon" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
</slot>
</div>
Expand Down
12 changes: 6 additions & 6 deletions src/runtime/components/Badge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ export interface BadgeProps extends Omit<UseComponentIconsProps, 'loading' | 'lo
}

export interface BadgeSlots {
leading(props?: {}): any
default(props?: {}): any
trailing(props?: {}): any
leading(props: { ui: Badge['ui'] }): any
default(props: { ui: Badge['ui'] }): any
trailing(props: { ui: Badge['ui'] }): any
}
</script>

Expand Down Expand Up @@ -69,18 +69,18 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.badge || {})

<template>
<Primitive :as="as" :class="ui.base({ class: [props.ui?.base, props.class] })">
<slot name="leading">
<slot name="leading" :ui="ui">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
<UAvatar v-else-if="!!avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
</slot>

<slot>
<slot :ui="ui">
<span v-if="label !== undefined && label !== null" :class="ui.label({ class: props.ui?.label })">
{{ label }}
</span>
</slot>

<slot name="trailing">
<slot name="trailing" :ui="ui">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
</slot>
</Primitive>
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/components/Banner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ export interface BannerProps {
}

export interface BannerSlots {
leading(props?: {}): any
leading(props: { ui: Banner['ui'] }): any
title(props?: {}): any
actions(props?: {}): any
close(props: { ui: any }): any
close(props: { ui: Banner['ui'] }): any
}

export interface BannerEmits {
Expand Down Expand Up @@ -135,7 +135,7 @@ function onClose() {
<div :class="ui.left({ class: props.ui?.left })" />

<div :class="ui.center({ class: props.ui?.center })">
<slot name="leading">
<slot name="leading" :ui="ui">
<UIcon v-if="icon" :name="icon" :class="ui.icon({ class: props.ui?.icon })" />
</slot>

Expand Down
8 changes: 4 additions & 4 deletions src/runtime/components/BlogPost.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export interface BlogPostSlots {
badge(props?: {}): any
title(props?: {}): any
description(props?: {}): any
authors(props?: {}): any
header(props?: {}): any
authors(props: { ui: BlogPost['ui'] }): any
header(props: { ui: BlogPost['ui'] }): any
body(props?: {}): any
footer(props?: {}): any
}
Expand Down Expand Up @@ -117,7 +117,7 @@ const ariaLabel = computed(() => {
<template>
<Primitive :as="as" :data-orientation="orientation" :class="ui.root({ class: [props.ui?.root, props.class] })" @click="onClick">
<div v-if="image || !!slots.header" :class="ui.header({ class: props.ui?.header })">
<slot name="header">
<slot name="header" :ui="ui">
<component
:is="ImageComponent"
v-bind="typeof image === 'string' ? { src: image, alt: title } : { alt: title, ...image }"
Expand Down Expand Up @@ -164,7 +164,7 @@ const ariaLabel = computed(() => {
</div>

<div v-if="authors?.length || !!slots.authors" :class="ui.authors({ class: props.ui?.authors })">
<slot name="authors">
<slot name="authors" :ui="ui">
<template v-if="authors?.length">
<UAvatarGroup v-if="authors.length > 1">
<ULink
Expand Down
30 changes: 23 additions & 7 deletions src/runtime/components/BlogPosts.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/blog-posts'
import type { BlogPostProps } from '../types'
import type { BlogPostProps, BlogPostSlots } from '../types'
import type { ComponentConfig } from '../types/tv'

type BlogPosts = ComponentConfig<typeof theme, AppConfig, 'blogPosts'>

export interface BlogPostsProps {
export interface BlogPostsProps<T extends BlogPostProps = BlogPostProps> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
posts?: BlogPostProps[]
posts?: T[]
/**
* The orientation of the blog posts.
* @defaultValue 'horizontal'
Expand All @@ -21,22 +22,33 @@ export interface BlogPostsProps {
class?: any
}

export interface BlogPostsSlots {
type ExtendSlotWithPost<T extends BlogPostProps, K extends keyof BlogPostSlots>
= BlogPostSlots[K] extends (props: infer P) => any
? (props: P & { post: T }) => any
: BlogPostSlots[K]

export type BlogPostsSlots<T extends BlogPostProps = BlogPostProps> = {
[K in keyof BlogPostSlots]: ExtendSlotWithPost<T, K>
} & {
default(props?: {}): any
}

</script>

<script setup lang="ts">
<script setup lang="ts" generic="T extends BlogPostProps">
import { computed } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { omit } from '../utils'
import { tv } from '../utils/tv'
import UBlogPost from './BlogPost.vue'

const props = withDefaults(defineProps<BlogPostsProps>(), {
orientation: 'horizontal'
})
defineSlots<BlogPostsSlots>()
const slots = defineSlots<BlogPostsSlots<T>>()

const getProxySlots = () => omit(slots, ['default'])

const appConfig = useAppConfig() as BlogPosts['AppConfig']

Expand All @@ -51,7 +63,11 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.blogPosts ||
:key="index"
:orientation="orientation === 'vertical' ? 'horizontal' : 'vertical'"
v-bind="post"
/>
>
<template v-for="(_, name) in getProxySlots()" #[name]="slotData">
<slot :name="name" v-bind="(slotData as any)" :post="post" />
</template>
</UBlogPost>
</slot>
</Primitive>
</template>
22 changes: 12 additions & 10 deletions src/runtime/components/Breadcrumb.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,17 @@ export interface BreadcrumbProps<T extends BreadcrumbItem = BreadcrumbItem> {
ui?: Breadcrumb['slots']
}

type SlotProps<T extends BreadcrumbItem> = (props: { item: T, index: number, active?: boolean }) => any
type SlotProps<T extends BreadcrumbItem> = (props: { item: T, index: number, active?: boolean, ui: Breadcrumb['ui'] }) => any

export type BreadcrumbSlots<T extends BreadcrumbItem = BreadcrumbItem> = {
'item': SlotProps<T>
'item-leading': SlotProps<T>
'item-label': SlotProps<T>
'item-trailing': SlotProps<T>
'separator': any
} & DynamicSlots<T, 'leading' | 'label' | 'trailing', { index: number, active?: boolean }>
'item-label': (props: { item: T, index: number, active?: boolean }) => any
'item-trailing': (props: { item: T, index: number, active?: boolean }) => any
'separator': (props: { ui: Breadcrumb['ui'] }) => any
}
& DynamicSlots<T, 'leading', { index: number, active?: boolean, ui: Breadcrumb['ui'] }>
& DynamicSlots<T, 'label' | 'trailing', { index: number, active?: boolean }>

</script>

Expand Down Expand Up @@ -90,26 +92,26 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.breadcrumb |
<li :class="ui.item({ class: [props.ui?.item, item.ui?.item] })">
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
<ULinkBase v-bind="slotProps" as="span" :aria-current="active && (index === items!.length - 1) ? 'page' : undefined" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: index === items!.length - 1, disabled: !!item.disabled, to: !!item.to })">
<slot :name="((item.slot || 'item') as keyof BreadcrumbSlots<T>)" :item="item" :index="index">
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index">
<slot :name="((item.slot || 'item') as keyof BreadcrumbSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :active="index === items!.length - 1" :index="index" :ui="ui">
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof BreadcrumbSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :active="index === items!.length - 1" :index="index" :ui="ui">
<UIcon v-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon], active: index === items!.length - 1 })" />
<UAvatar v-else-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: [props.ui?.linkLeadingAvatar, item.ui?.linkLeadingAvatar], active: index === items!.length - 1 })" />
</slot>

<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>]" :class="ui.linkLabel({ class: [props.ui?.linkLabel, item.ui?.linkLabel] })">
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index">
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof DynamicSlots<T, 'label'>)" :item="(item as Extract<T, { slot: string; }>)" :active="index === items!.length - 1" :index="index">
{{ get(item, props.labelKey as string) }}
</slot>
</span>

<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index" />
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof DynamicSlots<T, 'trailing'>)" :item="(item as Extract<T, { slot: string; }>)" :active="index === items!.length - 1" :index="index" />
</slot>
</ULinkBase>
</ULink>
</li>

<li v-if="index < items!.length - 1" role="presentation" aria-hidden="true" :class="ui.separator({ class: [props.ui?.separator, item.ui?.separator] })">
<slot name="separator">
<slot name="separator" :ui="ui">
<UIcon :name="separatorIcon" :class="ui.separatorIcon({ class: [props.ui?.separatorIcon, item.ui?.separatorIcon] })" />
</slot>
</li>
Expand Down
Loading
Loading