@falcondev-oss/nuxt-layers-base 0.40.3 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/app/app.tsx +14 -0
  2. package/app/assets/css/base.css +6 -0
  3. package/app/components/layout/LayoutAuth.tsx +22 -0
  4. package/app/components/layout/LayoutNavbar.tsx +193 -0
  5. package/app/components/layout/LayoutPage.tsx +183 -0
  6. package/app/components/layout/LayoutSidebar.tsx +167 -0
  7. package/app/components/modals/ConfirmModal.tsx +80 -0
  8. package/app/components/modals/EntityCrudModal.tsx +124 -0
  9. package/app/components/overlay/modal/ModalActions.tsx +36 -0
  10. package/app/components/overlay/modal/ModalConfirm.tsx +46 -0
  11. package/app/components/overlay/modal/ModalRadioGroup.tsx +62 -0
  12. package/app/components/u/UActions.tsx +30 -0
  13. package/app/components/u/UAuthForm.tsx +30 -0
  14. package/app/components/u/UCustomApp.tsx +33 -0
  15. package/app/components/u/UField.tsx +211 -0
  16. package/app/components/u/UForm.tsx +133 -0
  17. package/app/components/u/UImageWithFallback.tsx +47 -0
  18. package/app/components/u/UInputDatePicker.tsx +94 -0
  19. package/app/components/u/UInputDateRangePicker.tsx +143 -0
  20. package/app/components/u/UInputDurationMinutes.tsx +106 -0
  21. package/app/components/u/UInputFile.tsx +132 -0
  22. package/app/components/u/URibbonCard.tsx +54 -0
  23. package/app/components/u/URibbonSection.tsx +36 -0
  24. package/app/components/u/USwitchCard.tsx +64 -0
  25. package/app/components/util/Define.tsx +21 -0
  26. package/app/components/util/SlotRef.tsx +21 -0
  27. package/app/components/util/Sync.tsx +37 -0
  28. package/app/components/util/TemplateText.tsx +31 -0
  29. package/app/composables/confirm.ts +1 -1
  30. package/app/error.tsx +24 -0
  31. package/app/utils/define-setup-component.ts +39 -5
  32. package/nuxt.config.ts +8 -0
  33. package/package.json +4 -3
  34. package/app/app.vue +0 -3
  35. package/app/components/layout/LayoutAuth.vue +0 -13
  36. package/app/components/layout/LayoutNavbar.vue +0 -140
  37. package/app/components/layout/LayoutPage.vue +0 -174
  38. package/app/components/layout/LayoutSidebar.vue +0 -117
  39. package/app/components/modals/ConfirmModal.vue +0 -65
  40. package/app/components/modals/EntityCrudModal.vue +0 -109
  41. package/app/components/overlay/modal/ModalActions.vue +0 -25
  42. package/app/components/overlay/modal/ModalConfirm.vue +0 -25
  43. package/app/components/overlay/modal/ModalRadioGroup.vue +0 -37
  44. package/app/components/u/UActions.vue +0 -23
  45. package/app/components/u/UAuthForm.vue +0 -36
  46. package/app/components/u/UCustomApp.vue +0 -23
  47. package/app/components/u/UField.vue +0 -191
  48. package/app/components/u/UForm.vue +0 -96
  49. package/app/components/u/UImageWithFallback.vue +0 -35
  50. package/app/components/u/UInputDatePicker.vue +0 -79
  51. package/app/components/u/UInputDateRangePicker.vue +0 -100
  52. package/app/components/u/UInputDurationMinutes.vue +0 -90
  53. package/app/components/u/UInputFile.vue +0 -127
  54. package/app/components/u/URibbonCard.vue +0 -43
  55. package/app/components/u/URibbonSection.vue +0 -29
  56. package/app/components/u/USwitchCard.vue +0 -51
  57. package/app/components/util/Define.vue +0 -13
  58. package/app/components/util/SlotRef.vue +0 -17
  59. package/app/components/util/Sync.vue +0 -24
  60. package/app/components/util/TemplateText.vue +0 -20
  61. package/app/error.vue +0 -15
