Files
ui/docs/app/components/content/ComponentCode.vue
2024-07-10 15:23:21 +02:00

161 lines
4.9 KiB
Vue

<script setup lang="ts">
import { upperFirst, camelCase } from 'scule'
import * as theme from '#build/ui'
const props = defineProps<{
ignore?: string[]
props?: { [key: string]: any }
slots?: { [key: string]: any }
}>()
const route = useRoute()
const { $prettier } = useNuxtApp()
const camelName = camelCase(route.params.slug[route.params.slug.length - 1])
const name = `U${upperFirst(camelName)}`
const componentProps = reactive({ ...(props.props || {}) })
const componentTheme = theme[camelName]
const meta = await fetchComponentMeta(name as any)
const options = computed(() => Object.keys(props.props || {}).filter((key) => {
return !props.ignore?.includes(key)
}).map((key) => {
const prop = meta?.meta?.props?.find((prop: any) => prop.name === key)
const variants = Object.keys(componentTheme.variants?.[key] || {})
const items = prop?.type === 'boolean'
? [{ value: true, label: 'true' }, { value: false, label: 'false' }]
: variants.map(variant => ({
value: variant,
label: variant,
chip: key === 'color' ? { color: variant } : undefined
})).filter(variant => key === 'color' ? !['error'].includes(variant.value) : true)
return {
name: key,
label: key,
items
}
}))
const code = computed(() => {
let code = `\`\`\`vue
<template>
<${name}`
for (const [key, value] of Object.entries(componentProps)) {
if (value === undefined || value === null || value === '') {
continue
}
const prop = meta?.meta?.props?.find((prop: any) => prop.name === key)
if (typeof value === 'boolean') {
if (value && prop?.default === 'true') {
continue
}
if (!value && !prop?.default) {
continue
}
code += value ? ` ${key}` : ` :${key}="false"`
} else {
const propDefault = prop && (prop.default ?? prop.tags?.find(tag => tag.name === 'defaultValue')?.text ?? componentTheme?.defaultVariants?.[prop.name])
if (propDefault === value) {
continue
}
code += ` ${key}="${value}"`
}
}
if (props.slots) {
const hasOnlyDefaultSlot = props.slots && Object.keys(props.slots).length === 1 && props.slots.default
if (hasOnlyDefaultSlot) {
code += `>${props.slots.default}</${name}>`
} else {
code += `>
${Object.entries(props.slots).map(([key, value]) => `<template #${key}>
${value}
</template>`).join('\n ')}
</${name}>`
}
} else {
code += ' />'
}
code += `\n</template>
\`\`\`
`
return code
})
const { data: ast } = await useAsyncData(`${name}-code-${JSON.stringify({ props: componentProps, slots: props.slots })}`, async () => {
let formatted = ''
try {
formatted = await $prettier.format(code.value)
} catch (e) {
formatted = code.value
}
return parseMarkdown(formatted)
}, { watch: [code] })
</script>
<template>
<div>
<div v-if="options.length" class="flex items-center gap-2.5 border border-gray-300 dark:border-gray-700 border-b-0 relative rounded-t-md px-4 py-2.5">
<template v-for="option in options" :key="option.name">
<UFormField
:label="option.label"
size="sm"
class="inline-flex ring ring-gray-300 dark:ring-gray-700 rounded"
:ui="{
wrapper: 'bg-gray-50 dark:bg-gray-800/50 rounded-l flex border-r border-gray-300 dark:border-gray-700',
label: 'text-gray-500 dark:text-gray-400 px-2 py-1.5',
container: 'mt-0'
}"
>
<USelectMenu
v-if="option.items?.length"
v-model="componentProps[option.name]"
:items="option.items"
value-key="value"
color="gray"
variant="soft"
class="rounded rounded-l-none"
:search="false"
:class="[option.name === 'color' && 'pl-6']"
:ui="{ itemLeadingChip: 'size-2' }"
>
<template v-if="option.name === 'color'" #leading="{ modelValue, ui }">
<UChip
v-if="modelValue"
inset
standalone
:color="(modelValue as any)"
:size="ui.itemLeadingChipSize()"
class="size-2"
/>
</template>
</USelectMenu>
<UInput v-else v-model="componentProps[option.name]" color="gray" variant="soft" :ui="{ base: 'rounded rounded-l-none' }" />
</UFormField>
</template>
</div>
<div class="flex border border-b-0 border-gray-300 dark:border-gray-700 relative p-4" :class="[!options.length && 'rounded-t-md']">
<component :is="name" v-bind="componentProps">
<template v-for="slot in Object.keys(slots || {})" :key="slot" #[slot]>
<ContentSlot :name="slot" unwrap="p">
{{ slots[slot] }}
</ContentSlot>
</template>
</component>
</div>
</div>
<MDCRenderer v-if="ast" :body="ast.body" :data="ast.data" class="[&>div>pre]:!rounded-t-none [&>div>pre]:!mt-0" />
</template>