mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 20:19:34 +01:00
510 lines
13 KiB
Vue
510 lines
13 KiB
Vue
<!-- eslint-disable vue/no-template-shadow -->
|
||
<!-- eslint-disable vue/no-v-html -->
|
||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||
<template>
|
||
<UCard v-if="component" class="relative flex flex-col" body-class="px-4 py-5 sm:p-6 relative" footer-class="flex flex-col flex-1 overflow-hidden">
|
||
<div class="flex justify-center">
|
||
<component :is="`U${defaultProps[params.component].component.name}`" v-if="defaultProps[params.component] && defaultProps[params.component].component" v-bind="defaultProps[params.component].component.props" />
|
||
|
||
<component :is="is" v-bind="{ ...boundProps, ...eventProps }">
|
||
<template v-for="[key, slot] of Object.entries(defaultProps[params.component]?.slots || {}) || []" #[key]>
|
||
<template v-if="Array.isArray(slot)">
|
||
<div :key="key">
|
||
<component
|
||
:is="slot.component ? `U${slot.component.name}` : slot.tag"
|
||
v-for="(slot, index) of slot"
|
||
:key="index"
|
||
:class="slot.class"
|
||
v-bind="slot.component?.props || defaultProps[slot.component]"
|
||
v-html="slot.html"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<component :is="`U${slot.component.name}`" v-if="slot.component" :key="`${key}-component`" v-bind="slot.component?.props || defaultProps[slot.component]" />
|
||
<component :is="slot.tag" v-else :key="`${key}-tag`" :class="slot.class" v-html="slot.html" />
|
||
</template>
|
||
</template>
|
||
</component>
|
||
</div>
|
||
|
||
<template v-if="props.length" #footer>
|
||
<div class="border-b u-border-gray-200">
|
||
<pre class="text-sm leading-6 u-text-gray-900 flex-1 relative flex ligatures-none overflow-x-hidden px-4 sm:px-6 py-5 sm:py-6">
|
||
<code class="flex-none min-w-full whitespace-pre-wrap break-all">{{ code }}</code>
|
||
|
||
<UButton
|
||
class="absolute top-0 right-0"
|
||
:icon="copied ? 'i-heroicons-clipboard-document-check' : 'i-heroicons-clipboard-document'"
|
||
variant="transparent"
|
||
size="sm"
|
||
:custom-class="copied ? '!text-green-500' : ''"
|
||
@click="onCopy"
|
||
/>
|
||
</pre>
|
||
</div>
|
||
|
||
<div class="flex-1 px-4 py-5 sm:p-6 space-y-3">
|
||
<UFormGroup
|
||
v-for="prop of props"
|
||
:key="prop.key"
|
||
class="capitalize"
|
||
:name="prop.key"
|
||
:label="prop.key"
|
||
>
|
||
<UToggle
|
||
v-if="prop.type === 'Boolean'"
|
||
v-model="prop.value"
|
||
:name="prop.key"
|
||
/>
|
||
<USelect
|
||
v-else-if="prop.values"
|
||
v-model="prop.value"
|
||
:name="prop.key"
|
||
placeholder="Choose one..."
|
||
:options="prop.values"
|
||
size="sm"
|
||
/>
|
||
<UInput
|
||
v-else-if="prop.type === 'String'"
|
||
v-model="prop.value"
|
||
:name="prop.key"
|
||
size="sm"
|
||
autocomplete="off"
|
||
/>
|
||
<UInput
|
||
v-else-if="prop.type === 'Number'"
|
||
v-model="prop.value"
|
||
type="number"
|
||
:name="prop.key"
|
||
size="sm"
|
||
/>
|
||
<UTextarea
|
||
v-else
|
||
:model-value="prop.value && JSON.stringify(prop.value)"
|
||
:name="prop.key"
|
||
size="sm"
|
||
:rows="8"
|
||
autoresize
|
||
@update:model-value="value => prop.value = JSON.parse(value)"
|
||
/>
|
||
</UFormGroup>
|
||
</div>
|
||
</template>
|
||
</UCard>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { useClipboard } from '@vueuse/core'
|
||
import $ui from '#build/ui'
|
||
|
||
const nuxtApp = useNuxtApp()
|
||
const { params } = useRoute()
|
||
|
||
const is = `U${params.component}`
|
||
|
||
const component = nuxtApp.vueApp.component(is)
|
||
|
||
const people = [
|
||
{ id: 1, label: 'Durward Reynolds', disabled: false },
|
||
{ id: 2, label: 'Kenton Towne', disabled: false },
|
||
{ id: 3, label: 'Therese Wunsch', disabled: false },
|
||
{ id: 4, label: 'Benedict Kessler', disabled: true },
|
||
{ id: 5, label: 'Katelyn Rohan', disabled: false }
|
||
]
|
||
|
||
const selectCustom = ref(people[0])
|
||
const commandPalette = ref(people[0])
|
||
const alertDialog = ref(false)
|
||
const toggle = ref(false)
|
||
const modal = ref(false)
|
||
const slideover = ref(false)
|
||
|
||
const x = ref(0)
|
||
const y = ref(0)
|
||
const isContextMenuOpen = ref(false)
|
||
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
|
||
|
||
onMounted(() => {
|
||
document.addEventListener('mousemove', ({ clientX, clientY }) => {
|
||
x.value = clientX
|
||
y.value = clientY
|
||
})
|
||
})
|
||
|
||
function openContextMenu () {
|
||
const top = unref(y)
|
||
const left = unref(x)
|
||
|
||
virtualElement.value.getBoundingClientRect = () => ({
|
||
width: 0,
|
||
height: 0,
|
||
top,
|
||
left
|
||
})
|
||
isContextMenuOpen.value = true
|
||
}
|
||
|
||
const defaultProps = {
|
||
Button: {
|
||
label: 'Button text'
|
||
},
|
||
Badge: {
|
||
label: 'Badge'
|
||
},
|
||
Alert: {
|
||
title: 'A new software update is available. See what’s new in version 2.0.4.'
|
||
},
|
||
AlertDialog: {
|
||
icon: 'i-heroicons-exclamation-triangle',
|
||
title: 'Deactivate account',
|
||
description: 'Are you sure you want to deactivate your account? All of your data will be permanently removed from our servers forever. This action cannot be undone.',
|
||
modelValue: alertDialog,
|
||
'onUpdate:modelValue': (v) => { alertDialog.value = v },
|
||
component: {
|
||
name: 'Button',
|
||
props: {
|
||
variant: 'secondary',
|
||
label: 'Open modal',
|
||
onClick: () => { alertDialog.value = true }
|
||
}
|
||
}
|
||
},
|
||
Avatar: {
|
||
src: 'https://picsum.photos/200/300'
|
||
},
|
||
AvatarGroup: {
|
||
group: ['https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', 'https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.25&w=256&h=256&q=80', 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80']
|
||
},
|
||
Dropdown: {
|
||
items: [
|
||
[{
|
||
label: 'Edit',
|
||
icon: 'i-heroicons-pencil-square-20-solid'
|
||
}, {
|
||
label: 'Duplicate',
|
||
icon: 'i-heroicons-document-duplicate-20-solid'
|
||
}],
|
||
[{
|
||
label: 'Archive',
|
||
icon: 'i-heroicons-archive-box-20-solid'
|
||
}, {
|
||
label: 'Move',
|
||
icon: 'i-heroicons-arrow-right-circle-20-solid'
|
||
}],
|
||
[{
|
||
label: 'Delete',
|
||
icon: 'i-heroicons-trash-20-solid'
|
||
}]
|
||
]
|
||
},
|
||
VerticalNavigation: {
|
||
links: [
|
||
{
|
||
label: 'Home',
|
||
icon: 'i-heroicons-home',
|
||
to: '/'
|
||
},
|
||
{
|
||
label: 'Examples',
|
||
icon: 'i-heroicons-book-open',
|
||
to: '/examples'
|
||
},
|
||
{
|
||
label: 'Migration',
|
||
icon: 'i-heroicons-arrow-path',
|
||
to: '/migration'
|
||
},
|
||
{
|
||
label: 'External link',
|
||
icon: 'i-heroicons-link',
|
||
to: 'https://google.fr',
|
||
target: '_blank'
|
||
}
|
||
]
|
||
},
|
||
CommandPalette: {
|
||
modelValue: commandPalette,
|
||
'onUpdate:modelValue': (v) => { commandPalette.value = v },
|
||
groups: [{
|
||
key: 'people',
|
||
label: 'People',
|
||
commands: people
|
||
}]
|
||
},
|
||
Icon: {
|
||
name: 'i-heroicons-bell'
|
||
},
|
||
Input: {
|
||
name: 'input',
|
||
placeholder: 'Enter text'
|
||
},
|
||
FormGroup: {
|
||
name: 'input',
|
||
label: 'Input group',
|
||
slots: {
|
||
default: {
|
||
component: {
|
||
name: 'Input',
|
||
props: {
|
||
name: 'input',
|
||
placeholder: 'Works with every form element'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
Toggle: {
|
||
modelValue: toggle,
|
||
'onUpdate:modelValue': (v) => { toggle.value = v }
|
||
},
|
||
Checkbox: {
|
||
name: 'checkbox'
|
||
},
|
||
Radio: {
|
||
name: 'radio'
|
||
},
|
||
Select: {
|
||
name: 'select',
|
||
options: ['English', 'Spanish', 'French', 'German', 'Chinese']
|
||
},
|
||
SelectCustom: {
|
||
modelValue: selectCustom,
|
||
'onUpdate:modelValue': (v) => { selectCustom.value = v },
|
||
textAttribute: 'label',
|
||
options: people
|
||
},
|
||
Textarea: {
|
||
name: 'textarea'
|
||
},
|
||
Tooltip: {
|
||
text: 'Tooltip text'
|
||
},
|
||
Notification: {
|
||
id: '1',
|
||
title: 'Notification title',
|
||
callback: 'console.log(\'Timer expired\')'
|
||
},
|
||
ContextMenu: {
|
||
modelValue: isContextMenuOpen,
|
||
'onUpdate:modelValue': (v) => { isContextMenuOpen.value = v },
|
||
virtualElement,
|
||
component: {
|
||
name: 'Card',
|
||
props: {
|
||
variant: 'secondary',
|
||
label: 'Open context menu',
|
||
onClick: () => { isContextMenuOpen.value = false },
|
||
onContextmenu: (e) => {
|
||
e?.preventDefault()
|
||
openContextMenu()
|
||
},
|
||
class: 'relative w-[300px] h-[100px]'
|
||
}
|
||
},
|
||
slots: {
|
||
default: {
|
||
tag: 'div',
|
||
html: 'Context menu content',
|
||
class: 'rounded border u-border-gray-200 p-2'
|
||
}
|
||
}
|
||
},
|
||
Modal: {
|
||
modelValue: modal,
|
||
'onUpdate:modelValue': (v) => { modal.value = v },
|
||
component: {
|
||
name: 'Button',
|
||
props: {
|
||
variant: 'secondary',
|
||
label: 'Open modal',
|
||
onClick: () => { modal.value = true }
|
||
}
|
||
},
|
||
slots: {
|
||
default: {
|
||
tag: 'div',
|
||
html: 'Modal content'
|
||
},
|
||
footer: {
|
||
component: {
|
||
name: 'Button',
|
||
props: {
|
||
label: 'Close',
|
||
onClick: () => { modal.value = false }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
Slideover: {
|
||
modelValue: slideover,
|
||
'onUpdate:modelValue': (v) => { slideover.value = v },
|
||
component: {
|
||
name: 'Button',
|
||
props: {
|
||
variant: 'secondary',
|
||
label: 'Open slideover',
|
||
onClick: () => { slideover.value = true }
|
||
}
|
||
},
|
||
slots: {
|
||
default: {
|
||
tag: 'div',
|
||
html: 'Slideover content'
|
||
}
|
||
}
|
||
},
|
||
Popover: {
|
||
slots: {
|
||
panel: {
|
||
tag: 'div',
|
||
class: 'u-bg-gray-100 rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 p-6',
|
||
html: 'Popover content'
|
||
}
|
||
}
|
||
},
|
||
Tabs: {
|
||
links: [{
|
||
label: 'Usage',
|
||
to: '/',
|
||
exact: true
|
||
}, {
|
||
label: 'Examples',
|
||
to: '/examples'
|
||
}, {
|
||
label: 'Migration',
|
||
to: '/migration'
|
||
}, {
|
||
label: 'Tabs',
|
||
to: '/components/Tabs'
|
||
}]
|
||
},
|
||
Pills: {
|
||
links: [{
|
||
label: 'Usage',
|
||
to: '/',
|
||
exact: true
|
||
}, {
|
||
label: 'Examples',
|
||
to: '/examples'
|
||
}, {
|
||
label: 'Migration',
|
||
to: '/migration'
|
||
}, {
|
||
label: 'Pills',
|
||
to: '/components/Pills'
|
||
}]
|
||
}
|
||
}
|
||
|
||
const componentDefaultProps = defaultProps[params.component] || {}
|
||
const { props: componentProps } = await component.__asyncLoader()
|
||
|
||
function lowercaseFirstLetter (string) {
|
||
return string.charAt(0).toLowerCase() + string.slice(1)
|
||
}
|
||
|
||
const refProps = Object.entries(componentProps).map(([key, prop]) => {
|
||
const defaultValue = componentDefaultProps[key]
|
||
const propDefault = (typeof prop.default === 'function' ? prop.default() : prop.default)
|
||
let value = defaultValue !== undefined ? defaultValue : propDefault
|
||
let type = prop.type
|
||
if (Array.isArray(type)) {
|
||
type = type[0].name
|
||
} else {
|
||
type = type.name
|
||
}
|
||
|
||
let values
|
||
if (prop.validator) {
|
||
const arrayRegex = prop.validator.toString().match(/\[.*\]/g, '')
|
||
if (arrayRegex) {
|
||
values = JSON.parse(arrayRegex[0].replace(/'/g, '"')).filter(Boolean)
|
||
} else {
|
||
const $uiProp = $ui[lowercaseFirstLetter(params.component)][key]
|
||
if ($uiProp) {
|
||
values = Object.keys($uiProp).filter(Boolean)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (value) {
|
||
if (type === 'String' && typeof value === 'string') {
|
||
value = value.replace(/^'(.*)'$/, '$1')
|
||
} else if (type === 'Array') {
|
||
value = JSON.stringify(value)
|
||
}
|
||
}
|
||
|
||
return {
|
||
key,
|
||
type,
|
||
value,
|
||
values,
|
||
default: propDefault
|
||
}
|
||
})
|
||
|
||
const eventProps = Object.entries(componentDefaultProps)
|
||
.filter(([key]) => !refProps.find(prop => prop.key === key))
|
||
.filter(([key]) => !['slots'].includes(key))
|
||
.reduce((acc, [key, value]) => {
|
||
acc[key] = value
|
||
return acc
|
||
}, {})
|
||
|
||
const props = ref(refProps)
|
||
const boundProps = computed(() => {
|
||
const bound = {}
|
||
for (const prop of props.value) {
|
||
let value = prop.value
|
||
if (value === null) {
|
||
continue
|
||
}
|
||
|
||
try {
|
||
if (prop.type === 'Array') {
|
||
value = JSON.parse(value)
|
||
} else if (prop.type === 'Number') {
|
||
value = Number(value)
|
||
} else if (prop.type === 'Function') {
|
||
// eslint-disable-next-line no-new-func
|
||
value = Function(value)
|
||
}
|
||
|
||
bound[prop.key] = value
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
return bound
|
||
})
|
||
|
||
function toKebabCase (str) {
|
||
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
||
}
|
||
|
||
const { copy, copied } = useClipboard({ copiedDuring: 2000 })
|
||
const onCopy = () => {
|
||
copy(code.value)
|
||
}
|
||
|
||
const code = computed(() => {
|
||
let code = `<U${params.component}`
|
||
for (const prop of props.value) {
|
||
if (prop.value === null) {
|
||
continue
|
||
}
|
||
if (prop.value === prop.default) {
|
||
continue
|
||
}
|
||
|
||
const key = toKebabCase(prop.key)
|
||
code += `\n ${prop.type === 'Boolean' ? ':' : ''}${key === 'model-value' ? 'v-model' : key}="${prop.value}"`
|
||
}
|
||
code += '\n/>'
|
||
return code
|
||
})
|
||
</script>
|