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

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 (62) 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/u/UToggleButton.tsx +46 -0
  26. package/app/components/util/Define.tsx +21 -0
  27. package/app/components/util/SlotRef.tsx +21 -0
  28. package/app/components/util/Sync.tsx +37 -0
  29. package/app/components/util/TemplateText.tsx +31 -0
  30. package/app/composables/confirm.ts +1 -1
  31. package/app/error.tsx +24 -0
  32. package/app/utils/define-setup-component.ts +39 -5
  33. package/nuxt.config.ts +8 -0
  34. package/package.json +4 -3
  35. package/app/app.vue +0 -3
  36. package/app/components/layout/LayoutAuth.vue +0 -13
  37. package/app/components/layout/LayoutNavbar.vue +0 -140
  38. package/app/components/layout/LayoutPage.vue +0 -174
  39. package/app/components/layout/LayoutSidebar.vue +0 -117
  40. package/app/components/modals/ConfirmModal.vue +0 -65
  41. package/app/components/modals/EntityCrudModal.vue +0 -109
  42. package/app/components/overlay/modal/ModalActions.vue +0 -25
  43. package/app/components/overlay/modal/ModalConfirm.vue +0 -25
  44. package/app/components/overlay/modal/ModalRadioGroup.vue +0 -37
  45. package/app/components/u/UActions.vue +0 -23
  46. package/app/components/u/UAuthForm.vue +0 -36
  47. package/app/components/u/UCustomApp.vue +0 -23
  48. package/app/components/u/UField.vue +0 -191
  49. package/app/components/u/UForm.vue +0 -96
  50. package/app/components/u/UImageWithFallback.vue +0 -35
  51. package/app/components/u/UInputDatePicker.vue +0 -79
  52. package/app/components/u/UInputDateRangePicker.vue +0 -100
  53. package/app/components/u/UInputDurationMinutes.vue +0 -90
  54. package/app/components/u/UInputFile.vue +0 -127
  55. package/app/components/u/URibbonCard.vue +0 -43
  56. package/app/components/u/URibbonSection.vue +0 -29
  57. package/app/components/u/USwitchCard.vue +0 -51
  58. package/app/components/util/Define.vue +0 -13
  59. package/app/components/util/SlotRef.vue +0 -17
  60. package/app/components/util/Sync.vue +0 -24
  61. package/app/components/util/TemplateText.vue +0 -20
  62. package/app/error.vue +0 -15
