feat: migrate Dialog to shadcn-vue compound API
Also: fixes DialogContent/DialogFooter's close/action buttons to use our actual Button variant set (secondary) instead of the CLI's default ghost/outline variants, which don't exist on our re-themed Button. Adds an eslint override so shadcn-vue's single-word ui/ component names (Button.vue, Dialog.vue, ...) aren't flagged by vue/multi-word-component-names. This was the last PrimeVue-wrapped component, so lib/index.ts no longer imports anything from primevue or uses the ptThemeable helper. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
4755f9ab6e
commit
7bb554aeb0
18 changed files with 451 additions and 135 deletions
|
|
@ -22,6 +22,16 @@ export default defineConfigWithVueTs(
|
|||
...pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
|
||||
{
|
||||
// shadcn-vue's generated ui/ components use single-word names by convention
|
||||
// (Button.vue, Input.vue, ...) so their file names match the CLI's own output.
|
||||
name: 'app/shadcn-vue-component-names',
|
||||
files: ['**/components/ui/**/*.vue'],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
...pluginVitest.configs.recommended,
|
||||
files: ['src/**/__tests__/*'],
|
||||
|
|
|
|||
|
|
@ -21,3 +21,15 @@ export {
|
|||
SelectLabel,
|
||||
SelectSeparator,
|
||||
} from './ui/select'
|
||||
export {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
DialogOverlay,
|
||||
DialogScrollContent,
|
||||
} from './ui/dialog'
|
||||
|
|
|
|||
19
packages/library/src/components/ui/dialog/Dialog.vue
Normal file
19
packages/library/src/components/ui/dialog/Dialog.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
|
||||
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="dialog"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</DialogRoot>
|
||||
</template>
|
||||
15
packages/library/src/components/ui/dialog/DialogClose.vue
Normal file
15
packages/library/src/components/ui/dialog/DialogClose.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import type { DialogCloseProps } from "reka-ui"
|
||||
import { DialogClose } from "reka-ui"
|
||||
|
||||
const props = defineProps<DialogCloseProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose
|
||||
data-slot="dialog-close"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
||||
53
packages/library/src/components/ui/dialog/DialogContent.vue
Normal file
53
packages/library/src/components/ui/dialog/DialogContent.vue
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<script setup lang="ts">
|
||||
import { XIcon } from '@lucide/vue';
|
||||
|
||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from '@/components/ui/button'
|
||||
import DialogOverlay from "./DialogOverlay.vue"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<DialogContentProps & { class?: HTMLAttributes["class"], showCloseButton?: boolean }>(), {
|
||||
showCloseButton: true,
|
||||
})
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogContent
|
||||
data-slot="dialog-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="cn('bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none', props.class)"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
v-if="showCloseButton"
|
||||
data-slot="dialog-close"
|
||||
as-child
|
||||
>
|
||||
<Button variant="secondary" class="absolute top-2 right-2 size-7 bg-transparent hover:bg-black/10" size="icon">
|
||||
<XIcon class="size-3.5" />
|
||||
<span class="sr-only">Close</span>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import type { DialogDescriptionProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DialogDescription, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogDescription
|
||||
data-slot="dialog-description"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogDescription>
|
||||
</template>
|
||||
27
packages/library/src/components/ui/dialog/DialogFooter.vue
Normal file
27
packages/library/src/components/ui/dialog/DialogFooter.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { DialogClose } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
showCloseButton?: boolean
|
||||
}>(), {
|
||||
showCloseButton: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
:class="cn('bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)"
|
||||
>
|
||||
<slot />
|
||||
<DialogClose v-if="showCloseButton" as-child>
|
||||
<Button variant="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</template>
|
||||
17
packages/library/src/components/ui/dialog/DialogHeader.vue
Normal file
17
packages/library/src/components/ui/dialog/DialogHeader.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
:class="cn('gap-2 flex flex-col', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
21
packages/library/src/components/ui/dialog/DialogOverlay.vue
Normal file
21
packages/library/src/components/ui/dialog/DialogOverlay.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import type { DialogOverlayProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DialogOverlay } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogOverlay
|
||||
data-slot="dialog-overlay"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogOverlay>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<script setup lang="ts">
|
||||
import { XIcon } from '@lucide/vue';
|
||||
|
||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
>
|
||||
<DialogContent
|
||||
:class="
|
||||
cn(
|
||||
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
@pointer-down-outside="(event) => {
|
||||
const originalEvent = event.detail.originalEvent;
|
||||
const target = originalEvent.target as HTMLElement;
|
||||
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
|
||||
>
|
||||
<XIcon class="w-4 h-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
23
packages/library/src/components/ui/dialog/DialogTitle.vue
Normal file
23
packages/library/src/components/ui/dialog/DialogTitle.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import type { DialogTitleProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DialogTitle, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTitle
|
||||
data-slot="dialog-title"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-base leading-none font-medium cn-font-heading', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogTitle>
|
||||
</template>
|
||||
15
packages/library/src/components/ui/dialog/DialogTrigger.vue
Normal file
15
packages/library/src/components/ui/dialog/DialogTrigger.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import type { DialogTriggerProps } from "reka-ui"
|
||||
import { DialogTrigger } from "reka-ui"
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTrigger
|
||||
data-slot="dialog-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
</template>
|
||||
10
packages/library/src/components/ui/dialog/index.ts
Normal file
10
packages/library/src/components/ui/dialog/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export { default as Dialog } from "./Dialog.vue"
|
||||
export { default as DialogClose } from "./DialogClose.vue"
|
||||
export { default as DialogContent } from "./DialogContent.vue"
|
||||
export { default as DialogDescription } from "./DialogDescription.vue"
|
||||
export { default as DialogFooter } from "./DialogFooter.vue"
|
||||
export { default as DialogHeader } from "./DialogHeader.vue"
|
||||
export { default as DialogOverlay } from "./DialogOverlay.vue"
|
||||
export { default as DialogScrollContent } from "./DialogScrollContent.vue"
|
||||
export { default as DialogTitle } from "./DialogTitle.vue"
|
||||
export { default as DialogTrigger } from "./DialogTrigger.vue"
|
||||
|
|
@ -33,6 +33,15 @@ export {
|
|||
CardFooter,
|
||||
CardAction,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
DialogOverlay,
|
||||
DialogScrollContent,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import type { App } from 'vue'
|
||||
import {
|
||||
Dialog as PrimeDialog,
|
||||
} from 'primevue'
|
||||
import { ThemeLibrary, type Themeable, ThemeEngine, type LibComponent, type Key } from '../engine'
|
||||
import { ThemeLibrary, type Themeable, ThemeEngine } from '../engine'
|
||||
import {
|
||||
NychLoadingIcon,
|
||||
Button,
|
||||
|
|
@ -36,6 +33,16 @@ import {
|
|||
SelectGroup,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
DialogOverlay,
|
||||
DialogScrollContent,
|
||||
} from '../components'
|
||||
|
||||
export {
|
||||
|
|
@ -70,42 +77,18 @@ export {
|
|||
SelectGroup,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
DialogOverlay,
|
||||
DialogScrollContent,
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Themeable that themes a PrimeVue component through its passthrough (`pt`) API.
|
||||
*
|
||||
* The engine injects the base class (`nych-<base>`) onto `pt.root`. This factory layers a
|
||||
* `nych-<base>-<section>` class onto every additional section so multi-part components
|
||||
* (Checkbox box, Select overlay, Card body, …) can be fully styled in unstyled mode, where
|
||||
* PrimeVue emits no classes of its own.
|
||||
*/
|
||||
const ptThemeable = <T extends LibComponent>(
|
||||
component: T,
|
||||
base: string,
|
||||
sections: string[] = [],
|
||||
): Themeable<T> => ({
|
||||
component,
|
||||
unstyled: true as Themeable<T>['unstyled'],
|
||||
classes: [`nych-${base}`],
|
||||
injectionKeys: { class: 'pt' as Key<T> },
|
||||
propMutator: ((props: Record<string, unknown>) => {
|
||||
const pt = (props.pt as Record<string, { class?: string }>) ?? {}
|
||||
|
||||
for (const section of sections) {
|
||||
const existing = pt[section]?.class
|
||||
pt[section] = {
|
||||
...(pt[section] ?? {}),
|
||||
class: [existing, `nych-${base}-${section}`].filter(Boolean).join(' '),
|
||||
}
|
||||
}
|
||||
|
||||
props.pt = pt
|
||||
|
||||
return props
|
||||
}) as Themeable<T>['propMutator'],
|
||||
})
|
||||
|
||||
const loadingIconThemeable: Themeable<typeof NychLoadingIcon> = {
|
||||
component: NychLoadingIcon,
|
||||
unstyled: false,
|
||||
|
|
@ -114,21 +97,12 @@ const loadingIconThemeable: Themeable<typeof NychLoadingIcon> = {
|
|||
const nychthemeron: ThemeLibrary = {
|
||||
components: {
|
||||
LoadingIcon: loadingIconThemeable,
|
||||
Dialog: ptThemeable(PrimeDialog, 'dialog', [
|
||||
'mask',
|
||||
'header',
|
||||
'title',
|
||||
'headerActions',
|
||||
'content',
|
||||
'footer',
|
||||
]),
|
||||
},
|
||||
}
|
||||
|
||||
const _engine = new ThemeEngine(nychthemeron)
|
||||
|
||||
export const LoadingIcon = _engine.getComponent<typeof NychLoadingIcon>('LoadingIcon')
|
||||
export const Dialog: typeof PrimeDialog = _engine.getComponent<typeof PrimeDialog>('Dialog')
|
||||
|
||||
export const createNychthemeron = (): {
|
||||
Button: typeof Button
|
||||
|
|
@ -155,7 +129,16 @@ export const createNychthemeron = (): {
|
|||
CardContent: typeof CardContent
|
||||
CardFooter: typeof CardFooter
|
||||
CardAction: typeof CardAction
|
||||
Dialog: typeof PrimeDialog
|
||||
Dialog: typeof Dialog
|
||||
DialogTrigger: typeof DialogTrigger
|
||||
DialogContent: typeof DialogContent
|
||||
DialogHeader: typeof DialogHeader
|
||||
DialogTitle: typeof DialogTitle
|
||||
DialogDescription: typeof DialogDescription
|
||||
DialogFooter: typeof DialogFooter
|
||||
DialogClose: typeof DialogClose
|
||||
DialogOverlay: typeof DialogOverlay
|
||||
DialogScrollContent: typeof DialogScrollContent
|
||||
Alert: typeof Alert
|
||||
AlertTitle: typeof AlertTitle
|
||||
AlertDescription: typeof AlertDescription
|
||||
|
|
@ -188,6 +171,15 @@ export const createNychthemeron = (): {
|
|||
CardFooter,
|
||||
CardAction,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
DialogOverlay,
|
||||
DialogScrollContent,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
|
|
@ -219,6 +211,15 @@ export const createNychthemeron = (): {
|
|||
app.component('NychCardFooter', CardFooter)
|
||||
app.component('NychCardAction', CardAction)
|
||||
app.component('NychDialog', Dialog)
|
||||
app.component('NychDialogTrigger', DialogTrigger)
|
||||
app.component('NychDialogContent', DialogContent)
|
||||
app.component('NychDialogHeader', DialogHeader)
|
||||
app.component('NychDialogTitle', DialogTitle)
|
||||
app.component('NychDialogDescription', DialogDescription)
|
||||
app.component('NychDialogFooter', DialogFooter)
|
||||
app.component('NychDialogClose', DialogClose)
|
||||
app.component('NychDialogOverlay', DialogOverlay)
|
||||
app.component('NychDialogScrollContent', DialogScrollContent)
|
||||
app.component('NychAlert', Alert)
|
||||
app.component('NychAlertTitle', AlertTitle)
|
||||
app.component('NychAlertDescription', AlertDescription)
|
||||
|
|
|
|||
35
packages/library/tests/Dialog.spec.ts
Normal file
35
packages/library/tests/Dialog.spec.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { h, nextTick } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { Dialog, DialogContent, DialogTitle } from '../src/lib'
|
||||
|
||||
describe('Dialog', () => {
|
||||
it('does not render content when closed', async () => {
|
||||
mount(Dialog, {
|
||||
props: { open: false },
|
||||
slots: {
|
||||
default: () => h(DialogContent, undefined, {
|
||||
default: () => h(DialogTitle, undefined, { default: () => 'Oracle of Delphi' }),
|
||||
}),
|
||||
},
|
||||
attachTo: document.body,
|
||||
})
|
||||
await nextTick()
|
||||
expect(document.body.textContent).not.toContain('Oracle of Delphi')
|
||||
})
|
||||
|
||||
it('renders content in a teleported portal when open', async () => {
|
||||
mount(Dialog, {
|
||||
props: { open: true },
|
||||
slots: {
|
||||
default: () => h(DialogContent, undefined, {
|
||||
default: () => h(DialogTitle, undefined, { default: () => 'Oracle of Delphi' }),
|
||||
}),
|
||||
},
|
||||
attachTo: document.body,
|
||||
})
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
expect(document.body.textContent).toContain('Oracle of Delphi')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,35 +1,6 @@
|
|||
import { describe, it, expect, vi } from 'vitest'
|
||||
import type { App } from 'vue'
|
||||
import {
|
||||
createNychthemeron,
|
||||
LoadingIcon,
|
||||
Dialog,
|
||||
} from '../src/index'
|
||||
|
||||
//eslint-disable-next-line
|
||||
const callSetup = (component: any, props: Record<string, unknown>) => {
|
||||
const mergedProps: Record<string, unknown> = {}
|
||||
|
||||
if (component.props) {
|
||||
for (const [key, propDef] of Object.entries(component.props)) {
|
||||
if (typeof propDef === 'object' && propDef !== null && 'default' in propDef) {
|
||||
mergedProps[key] = (propDef as { default: unknown }).default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(mergedProps, props)
|
||||
|
||||
if (component.setup) {
|
||||
const result = component.setup(mergedProps, { slots: {}, attrs: {} })
|
||||
if (typeof result === 'function') {
|
||||
const vnode = result()
|
||||
return (vnode?.props as Record<string, unknown>) || {}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return mergedProps
|
||||
}
|
||||
import { createNychthemeron, LoadingIcon } from '../src/index'
|
||||
|
||||
describe('createNychthemeron', () => {
|
||||
it('returns a Vue plugin with install method', () => {
|
||||
|
|
@ -57,26 +28,3 @@ describe('LoadingIcon', () => {
|
|||
expect(LoadingIcon).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('themed pt components', () => {
|
||||
const cases = [
|
||||
{ name: 'Dialog', component: Dialog, base: 'nych-dialog' },
|
||||
]
|
||||
|
||||
it.each(cases)('$name is a valid Vue component', ({ component }) => {
|
||||
expect(component).toBeDefined()
|
||||
expect(typeof (component as Record<string, unknown>).setup).toBe('function')
|
||||
})
|
||||
|
||||
it.each(cases)('$name injects its base class onto pt.root', ({ component, base }) => {
|
||||
const result = callSetup(component, { pt: {} })
|
||||
expect(result.pt?.root?.class).toContain(base)
|
||||
})
|
||||
|
||||
it.each(cases)('$name does not leak engine internal props', ({ component }) => {
|
||||
const result = callSetup(component, { pt: {} })
|
||||
expect(result).not.toHaveProperty('injectedClasses')
|
||||
expect(result).not.toHaveProperty('injectedStyles')
|
||||
expect(result).not.toHaveProperty('stylePlug')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { Dialog as NychDialog, Button as NychButton } from '@nychthemeron/library'
|
||||
import {
|
||||
Dialog as NychDialog,
|
||||
DialogTrigger as NychDialogTrigger,
|
||||
DialogContent as NychDialogContent,
|
||||
DialogHeader as NychDialogHeader,
|
||||
DialogTitle as NychDialogTitle,
|
||||
DialogFooter as NychDialogFooter,
|
||||
Button as NychButton,
|
||||
} from '@nychthemeron/library'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Dialog',
|
||||
|
|
@ -17,47 +25,52 @@ type Story = StoryObj<typeof meta>
|
|||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { NychDialog, NychButton },
|
||||
setup() {
|
||||
const visible = ref(false)
|
||||
return { visible }
|
||||
},
|
||||
components: { NychDialog, NychDialogTrigger, NychDialogContent, NychDialogHeader, NychDialogTitle, NychDialogFooter, NychButton },
|
||||
template: `
|
||||
<div>
|
||||
<NychButton label="Summon the Oracle" @click="visible = true" />
|
||||
<NychDialog v-model:visible="visible" modal header="Oracle of Delphi" style="width: 28rem;">
|
||||
<p style="margin: 0;">
|
||||
<NychDialog>
|
||||
<NychDialogTrigger as-child>
|
||||
<NychButton>Summon the Oracle</NychButton>
|
||||
</NychDialogTrigger>
|
||||
<NychDialogContent style="width: 28rem;">
|
||||
<NychDialogHeader>
|
||||
<NychDialogTitle>Oracle of Delphi</NychDialogTitle>
|
||||
</NychDialogHeader>
|
||||
<p>
|
||||
Seekers travelled from across the Aegean to hear the Pythia speak the
|
||||
will of Apollo in riddling verse. Approach, and ask your question.
|
||||
</p>
|
||||
<template #footer>
|
||||
<NychButton label="Withdraw" severity="secondary" @click="visible = false" />
|
||||
<NychButton label="Ask" @click="visible = false" />
|
||||
</template>
|
||||
<NychDialogFooter>
|
||||
<NychButton variant="secondary">Withdraw</NychButton>
|
||||
<NychButton>Ask</NychButton>
|
||||
</NychDialogFooter>
|
||||
</NychDialogContent>
|
||||
</NychDialog>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// Rendered open by default so the dialog surface is visible in docs/snapshots.
|
||||
export const Open: Story = {
|
||||
render: () => ({
|
||||
components: { NychDialog, NychButton },
|
||||
components: { NychDialog, NychDialogContent, NychDialogHeader, NychDialogTitle, NychDialogFooter, NychButton },
|
||||
setup() {
|
||||
const visible = ref(true)
|
||||
return { visible }
|
||||
const open = ref(true)
|
||||
return { open }
|
||||
},
|
||||
template: `
|
||||
<NychDialog v-model:visible="visible" modal header="Passage of the Styx" style="width: 28rem;">
|
||||
<p style="margin: 0;">
|
||||
<NychDialog v-model:open="open">
|
||||
<NychDialogContent style="width: 28rem;">
|
||||
<NychDialogHeader>
|
||||
<NychDialogTitle>Passage of the Styx</NychDialogTitle>
|
||||
</NychDialogHeader>
|
||||
<p>
|
||||
Charon ferries the souls of the dead across the river that divides the
|
||||
world of the living from the world of the dead. One obol is required.
|
||||
</p>
|
||||
<template #footer>
|
||||
<NychButton label="Turn back" severity="secondary" @click="visible = false" />
|
||||
<NychButton label="Pay the toll" @click="visible = false" />
|
||||
</template>
|
||||
<NychDialogFooter>
|
||||
<NychButton variant="secondary" @click="open = false">Turn back</NychButton>
|
||||
<NychButton @click="open = false">Pay the toll</NychButton>
|
||||
</NychDialogFooter>
|
||||
</NychDialogContent>
|
||||
</NychDialog>
|
||||
`,
|
||||
}),
|
||||
|
|
@ -65,14 +78,19 @@ export const Open: Story = {
|
|||
|
||||
export const WithoutFooter: Story = {
|
||||
render: () => ({
|
||||
components: { NychDialog },
|
||||
components: { NychDialog, NychDialogContent, NychDialogHeader, NychDialogTitle },
|
||||
setup() {
|
||||
const visible = ref(true)
|
||||
return { visible }
|
||||
const open = ref(true)
|
||||
return { open }
|
||||
},
|
||||
template: `
|
||||
<NychDialog v-model:visible="visible" modal header="An Omen" style="width: 26rem;">
|
||||
<p style="margin: 0;">A crow has settled upon the western gate. Interpret it as you will.</p>
|
||||
<NychDialog v-model:open="open">
|
||||
<NychDialogContent style="width: 26rem;">
|
||||
<NychDialogHeader>
|
||||
<NychDialogTitle>An Omen</NychDialogTitle>
|
||||
</NychDialogHeader>
|
||||
<p>A crow has settled upon the western gate. Interpret it as you will.</p>
|
||||
</NychDialogContent>
|
||||
</NychDialog>
|
||||
`,
|
||||
}),
|
||||
|
|
|
|||
Loading…
Reference in a new issue