package/app/app.tsx ADDED
@@ -0,0 +1,14 @@
1
+ import { UCustomApp } from '#components'
2
+
3
+ function render() {
4
+ return <UCustomApp />
5
+ }
6
+
7
+ export default defineSetupComponent((_: object) =>
8
+ options(_, {
9
+ name: 'App',
10
+ props: [],
11
+ emits: [],
12
+ setup: () => render,
13
+ }),
14
+ )
@@ -3,6 +3,7 @@
3
3
 
4
4
  @import './theme.css';
5
5
  @import './utilities.css';
6
+ @import './variants.css';
6
7
 
7
8
  @layer base {
8
9
  html,
@@ -33,3 +34,8 @@
33
34
  flex-shrink: 0;
34
35
  }
35
36
  }
37
+
38
+ .u-field [aria-invalid='true']::placeholder {
39
+ color: var(--ui-color-error-500);
40
+ opacity: 0.5;
41
+ }
@@ -0,0 +1,22 @@
1
+ import type { VNode } from 'vue'
2
+ import { UPageCard } from '#components'
3
+
4
+ export default defineSetupComponent(
5
+ (_: {
6
+ slots: {
7
+ default: () => VNode[]
8
+ }
9
+ }) =>
10
+ options(_, {
11
+ name: 'LayoutAuth',
12
+ props: [],
13
+ emits: [],
14
+ setup:
15
+ (_props, { slots }) =>
16
+ () => (
17
+ <div class="flex min-h-full flex-col items-center justify-center gap-4 p-4">
18
+ <UPageCard class="w-full max-w-md">{slots.default?.()}</UPageCard>
19
+ </div>
20
+ ),
21
+ }),
22
+ )
@@ -0,0 +1,193 @@
1
+ import type {
2
+ BreadcrumbItem,
3
+ BreadcrumbProps,
4
+ DashboardNavbarProps,
5
+ DashboardNavbarSlots,
6
+ DashboardPanelProps,
7
+ DashboardToolbarProps,
8
+ NavigationMenuItem,
9
+ NavigationMenuProps,
10
+ } from '@nuxt/ui'
11
+ import type { MaybeRefOrGetter, Ref, VNode } from 'vue'
12
+ import type { ToolbarTool } from '../../composables/useToolbar'
13
+ import type { AddPropertyPrefix } from '../../types/helpers'
14
+ import * as R from 'remeda'
15
+ import {
16
+ ForwardSlots,
17
+ UBreadcrumb,
18
+ UDashboardNavbar,
19
+ UDashboardPanel,
20
+ UDashboardSidebarCollapse,
21
+ UDashboardToolbar,
22
+ UNavigationMenu,
23
+ } from '#components'
24
+ import { toolbarToolsKey } from '../../composables/useToolbar'
25
+ import { mergeSlotClass } from '../../utils/ui'
26
+
27
+ export default defineSetupComponent(
28
+ (_: {
29
+ props: {
30
+ panel?: DashboardPanelProps
31
+ navbar?: {
32
+ sidebarToggle?: boolean
33
+ title?: string
34
+ breadcrumb?: BreadcrumbItem[]
35
+ breadcrumbUi?: BreadcrumbProps<BreadcrumbItem>['ui']
36
+ ui?: DashboardNavbarProps['ui']
37
+ }
38
+ tabs?: {
39
+ items?: NavigationMenuItem[]
40
+ ui?: NavigationMenuProps['ui']
41
+ }
42
+ tools?: {
43
+ items?: NavigationMenuItem[]
44
+ ui?: NavigationMenuProps['ui']
45
+ }
46
+ toolbarUi?: DashboardToolbarProps['ui']
47
+ }
48
+ slots: {
49
+ 'default': () => VNode[]
50
+ 'navbar-title'?: () => VNode[]
51
+ 'navbar-trailing'?: () => VNode[]
52
+ 'navbar-actions'?: () => VNode[]
53
+ } & AddPropertyPrefix<DashboardNavbarSlots, 'navbar'>
54
+ }) =>
55
+ options(_, {
56
+ name: 'LayoutNavbar',
57
+ props: ['panel', 'navbar', 'tabs', 'tools', 'toolbarUi'],
58
+ emits: [],
59
+ setup: (props, { slots }) => {
60
+ // rendered manually in the `left` slot below
61
+ const omitNavbarSlots = [
62
+ 'navbar-left',
63
+ 'navbar-leading',
64
+ 'navbar-title',
65
+ 'navbar-trailing',
66
+ 'navbar-right',
67
+ 'navbar-actions',
68
+ ] satisfies (keyof typeof slots)[]
69
+ const navbarSlots = computed(() =>
70
+ R.pipe(
71
+ slots,
72
+ R.pickBy((_slot, key) => key.startsWith('navbar-')),
73
+ R.omit(omitNavbarSlots),
74
+ R.mapKeys((key) => key.replace('navbar-', '')),
75
+ ),
76
+ )
77
+
78
+ // added by the content rendered inside this navbar through `useToolbar()`
79
+ // `ref()`'s deep unwrapping blows up on the recursive `NavigationMenuItem` type
80
+ const providedTools = ref([]) as Ref<MaybeRefOrGetter<ToolbarTool>[]>
81
+ provide(toolbarToolsKey, providedTools)
82
+
83
+ const toolItems = computed(() => [
84
+ ...(props.tools?.items ?? []),
85
+ ...providedTools.value.map((tool) => toValue(tool)),
86
+ ])
87
+
88
+ const navbarUi = computed<DashboardNavbarProps['ui']>(() => ({
89
+ ...props.navbar?.ui,
90
+ toggle: mergeSlotClass(props.navbar?.ui?.toggle, '-ml-1'),
91
+ ...(props.navbar?.breadcrumb && {
92
+ root: mergeSlotClass(props.navbar.ui?.root, 'h-auto min-h-(--ui-header-height) py-2'),
93
+ }),
94
+ }))
95
+
96
+ return () => (
97
+ <UDashboardPanel
98
+ {...props.panel}
99
+ v-slots={vSlots(UDashboardPanel, {
100
+ header: () => [
101
+ ...(props.navbar
102
+ ? [
103
+ <ForwardSlots slots={navbarSlots.value}>
104
+ <UDashboardNavbar
105
+ ui={navbarUi.value}
106
+ class="bg-white"
107
+ title={props.navbar.title}
108
+ v-slots={vSlots(UDashboardNavbar, {
109
+ left: () => [
110
+ ...(props.navbar?.sidebarToggle
111
+ ? [<UDashboardSidebarCollapse class="-ml-1" />]
112
+ : []),
113
+
114
+ <div class="flex min-w-0 flex-col items-start gap-0.5">
115
+ <div class="flex min-w-0 items-center gap-1.5">
116
+ <h1 class="text-highlighted truncate font-semibold">
117
+ {slots['navbar-title']?.() ?? props.navbar?.title}
118
+ </h1>
119
+ {slots['navbar-trailing']?.()}
120
+ </div>
121
+
122
+ {props.navbar?.breadcrumb ? (
123
+ <UBreadcrumb
124
+ items={props.navbar.breadcrumb}
125
+ ui={{
126
+ ...props.navbar.breadcrumbUi,
127
+ link: mergeSlotClass(
128
+ props.navbar.breadcrumbUi?.link,
129
+ 'text-xs',
130
+ ),
131
+ separatorIcon: mergeSlotClass(
132
+ props.navbar.breadcrumbUi?.separatorIcon,
133
+ 'size-3.5',
134
+ ),
135
+ }}
136
+ />
137
+ ) : null}
138
+ </div>,
139
+ ],
140
+ right: (slotProps) => [
141
+ <>{slots['navbar-right']?.(slotProps)}</>,
142
+ <div id="navbar-actions" class="flex items-center gap-2">
143
+ {slots['navbar-actions']?.()}
144
+ </div>,
145
+ ],
146
+ })}
147
+ />
148
+ </ForwardSlots>,
149
+ ]
150
+ : []),
151
+ ...(props.tabs || toolItems.value.length > 0
152
+ ? [
153
+ <UDashboardToolbar
154
+ ui={props.toolbarUi}
155
+ class="bg-white *:first:-ml-2"
156
+ v-slots={vSlots(UDashboardToolbar, {
157
+ left: () => [
158
+ props.tabs ? (
159
+ <UNavigationMenu
160
+ items={props.tabs.items}
161
+ highlight
162
+ variant="link"
163
+ ui={props.tabs.ui}
164
+ />
165
+ ) : (
166
+ <UNavigationMenu
167
+ items={toolItems.value}
168
+ highlight
169
+ ui={props.tools?.ui}
170
+ />
171
+ ),
172
+ ],
173
+ ...(props.tabs && {
174
+ right: () => [
175
+ <UNavigationMenu
176
+ items={toolItems.value}
177
+ highlight
178
+ ui={props.tools?.ui}
179
+ />,
180
+ ],
181
+ }),
182
+ })}
183
+ />,
184
+ ]
185
+ : []),
186
+ ],
187
+ body: () => slots.default?.() ?? [],
188
+ })}
189
+ />
190
+ )
191
+ },
192
+ }),
193
+ )
@@ -0,0 +1,183 @@
1
+ import type {
2
+ ButtonProps,
3
+ FooterProps,
4
+ FooterSlots,
5
+ HeaderProps,
6
+ HeaderSlots,
7
+ NavigationMenuItem,
8
+ NavigationMenuProps,
9
+ } from '@nuxt/ui'
10
+ import type { VNode } from 'vue'
11
+ import type { AddPropertyPrefix } from '../../types/helpers'
12
+ import { mapKeys, omit, pickBy, pipe } from 'remeda'
13
+ import { UActions, UFooter, UHeader, UMain, UNavigationMenu, USeparator } from '#components'
14
+
15
+ const actionDefaults = {
16
+ variant: 'subtle',
17
+ } satisfies Partial<ButtonProps>
18
+
19
+ export default defineSetupComponent(
20
+ (_: {
21
+ props: {
22
+ header?: {
23
+ logo?: {
24
+ src?: string
25
+ iconSrc?: string
26
+ }
27
+ navigation?: NavigationMenuProps
28
+ actions?: ButtonProps[]
29
+ ui?: HeaderProps['ui']
30
+ mobileActions?: ButtonProps[]
31
+ }
32
+ footer?: {
33
+ items?: NavigationMenuItem[]
34
+ actions?: ButtonProps[]
35
+ ui?: FooterProps['ui']
36
+ }
37
+ }
38
+ slots: {
39
+ default: () => VNode[]
40
+ } & AddPropertyPrefix<HeaderSlots, 'header'> &
41
+ AddPropertyPrefix<FooterSlots, 'footer'>
42
+ }) =>
43
+ options(_, {
44
+ name: 'LayoutPage',
45
+ props: ['header', 'footer'],
46
+ emits: [],
47
+ setup: (props, { slots }) => {
48
+ const omitHeaderSlots = [
49
+ 'header-title',
50
+ 'header-right',
51
+ 'header-default',
52
+ 'header-body',
53
+ ] satisfies (keyof typeof slots)[]
54
+
55
+ const headerSlots = computed(() =>
56
+ pipe(
57
+ slots,
58
+ pickBy((_slot, key) => key.startsWith('header-')),
59
+ omit(omitHeaderSlots),
60
+ mapKeys((key) => key.replace('header-', '')),
61
+ ),
62
+ )
63
+
64
+ const omitFooterSlots = [
65
+ 'footer-left',
66
+ 'footer-right',
67
+ 'footer-default',
68
+ ] satisfies (keyof typeof slots)[]
69
+
70
+ const footerSlots = computed(() =>
71
+ pipe(
72
+ slots,
73
+ pickBy((_slot, key) => key.startsWith('footer-')),
74
+ omit(omitFooterSlots),
75
+ mapKeys((key) => key.replace('footer-', '')),
76
+ ),
77
+ )
78
+
79
+ return () => (
80
+ <div>
81
+ {props.header ? (
82
+ <UHeader
83
+ ui={props.header.ui}
84
+ v-slots={vSlots(UHeader, {
85
+ ...((props.header.logo || slots['header-title']) && {
86
+ title: () =>
87
+ slots['header-title']?.() ??
88
+ (props.header?.logo
89
+ ? [
90
+ ...(props.header.logo.src
91
+ ? [<img class="h-5 w-auto shrink-0" src={props.header.logo.src} />]
92
+ : []),
93
+ ...(!props.header.logo.src && props.header.logo.iconSrc
94
+ ? [<img class="size-5" src={props.header.logo.iconSrc} />]
95
+ : []),
96
+ ]
97
+ : []),
98
+ }),
99
+
100
+ default: () =>
101
+ props.header?.navigation
102
+ ? [<UNavigationMenu {...props.header.navigation} />]
103
+ : [],
104
+
105
+ ...((props.header.actions || slots['header-right']) && {
106
+ right: () =>
107
+ slots['header-right']?.() ?? [
108
+ ...(props.header?.mobileActions
109
+ ? [
110
+ <UActions
111
+ class="md:hidden"
112
+ actions={props.header.mobileActions}
113
+ defaults={actionDefaults}
114
+ />,
115
+ ]
116
+ : []),
117
+ ...(props.header?.actions
118
+ ? [
119
+ <UActions
120
+ class="max-sm:hidden"
121
+ actions={props.header.actions}
122
+ defaults={actionDefaults}
123
+ />,
124
+ ]
125
+ : []),
126
+ ],
127
+ }),
128
+
129
+ body: () =>
130
+ slots['header-body']?.() ?? [
131
+ <div class="flex flex-col gap-4">
132
+ {props.header?.navigation ? (
133
+ <UNavigationMenu {...props.header.navigation} orientation="vertical" />
134
+ ) : null}
135
+ {props.header?.navigation && props.header.actions ? <USeparator /> : null}
136
+ {props.header?.actions ? (
137
+ <UActions
138
+ class="flex-col"
139
+ actions={props.header.actions}
140
+ defaults={actionDefaults}
141
+ />
142
+ ) : null}
143
+ </div>,
144
+ ],
145
+
146
+ ...headerSlots.value,
147
+ })}
148
+ />
149
+ ) : null}
150
+ <UMain>{slots.default?.()}</UMain>
151
+ {props.footer ? (
152
+ <UFooter
153
+ ui={props.footer.ui}
154
+ v-slots={vSlots(UFooter, {
155
+ left: () =>
156
+ slots['footer-left']?.() ?? [
157
+ <p class="text-muted text-sm">Copyright © {new Date().getFullYear()}</p>,
158
+ ],
159
+
160
+ default: () =>
161
+ props.footer?.items
162
+ ? [<UNavigationMenu items={props.footer.items} variant="link" />]
163
+ : [],
164
+
165
+ ...((props.footer.actions || slots['footer-right']) && {
166
+ right: () =>
167
+ slots['footer-right']?.() ?? [
168
+ <UActions
169
+ actions={props.footer?.actions}
170
+ defaults={{ variant: 'ghost' }}
171
+ />,
172
+ ],
173
+ }),
174
+
175
+ ...footerSlots.value,
176
+ })}
177
+ />
178
+ ) : null}
179
+ </div>
180
+ )
181
+ },
182
+ }),
183
+ )
@@ -0,0 +1,167 @@
1
+ import type {
2
+ ArrayOrNested,
3
+ AvatarProps,
4
+ DashboardSearchProps,
5
+ DashboardSidebarProps,
6
+ DropdownMenuItem,
7
+ NavigationMenuItem,
8
+ NavigationMenuProps,
9
+ } from '@nuxt/ui'
10
+ import type { VNode } from 'vue'
11
+ import {
12
+ UButton,
13
+ UDashboardGroup,
14
+ UDashboardSearch,
15
+ UDashboardSearchButton,
16
+ UDashboardSidebar,
17
+ UDropdownMenu,
18
+ UNavigationMenu,
19
+ } from '#components'
20
+ import { mergeSlotClass } from '../../utils/ui'
21
+
22
+ export default defineSetupComponent(
23
+ (_: {
24
+ props: {
25
+ sidebar?: DashboardSidebarProps
26
+ logo?: {
27
+ src?: string
28
+ iconSrc?: string
29
+ }
30
+ search?: DashboardSearchProps
31
+ items?: NavigationMenuItem[]
32
+ bottomItems?: NavigationMenuItem[]
33
+ userMenu?: {
34
+ name: string
35
+ avatar?: AvatarProps
36
+ items: ArrayOrNested<DropdownMenuItem>
37
+ }
38
+ itemsUi?: NavigationMenuProps['ui']
39
+ bottomItemsUi?: NavigationMenuProps['ui']
40
+ }
41
+ slots: {
42
+ default: () => VNode[]
43
+ logo?: () => VNode[]
44
+ icon?: () => VNode[]
45
+ }
46
+ }) =>
47
+ options(_, {
48
+ name: 'LayoutSidebar',
49
+ props: [
50
+ 'sidebar',
51
+ 'logo',
52
+ 'search',
53
+ 'items',
54
+ 'bottomItems',
55
+ 'userMenu',
56
+ 'itemsUi',
57
+ 'bottomItemsUi',
58
+ ],
59
+ emits: [],
60
+ setup: (props, { slots }) => {
61
+ const config = useRuntimeConfig()
62
+
63
+ return () => (
64
+ <UDashboardGroup
65
+ storage="local"
66
+ storageKey={`${config.public.projectId}-dashboard`}
67
+ unit="rem"
68
+ >
69
+ <UDashboardSidebar
70
+ {...props.sidebar}
71
+ class="bg-white"
72
+ mode="drawer"
73
+ collapsible
74
+ ui={{
75
+ ...props.sidebar?.ui,
76
+ header: mergeSlotClass(props.sidebar?.ui?.header, 'border-b border-default'),
77
+ footer: mergeSlotClass(props.sidebar?.ui?.footer, 'border-t border-default'),
78
+ }}
79
+ v-slots={vSlots(UDashboardSidebar, {
80
+ ...((props.logo?.src || props.logo?.iconSrc || slots.logo || slots.icon) && {
81
+ header: ({ collapsed }) => [
82
+ ...(collapsed
83
+ ? []
84
+ : (slots.logo?.() ??
85
+ (props.logo?.src
86
+ ? [<img class="h-5 w-auto shrink-0" src={props.logo.src} />]
87
+ : []))),
88
+ ...(collapsed || (!props.logo?.src && !slots.logo)
89
+ ? [
90
+ <div class={{ 'mx-auto': collapsed }}>
91
+ {slots.icon?.() ??
92
+ (props.logo?.iconSrc ? (
93
+ <img class="size-5" src={props.logo.iconSrc} />
94
+ ) : null)}
95
+ </div>,
96
+ ]
97
+ : []),
98
+ ],
99
+ }),
100
+ default: ({ collapsed }) => [
101
+ ...(props.search
102
+ ? [
103
+ // height and negative margins line the border up with the `LayoutNavbar` toolbar's, cancelling the sidebar body's padding
104
+ <div class="border-default -mx-4 -mt-2 -mb-2 flex h-[calc(--spacing(12)+1px)] shrink-0 items-center border-b px-4 sm:max-lg:-mx-6 sm:max-lg:px-6">
105
+ <UDashboardSearchButton
106
+ collapsed={collapsed}
107
+ variant="outline"
108
+ tooltip
109
+ block
110
+ />
111
+ </div>,
112
+ ]
113
+ : []),
114
+ ...(props.items
115
+ ? [
116
+ <UNavigationMenu
117
+ collapsed={collapsed}
118
+ items={props.items}
119
+ orientation="vertical"
120
+ ui={props.itemsUi}
121
+ />,
122
+ ]
123
+ : []),
124
+ ...(props.bottomItems
125
+ ? [
126
+ <UNavigationMenu
127
+ collapsed={collapsed}
128
+ items={props.bottomItems}
129
+ orientation="vertical"
130
+ class="mt-auto"
131
+ ui={props.bottomItemsUi}
132
+ />,
133
+ ]
134
+ : []),
135
+ ],
136
+ ...(props.userMenu && {
137
+ footer: ({ collapsed }) => [
138
+ <UDropdownMenu items={props.userMenu!.items}>
139
+ <UButton
140
+ label={collapsed ? undefined : props.userMenu!.name}
141
+ trailingIcon={collapsed ? undefined : 'ph:caret-up-down'}
142
+ icon={props.userMenu!.avatar ? undefined : 'lucide:user-round'}
143
+ avatar={props.userMenu!.avatar}
144
+ color="neutral"
145
+ variant="ghost"
146
+ block
147
+ square={collapsed}
148
+ class="data-[state=open]:bg-elevated"
149
+ ui={{
150
+ leadingIcon: 'text-accented',
151
+ trailingIcon: 'text-dimmed',
152
+ }}
153
+ />
154
+ </UDropdownMenu>,
155
+ ],
156
+ }),
157
+ })}
158
+ />
159
+
160
+ {props.search ? <UDashboardSearch colorMode={false} {...props.search} /> : null}
161
+
162
+ {slots.default?.()}
163
+ </UDashboardGroup>
164
+ )
165
+ },
166
+ }),
167
+ )
@@ -0,0 +1,80 @@
1
+ import type { ButtonProps, RadioGroupItem, RadioGroupProps } from '@nuxt/ui'
2
+ import type { ComponentPublicInstance } from 'vue'
3
+ import type { ConfirmModalResult } from '../../composables/confirm'
4
+ import type { templateParts } from '../../utils/display'
5
+ import { SlotRef, TemplateText, UButton, UModal, URadioGroup } from '#components'
6
+
7
+ export type ConfirmModalProps<O extends RadioGroupItem[]> = {
8
+ title: string
9
+ confirmLabel: string
10
+ confirmDisabled?: () => boolean
11
+ text?: string | ReturnType<typeof templateParts>
12
+ slotRef?: ComponentPublicInstance
13
+ options?: O
14
+ color?: ButtonProps['color']
15
+ }
16
+
17
+ export default defineSetupComponent(
18
+ <const O extends RadioGroupItem[], R extends ConfirmModalResult<O>>(_: {
19
+ props: ConfirmModalProps<O>
20
+ emits: {
21
+ close: (result: R) => void
22
+ }
23
+ }) =>
24
+ options(_, {
25
+ name: 'ConfirmModal',
26
+ props: ['title', 'confirmLabel', 'confirmDisabled', 'text', 'slotRef', 'options', 'color'],
27
+ emits: ['close'],
28
+ setup: (props, { emit }) => {
29
+ const selectedOption = ref<RadioGroupProps<O>['modelValue']>()
30
+
31
+ return () => (
32
+ <UModal
33
+ title={props.title}
34
+ close={{ onClick: () => emit('close', false as R) }}
35
+ dismissible={false}
36
+ ui={{ header: 'border-b-0!', body: 'pt-0!' }}
37
+ v-slots={vSlots(UModal, {
38
+ body: () => [
39
+ <div class="flex flex-col gap-4">
40
+ {props.text && typeof props.text === 'string' ? <p>{props.text}</p> : null}
41
+ {typeof props.text === 'object' ? <TemplateText text={props.text} /> : null}
42
+
43
+ <SlotRef slotRef={props.slotRef} />
44
+
45
+ <URadioGroup
46
+ modelValue={selectedOption.value}
47
+ onUpdate:modelValue={(value) => {
48
+ selectedOption.value = value
49
+ }}
50
+ items={props.options}
51
+ variant="table"
52
+ color={props.color ?? 'error'}
53
+ />
54
+ </div>,
55
+ ],
56
+ footer: () => [
57
+ <div class="flex w-full justify-end gap-2">
58
+ <UButton
59
+ color="neutral"
60
+ variant="soft"
61
+ label="Abbrechen"
62
+ onClick={() => emit('close', false as R)}
63
+ />
64
+ <UButton
65
+ color={props.color ?? 'error'}
66
+ label={props.confirmLabel}
67
+ disabled={
68
+ (!!props.options?.length && selectedOption.value === undefined) ||
69
+ props.confirmDisabled?.()
70
+ }
71
+ onClick={() => emit('close', (selectedOption.value ?? true) as R)}
72
+ />
73
+ </div>,
74
+ ],
75
+ })}
76
+ />
77
+ )
78
+ },
79
+ }),
80
+ )