@@ -0,0 +1,54 @@
1
+ import type { CardProps, CardSlots } from '@nuxt/ui'
2
+ import type { VNode } from 'vue'
3
+ import { useForwardProps } from 'reka-ui'
4
+ import { omit } from 'remeda'
5
+ import { UCard } from '#components'
6
+ import { mergeSlotClass } from '../../utils/ui'
7
+
8
+ export default defineSetupComponent(
9
+ (_: {
10
+ props: CardProps
11
+ slots: CardSlots & {
12
+ /** Ribbon strip above the body — fill it with `URibbonSection`s. */
13
+ ribbon?: () => VNode[]
14
+ }
15
+ }) =>
16
+ options(_, {
17
+ name: 'URibbonCard',
18
+ props: ['as', 'title', 'description', 'variant', 'class', 'ui'],
19
+ emits: [],
20
+ inheritAttrs: false,
21
+ setup: (props, { slots, attrs }) => {
22
+ const forwarded = useForwardProps(props)
23
+
24
+ return () => (
25
+ <div class="flex flex-col">
26
+ {slots.ribbon ? (
27
+ // the ribbon's lower edge runs behind the card, so the card keeps its own rounded top
28
+ <div
29
+ class={[
30
+ 'divide-default ring-default bg-elevated flex items-stretch divide-x overflow-x-auto rounded-t-lg shadow-[inset_0_2px_3px_-2px_rgb(0_0_0/0.06),inset_2px_0_3px_-2px_rgb(0_0_0/0.06),inset_-2px_0_3px_-2px_rgb(0_0_0/0.06)] ring',
31
+ slots.default ? '-mb-2 pb-2' : 'rounded-b-lg',
32
+ ]}
33
+ >
34
+ {slots.ribbon()}
35
+ </div>
36
+ ) : null}
37
+
38
+ {slots.default ? (
39
+ <UCard
40
+ {...attrs}
41
+ {...forwarded.value}
42
+ ui={{
43
+ ...forwarded.value.ui,
44
+ body: mergeSlotClass(forwarded.value.ui?.body, 'p-0!'),
45
+ }}
46
+ class="relative shadow-[0_-2px_3px_-1px_rgb(0_0_0/0.08)]"
47
+ v-slots={vSlots(UCard, omit(slots, ['ribbon']))}
48
+ />
49
+ ) : null}
50
+ </div>
51
+ )
52
+ },
53
+ }),
54
+ )
@@ -0,0 +1,36 @@
1
+ import type { VNode } from 'vue'
2
+
3
+ export default defineSetupComponent(
4
+ (_: {
5
+ props: {
6
+ title?: string
7
+ end?: boolean
8
+ }
9
+ slots: {
10
+ default: () => VNode[]
11
+ }
12
+ }) =>
13
+ options(_, {
14
+ name: 'URibbonSection',
15
+ inheritAttrs: false,
16
+ props: ['title', 'end'],
17
+ emits: [],
18
+ setup:
19
+ (props, { slots, attrs }) =>
20
+ () => (
21
+ <div
22
+ data-end={props.end || undefined}
23
+ class={[
24
+ 'flex shrink-0 flex-col gap-1.5 px-3 py-2',
25
+ props.end &&
26
+ 'border-default [&:not([data-end]~*)]:ml-auto [&:not([data-end]~*)]:border-l',
27
+ ]}
28
+ >
29
+ {props.title ? <p class="text-dimmed text-[10px] leading-none">{props.title}</p> : null}
30
+ <div class="flex flex-1 flex-wrap items-center gap-1" {...attrs}>
31
+ {slots.default?.()}
32
+ </div>
33
+ </div>
34
+ ),
35
+ }),
36
+ )
@@ -0,0 +1,64 @@
1
+ import type { VNode } from 'vue'
2
+ import { UCard, UIcon, USwitch } from '#components'
3
+
4
+ export default defineSetupComponent(
5
+ (_: {
6
+ props: {
7
+ title: string
8
+ modelValue: boolean
9
+ icon?: string
10
+ loading?: boolean
11
+ alwaysExpanded?: boolean
12
+ }
13
+ emits: {
14
+ 'toggle': (value: boolean, controller: AbortController) => void
15
+ 'update:modelValue': (value: boolean) => void
16
+ }
17
+ slots: {
18
+ default: () => VNode[]
19
+ }
20
+ }) =>
21
+ options(_, {
22
+ name: 'USwitchCard',
23
+ props: ['title', 'modelValue', 'icon', 'loading', 'alwaysExpanded'],
24
+ emits: ['toggle', 'update:modelValue'],
25
+ setup: (props, { emit, slots }) => {
26
+ const expanded = computed(() => props.alwaysExpanded || props.modelValue)
27
+
28
+ function onToggle(value: boolean) {
29
+ const controller = new AbortController()
30
+ emit('toggle', value, controller)
31
+ if (controller.signal.aborted) return
32
+
33
+ emit('update:modelValue', value)
34
+ }
35
+
36
+ return () => (
37
+ <UCard
38
+ class="m-px"
39
+ variant="subtle"
40
+ ui={{
41
+ header: `text-sm font-semibold py-2 px-3! ${expanded.value ? '' : 'border-b-0'}`,
42
+ body: `p-3! ${expanded.value ? '' : 'hidden'}`,
43
+ }}
44
+ v-slots={vSlots(UCard, {
45
+ header: () => [
46
+ <div class="flex items-center justify-between gap-8">
47
+ <span class="flex items-center gap-2">
48
+ {props.icon ? <UIcon name={props.icon} class="text-muted size-5" /> : null}
49
+ {props.title}
50
+ </span>
51
+ <USwitch
52
+ modelValue={props.modelValue}
53
+ loading={props.loading}
54
+ onUpdate:modelValue={(value) => onToggle(value)}
55
+ />
56
+ </div>,
57
+ ],
58
+ default: () => slots.default?.() ?? [],
59
+ })}
60
+ />
61
+ )
62
+ },
63
+ }),
64
+ )
@@ -0,0 +1,46 @@
1
+ import { Icon, UButton } from '#components'
2
+
3
+ export default defineSetupComponent(
4
+ (_: {
5
+ props: {
6
+ modelValue: boolean
7
+ label: string
8
+ icon: string
9
+ }
10
+ emits: { 'update:modelValue': (value: boolean) => void }
11
+ }) =>
12
+ options(_, {
13
+ name: 'UToggleButton',
14
+ props: ['modelValue', 'label', 'icon'],
15
+ emits: ['update:modelValue'],
16
+ setup:
17
+ (props, { emit }) =>
18
+ () => (
19
+ <UButton
20
+ color="neutral"
21
+ variant="soft"
22
+ class={['ring-accented px-1.5! ring', props.modelValue && 'bg-accented']}
23
+ aria-pressed={props.modelValue}
24
+ onClick={() => {
25
+ emit('update:modelValue', !props.modelValue)
26
+ }}
27
+ >
28
+ {{
29
+ leading: () => (
30
+ <span
31
+ class={[
32
+ 'flex size-5 items-center justify-center rounded-sm transition-colors',
33
+ props.modelValue
34
+ ? 'bg-primary text-inverted'
35
+ : 'ring-accented text-muted ring inset-ring-0',
36
+ ]}
37
+ >
38
+ <Icon name={props.icon} class="size-4" />
39
+ </span>
40
+ ),
41
+ default: () => props.label,
42
+ }}
43
+ </UButton>
44
+ ),
45
+ }),
46
+ )
@@ -0,0 +1,21 @@
1
+ import type { VNode } from 'vue'
2
+
3
+ export default defineSetupComponent(
4
+ <T extends object>(_: {
5
+ props: {
6
+ value: T
7
+ }
8
+ slots: {
9
+ default: (props: T) => VNode[]
10
+ }
11
+ }) =>
12
+ options(_, {
13
+ name: 'Define',
14
+ props: ['value'],
15
+ emits: [],
16
+ setup:
17
+ (props, { slots }) =>
18
+ () =>
19
+ slots.default?.(props.value),
20
+ }),
21
+ )
@@ -0,0 +1,21 @@
1
+ import type { ComponentPublicInstance, VNode } from 'vue'
2
+
3
+ export default defineSetupComponent(
4
+ (_: {
5
+ props: {
6
+ slotRef?: ComponentPublicInstance
7
+ }
8
+ slots: {
9
+ default: () => VNode[]
10
+ }
11
+ }) =>
12
+ options(_, {
13
+ name: 'SlotRef',
14
+ props: ['slotRef'],
15
+ emits: [],
16
+ setup: (props) => () => {
17
+ const slot = props.slotRef?.$slots.default
18
+ return slot ? h(slot) : null
19
+ },
20
+ }),
21
+ )
@@ -0,0 +1,37 @@
1
+ function renderNothing() {
2
+ return null
3
+ }
4
+
5
+ export default defineSetupComponent(
6
+ <T,>(_: {
7
+ props: {
8
+ input: T
9
+ output: T
10
+ deep?: boolean
11
+ once?: boolean
12
+ }
13
+ emits: {
14
+ 'update:output': (value: T) => void
15
+ }
16
+ }) =>
17
+ options(_, {
18
+ name: 'Sync',
19
+ props: ['input', 'output', 'deep', 'once'],
20
+ emits: ['update:output'],
21
+ setup: (props, { emit }) => {
22
+ watch(
23
+ () => props.input,
24
+ () => {
25
+ emit('update:output', props.input)
26
+ },
27
+ {
28
+ immediate: true,
29
+ deep: props.deep,
30
+ once: props.once,
31
+ },
32
+ )
33
+
34
+ return renderNothing
35
+ },
36
+ }),
37
+ )
@@ -0,0 +1,31 @@
1
+ import type { templateParts } from '../../utils/display'
2
+ import { UBadge } from '#components'
3
+
4
+ export default defineSetupComponent(
5
+ (_: {
6
+ props: {
7
+ text: ReturnType<typeof templateParts>
8
+ }
9
+ }) =>
10
+ options(_, {
11
+ name: 'TemplateText',
12
+ props: ['text'],
13
+ emits: [],
14
+ setup: (props) => () => (
15
+ <p>
16
+ {props.text.strings.map((string, index) => (
17
+ <span key={index}>
18
+ <span>{string}</span>
19
+ {props.text.values[index] ? (
20
+ <strong class="font-semibold">
21
+ <UBadge variant="soft" color="neutral" size="lg" class="h-6! p-1! font-semibold">
22
+ {props.text.values[index]}
23
+ </UBadge>
24
+ </strong>
25
+ ) : null}
26
+ </span>
27
+ ))}
28
+ </p>
29
+ ),
30
+ }),
31
+ )
@@ -1,5 +1,5 @@
1
1
  import type { RadioGroupItem } from '@nuxt/ui'
2
- import type { ConfirmModalProps } from '../components/modals/ConfirmModal.vue'
2
+ import type { ConfirmModalProps } from '../components/modals/ConfirmModal'
3
3
  import { LazyConfirmModal } from '#components'
4
4
 
5
5
  export type ConfirmModalSuccess<O extends RadioGroupItem[]> = 0 extends O['length']
package/app/error.tsx ADDED
@@ -0,0 +1,24 @@
1
+ import type { NuxtError } from '#app'
2
+ import { UApp, UError } from '#components'
3
+
4
+ export default defineSetupComponent(
5
+ (_: {
6
+ props: {
7
+ error: NuxtError
8
+ }
9
+ }) =>
10
+ options(_, {
11
+ name: 'Error',
12
+ props: ['error'],
13
+ emits: [],
14
+ setup: (props) => {
15
+ console.error(props.error)
16
+
17
+ return () => (
18
+ <UApp>
19
+ <UError error={props.error} />
20
+ </UApp>
21
+ )
22
+ },
23
+ }),
24
+ )
@@ -15,11 +15,14 @@ import { defineComponent } from 'vue'
15
15
 
16
16
  declare module 'vue' {
17
17
  interface ComponentCustomProps {
18
- // Deliberately loose: this only makes `vSlots` an accepted prop name on every
18
+ // `v-slots` is what `@vue/babel-plugin-jsx` turns into slot children, so the name has
19
+ // to be spelled that way to reach the runtime at all.
20
+ //
21
+ // Deliberately loose: this only makes `v-slots` an accepted prop name on every
19
22
  // component. `Slots` would reject it for any component whose slots are declared as an
20
23
  // interface without an index signature (most of @nuxt/ui). The real check is the
21
24
  // `vSlots()` helper below, which types the object against the target's own slots.
22
- vSlots?: Record<string, any>
25
+ 'v-slots'?: Record<string, any>
23
26
  }
24
27
  }
25
28
 
@@ -29,7 +32,7 @@ interface ComponentTypes {
29
32
  slots?: Record<string, any>
30
33
  /**
31
34
  * Prop names to register when they are deliberately fewer than `keyof props`; the rest
32
- * arrive as `attrs`. See `docs/agents/vue-to-tsx-migration.md` for when that is right.
35
+ * arrive as `attrs`.
33
36
  */
34
37
  propKeys?: string
35
38
  /**
@@ -167,7 +170,7 @@ export function defineSetupComponent<const T extends ComponentTypes>(
167
170
  ComponentOptionsMixin,
168
171
  Field<T, 'emits'>,
169
172
  // `Partial`, to match `SlotsType<Partial<...>>` below and the partial type `vSlots()`
170
- PublicProps & { vSlots?: Partial<Field<T, 'slots'>> },
173
+ PublicProps & { 'v-slots'?: Partial<Field<T, 'slots'>> },
171
174
  {},
172
175
  false,
173
176
  {},
@@ -190,7 +193,38 @@ type ComponentSlots<T> = T extends new (...args: any) => { $slots: infer S }
190
193
  ? NonNullable<S>
191
194
  : {}
192
195
 
193
- export function vSlots<C>(component: C, slots: ComponentSlots<C>) {
196
+ /**
197
+ * A slot whose declared type intersects several call signatures, collapsed into one that takes
198
+ * the intersection of their props.
199
+ *
200
+ * `@nuxt/ui` builds `TableSlots` by intersecting a `Record` over header props with one over cell
201
+ * props, each carrying a `string` index signature. The names those `Record`s spell out keep
202
+ * their own props, but any other name — a column id that is no key of the row type — resolves to
203
+ * both signatures at once, which no single function satisfies. Intersecting their props leaves
204
+ * it writable, and its props inferrable.
205
+ */
206
+ type MergeSlotSignatures<S> = S extends {
207
+ (props: infer A): infer R
208
+ (props: infer B): any
209
+ }
210
+ ? // a slot declared with one signature matches the pattern too, inferring the same props
211
+ // twice; rewriting it would turn `() => VNode[]` into `(props: unknown) => VNode[]`
212
+ Same<A, B> extends true
213
+ ? S
214
+ : (props: A & B) => R
215
+ : S
216
+
217
+ /** Whether `A` and `B` are the same type, wrapped in tuples so neither side distributes. */
218
+ type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false
219
+
220
+ export function vSlots<C>(
221
+ component: C,
222
+ slots: {
223
+ [Name in keyof ComponentSlots<C>]?: MergeSlotSignatures<ComponentSlots<C>[Name]>
224
+ },
225
+ ): Partial<ComponentSlots<C>> {
226
+ // the parameter type only relaxes *how* a slot may be written; what comes back is the target's
227
+ // own slots, which is what its `v-slots` prop is declared as
194
228
  return slots
195
229
  }
196
230
 
package/nuxt.config.ts CHANGED
@@ -16,6 +16,14 @@ export default defineNuxtConfig({
16
16
  },
17
17
 
18
18
  // dev
19
+ vite: {
20
+ // `@vitejs/plugin-vue-jsx` only wires up HMR for exports whose initializer calls one of
21
+ // these names. Every component here is `export default defineSetupComponent(...)`, so
22
+ // without this no module is an HMR boundary and every edit escalates to a full reload.
23
+ vueJsx: {
24
+ defineComponentName: ['defineComponent', 'defineSetupComponent'],
25
+ },
26
+ },
19
27
  typescript: {
20
28
  strict: true,
21
29
  tsConfig: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@falcondev-oss/nuxt-layers-base",
3
3
  "type": "module",
4
- "version": "0.40.3",
4
+ "version": "0.41.1",
5
5
  "description": "Nuxt layer with lots of useful helpers and @nuxt/ui components",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -57,14 +57,15 @@
57
57
  "vue-router": "^5.2.0"
58
58
  },
59
59
  "devDependencies": {
60
+ "@types/luxon": "^3.7.5",
61
+ "luxon": "^3.7.2",
60
62
  "nuxt": "^4.5.2",
61
63
  "typescript": "^5.9.3",
62
- "vue-tsc": "^3.3.11",
63
64
  "zod": "^4.4.3"
64
65
  },
65
66
  "scripts": {
66
67
  "dev": "nuxi dev .playground",
67
- "type-check": "vue-tsc --build",
68
+ "type-check": "tsc --build",
68
69
  "lint": "pnpm -w eslint:cmd \"$PWD\" && pnpm -w prettier:cmd \"$PWD\" --check",
69
70
  "lint:fix": "pnpm -w eslint:cmd \"$PWD\" --fix && pnpm -w prettier:cmd \"$PWD\" --write"
70
71
  }
package/app/app.vue DELETED
@@ -1,3 +0,0 @@
1
- <template>
2
- <UCustomApp />
3
- </template>
@@ -1,13 +0,0 @@
1
- <script setup lang="ts">
2
- defineSlots<{
3
- default: any
4
- }>()
5
- </script>
6
-
7
- <template>
8
- <div class="flex min-h-full flex-col items-center justify-center gap-4 p-4">
9
- <UPageCard class="w-full max-w-md">
10
- <slot />
11
- </UPageCard>
12
- </div>
13
- </template>
@@ -1,140 +0,0 @@
1
- <script setup lang="ts">
2
- import type {
3
- BreadcrumbItem,
4
- BreadcrumbProps,
5
- DashboardNavbarProps,
6
- DashboardNavbarSlots,
7
- DashboardPanelProps,
8
- DashboardToolbarProps,
9
- NavigationMenuItem,
10
- NavigationMenuProps,
11
- } from '@nuxt/ui'
12
- import type { MaybeRefOrGetter, Ref } from 'vue'
13
- import type { ToolbarTool } from '../../composables/useToolbar'
14
- import type { AddPropertyPrefix } from '../../types/helpers'
15
- import * as R from 'remeda'
16
- import { toolbarToolsKey } from '../../composables/useToolbar'
17
- import { mergeSlotClass } from '../../utils/ui'
18
-
19
- const props = defineProps<{
20
- panel?: DashboardPanelProps
21
- navbar?: {
22
- sidebarToggle?: boolean
23
- title?: string
24
- breadcrumb?: BreadcrumbItem[]
25
- breadcrumbUi?: BreadcrumbProps<BreadcrumbItem>['ui']
26
- ui?: DashboardNavbarProps['ui']
27
- }
28
- tabs?: {
29
- items?: NavigationMenuItem[]
30
- ui?: NavigationMenuProps['ui']
31
- }
32
- tools?: {
33
- items?: NavigationMenuItem[]
34
- ui?: NavigationMenuProps['ui']
35
- }
36
- toolbarUi?: DashboardToolbarProps['ui']
37
- }>()
38
-
39
- const slots = defineSlots<
40
- {
41
- 'default': any
42
- 'navbar-title'?: any
43
- 'navbar-trailing'?: any
44
- 'navbar-actions'?: any
45
- } & AddPropertyPrefix<DashboardNavbarSlots, 'navbar'>
46
- >()
47
-
48
- // rendered manually in the `left` slot below
49
- const omitNavbarSlots = [
50
- 'navbar-left',
51
- 'navbar-leading',
52
- 'navbar-title',
53
- 'navbar-trailing',
54
- 'navbar-right',
55
- 'navbar-actions',
56
- ] satisfies (keyof typeof slots)[]
57
- const navbarSlots = computed(() =>
58
- R.pipe(
59
- slots,
60
- R.pickBy((_, key) => key.startsWith('navbar-')),
61
- R.omit(omitNavbarSlots),
62
- R.mapKeys((key) => key.replace('navbar-', '')),
63
- ),
64
- )
65
-
66
- // added by the content rendered inside this navbar through `useToolbar()`
67
- // `ref()`'s deep unwrapping blows up on the recursive `NavigationMenuItem` type
68
- const providedTools = ref([]) as Ref<MaybeRefOrGetter<ToolbarTool>[]>
69
- provide(toolbarToolsKey, providedTools)
70
-
71
- const toolItems = computed(() => [
72
- ...(props.tools?.items ?? []),
73
- ...providedTools.value.map((tool) => toValue(tool)),
74
- ])
75
-
76
- const navbarUi = computed<DashboardNavbarProps['ui']>(() => ({
77
- ...props.navbar?.ui,
78
- toggle: mergeSlotClass(props.navbar?.ui?.toggle, '-ml-1'),
79
- ...(props.navbar?.breadcrumb && {
80
- root: mergeSlotClass(props.navbar.ui?.root, 'h-auto min-h-(--ui-header-height) py-2'),
81
- }),
82
- }))
83
- </script>
84
-
85
- <template>
86
- <UDashboardPanel v-bind="panel">
87
- <template #header>
88
- <ForwardSlots v-if="navbar" :slots="navbarSlots">
89
- <UDashboardNavbar :ui="navbarUi" class="bg-white" :title="navbar.title">
90
- <template #left>
91
- <UDashboardSidebarCollapse v-if="navbar.sidebarToggle" class="-ml-1" />
92
-
93
- <div class="flex min-w-0 flex-col items-start gap-0.5">
94
- <div class="flex min-w-0 items-center gap-1.5">
95
- <h1 class="text-highlighted truncate font-semibold">
96
- <slot name="navbar-title">{{ navbar.title }}</slot>
97
- </h1>
98
- <slot name="navbar-trailing" />
99
- </div>
100
-
101
- <UBreadcrumb
102
- v-if="navbar.breadcrumb"
103
- :items="navbar.breadcrumb"
104
- :ui="{
105
- ...navbar.breadcrumbUi,
106
- link: mergeSlotClass(navbar.breadcrumbUi?.link, 'text-xs'),
107
- separatorIcon: mergeSlotClass(navbar.breadcrumbUi?.separatorIcon, 'size-3.5'),
108
- }"
109
- />
110
- </div>
111
- </template>
112
-
113
- <template #right>
114
- <!-- eslint-disable-next-line vue/require-explicit-slots -->
115
- <slot name="navbar-right" />
116
- <div id="navbar-actions" class="flex items-center gap-2">
117
- <slot name="navbar-actions" />
118
- </div>
119
- </template>
120
- </UDashboardNavbar>
121
- </ForwardSlots>
122
- <UDashboardToolbar
123
- v-if="tabs || toolItems.length"
124
- :ui="toolbarUi"
125
- class="bg-white *:first:-ml-2"
126
- >
127
- <template #left>
128
- <UNavigationMenu v-if="tabs" :items="tabs.items" highlight variant="link" :ui="tabs.ui" />
129
- <UNavigationMenu v-else :items="toolItems" highlight :ui="tools?.ui" />
130
- </template>
131
- <template v-if="tabs" #right>
132
- <UNavigationMenu :items="toolItems" highlight :ui="tools?.ui" />
133
- </template>
134
- </UDashboardToolbar>
135
- </template>
136
- <template #body>
137
- <slot />
138
- </template>
139
- </UDashboardPanel>
140
- </template>