@frigear-nu/ui 0.1.1 → 0.1.3

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 (26) hide show
  1. package/README.md +35 -0
  2. package/dist/runtime/components/global/AppShell.d.vue.ts +29 -0
  3. package/dist/runtime/components/global/AppShell.vue +171 -0
  4. package/dist/runtime/components/global/AppShell.vue.d.ts +29 -0
  5. package/dist/runtime/components/global/FormRow.d.vue.ts +13 -0
  6. package/dist/runtime/components/global/FormRow.vue +32 -0
  7. package/dist/runtime/components/global/FormRow.vue.d.ts +13 -0
  8. package/dist/runtime/components/global/PageSectionSwitcher.d.vue.ts +14 -0
  9. package/dist/runtime/components/global/PageSectionSwitcher.vue +41 -0
  10. package/dist/runtime/components/global/PageSectionSwitcher.vue.d.ts +14 -0
  11. package/dist/runtime/components/global/StatusBadge.d.vue.ts +20 -0
  12. package/dist/runtime/components/global/StatusBadge.vue +23 -0
  13. package/dist/runtime/components/global/StatusBadge.vue.d.ts +20 -0
  14. package/dist/runtime/components/global/TableCard.d.vue.ts +11 -6
  15. package/dist/runtime/components/global/TableCard.vue +28 -3
  16. package/dist/runtime/components/global/TableCard.vue.d.ts +11 -6
  17. package/dist/runtime/composables/index.d.ts +12 -10
  18. package/dist/runtime/composables/index.js +12 -10
  19. package/dist/runtime/composables/useBootstrap.d.ts +33 -0
  20. package/dist/runtime/composables/useBootstrap.js +46 -0
  21. package/dist/runtime/composables/useCachedFetch.d.ts +76 -0
  22. package/dist/runtime/composables/useCachedFetch.js +61 -0
  23. package/dist/runtime/i18n/lang/da.json +1 -0
  24. package/dist/runtime/i18n/lang/en.json +1 -0
  25. package/dist/runtime/types.d.ts +31 -1
  26. package/package.json +5 -5
package/README.md CHANGED
@@ -7,11 +7,45 @@ card uses the same `bg-elevated/50 ring ring-default rounded-lg` surface and the
7
7
  Built-in strings come from `@nuxtjs/i18n` when installed (`patterns.*`, `actions.*`; `en` and `da` included), with
8
8
  English fallbacks; every one of them can be overridden by a prop.
9
9
 
10
+ ## App shell
11
+
12
+ `AppShell` is the dashboard shell of the app areas (admin, account); an area layout wraps its pages in it:
13
+
14
+ ```vue
15
+ <AppShell
16
+ id="admin"
17
+ :title="t('title', 'Administration')"
18
+ home="/admin"
19
+ :navigation="navigation"
20
+ :nav-label="t('nav.label', 'Admin navigation')"
21
+ :search-groups="searchGroups"
22
+ :back-to="{ to: '/', label: t('backToSite', 'Back to the site') }"
23
+ >
24
+ <template #sidebar-footer="{ collapsed }">
25
+ <AccountMenu block :collapsed="collapsed" />
26
+ </template>
27
+ <slot />
28
+ </AppShell>
29
+ ```
30
+
31
+ - `id` keys the `dashboard-sidebar-<id>` cookie (collapse, width); one per area. `home` and `backTo.to` are raw
32
+ paths, the shell adds the locale prefix.
33
+ - `navigation` is a list of `UNavigationMenu` groups (`type: 'label'` rows head a group). The navbar shows a muted
34
+ eyebrow, `title › active section`, never a heading: the page renders the only `<h1>` through `PageHeading`.
35
+ - `searchGroups` adds the ⌘K button and palette (without the colour-mode group); leave it out for no search.
36
+ - The body is centred at `max-w-7xl`; `wide` or `definePageMeta({ wide: true })` makes it full width.
37
+ - The document title defaults to the active section (else `title`); a page's own `useHead` wins.
38
+ - Slots: `brand`, `sidebar-top`, `sidebar-footer` (all get `{ collapsed }`), `navbar-left` (replaces the eyebrow),
39
+ `navbar-right` (defaults to `LanguageSelect` when the app has one and more than one locale), default.
40
+ - The light app theme is utility classes on the shell (`bg-default` body, `bg-elevated/25` sidebar, a
41
+ primary-coloured brand tile); hosts can target `[data-area=app]`.
42
+
10
43
  ## Page patterns
11
44
 
12
45
  | Component | Use |
13
46
  |---|---|
14
47
  | `PageHeading` | Title block of an app page: breadcrumb (array) or eyebrow line (string), `#badge`, description, meta row, `#actions` |
48
+ | `PageSectionSwitcher` | Full-width button naming the current section, with a menu of all sections (mobile section navigation) |
15
49
  | `PageTabs` | Tabs under a heading: links (`to`, route decides) or state (`v-model` + `value`) |
16
50
  | `SectionCard` | A titled card for one section (form, key facts); `flush` for tables and lists |
17
51
  | `TableCard` | The surface a `UTable` sits in (`:ui="tableUi()"`), with `#header` and `#footer` (total, pagination) |
@@ -20,6 +54,7 @@ English fallbacks; every one of them can be overridden by a prop.
20
54
  | `ListCard` | A short divided list with a "View all" link |
21
55
  | `DescriptionList` | Label/value rows; empty values read "Not set"; slot per `key` |
22
56
  | `PageToolbar` | Search and filters above a table, `#right` for counts |
57
+ | `FormRow` | A `UFormField` as a settings row (label and hint left, control right, stacked below `sm`) for inline forms in a `SectionCard`; parent `divide-y divide-default` |
23
58
  | `FormSlideover` | Create/edit form in a slideover with Cancel + submit (`form="<formId>"`); works with `useOverlay` |
24
59
  | `ConfirmDialog` / `useConfirmDialog()` | Confirmation before destructive actions |
25
60
 
@@ -0,0 +1,29 @@
1
+ import type { AppShellProps } from '../../types.js';
2
+ type __VLS_Slots = {
3
+ /** Replaces the brand link in the sidebar header. */
4
+ 'brand'?: (props: {
5
+ collapsed: boolean;
6
+ }) => unknown;
7
+ /** Between the search button and the navigation. */
8
+ 'sidebar-top'?: (props: {
9
+ collapsed: boolean;
10
+ }) => unknown;
11
+ /** Below the `backTo` link, e.g. the account menu. */
12
+ 'sidebar-footer'?: (props: {
13
+ collapsed: boolean;
14
+ }) => unknown;
15
+ /** Replaces the navbar eyebrow (the collapse button stays). */
16
+ 'navbar-left'?: () => unknown;
17
+ /** Right side of the navbar; defaults to `LanguageSelect` when there is more than one locale. */
18
+ 'navbar-right'?: () => unknown;
19
+ 'default'?: () => unknown;
20
+ };
21
+ declare const __VLS_base: import("vue").DefineComponent<AppShellProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<AppShellProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
23
+ declare const _default: typeof __VLS_export;
24
+ export default _default;
25
+ type __VLS_WithSlots<T, S> = T & {
26
+ new (): {
27
+ $slots: S;
28
+ };
29
+ };
@@ -0,0 +1,171 @@
1
+ <script setup>
2
+ import { useLocaleLink } from "../../composables/useLocaleLink";
3
+ const props = defineProps({
4
+ id: { type: String, required: true },
5
+ title: { type: String, required: true },
6
+ home: { type: String, required: true },
7
+ icon: { type: String, required: false },
8
+ navigation: { type: Array, required: true },
9
+ navLabel: { type: String, required: true },
10
+ searchGroups: { type: Array, required: false },
11
+ searchLabel: { type: String, required: false },
12
+ backTo: { type: Object, required: false },
13
+ wide: { type: Boolean, required: false }
14
+ });
15
+ defineSlots();
16
+ const route = useRoute();
17
+ const { localize } = useLocaleLink();
18
+ const resolvedLanguageSelect = resolveComponent("LanguageSelect");
19
+ const languageSelect = typeof resolvedLanguageSelect === "string" ? void 0 : resolvedLanguageSelect;
20
+ const publicConfig = useRuntimeConfig().public ?? {};
21
+ const nuxtApp = useNuxtApp();
22
+ const multiLocale = typeof publicConfig.multiLocale === "boolean" ? publicConfig.multiLocale : (nuxtApp.$i18n?.locales?.value?.length ?? 0) > 1;
23
+ const section = computed(() => {
24
+ const items = props.navigation.flat().flatMap((item) => [item, ...item.children ?? []]);
25
+ return items.find((item) => item.type !== "label" && item.active === true)?.label;
26
+ });
27
+ const crumb = computed(() => section.value && section.value !== props.title ? section.value : void 0);
28
+ const isWide = computed(() => props.wide || route.meta.wide === true);
29
+ useHead({ title: () => section.value ?? props.title }, { tagPriority: "low" });
30
+ </script>
31
+
32
+ <template>
33
+ <UDashboardGroup
34
+ unit="rem"
35
+ data-area="app"
36
+ class="bg-default"
37
+ >
38
+ <UDashboardSidebar
39
+ :id="id"
40
+ collapsible
41
+ resizable
42
+ class="bg-elevated/25"
43
+ :ui="{
44
+ header: 'border-b border-default',
45
+ footer: 'border-t border-default flex-col items-stretch gap-1'
46
+ }"
47
+ >
48
+ <template #header="{ collapsed }">
49
+ <slot
50
+ name="brand"
51
+ :collapsed="collapsed"
52
+ >
53
+ <NuxtLink
54
+ :to="localize(home)"
55
+ :aria-label="collapsed ? title : void 0"
56
+ class="flex min-w-0 items-center gap-2"
57
+ >
58
+ <span class="bg-primary text-inverted flex size-7 shrink-0 items-center justify-center rounded-lg">
59
+ <UIcon
60
+ :name="icon ?? 'i-lucide-layout-dashboard'"
61
+ class="size-4"
62
+ />
63
+ </span>
64
+ <span
65
+ v-if="!collapsed"
66
+ class="text-highlighted truncate font-semibold"
67
+ >{{ title }}</span>
68
+ </NuxtLink>
69
+ </slot>
70
+ </template>
71
+
72
+ <template #default="{ collapsed }">
73
+ <UDashboardSearchButton
74
+ v-if="searchGroups"
75
+ :collapsed="collapsed"
76
+ :label="searchLabel"
77
+ class="bg-transparent ring-default"
78
+ />
79
+
80
+ <slot
81
+ name="sidebar-top"
82
+ :collapsed="collapsed"
83
+ />
84
+
85
+ <UNavigationMenu
86
+ :collapsed="collapsed"
87
+ :items="navigation"
88
+ orientation="vertical"
89
+ tooltip
90
+ :aria-label="navLabel"
91
+ />
92
+ </template>
93
+
94
+ <template
95
+ v-if="backTo || $slots['sidebar-footer']"
96
+ #footer="{ collapsed }"
97
+ >
98
+ <UButton
99
+ v-if="backTo"
100
+ :to="localize(backTo.to)"
101
+ color="neutral"
102
+ variant="ghost"
103
+ :icon="backTo.icon ?? 'i-lucide-arrow-left'"
104
+ :label="collapsed ? void 0 : backTo.label"
105
+ :aria-label="collapsed ? backTo.label : void 0"
106
+ :square="collapsed"
107
+ block
108
+ class="justify-start"
109
+ />
110
+ <slot
111
+ name="sidebar-footer"
112
+ :collapsed="collapsed"
113
+ />
114
+ </template>
115
+ </UDashboardSidebar>
116
+
117
+ <UDashboardSearch
118
+ v-if="searchGroups"
119
+ :groups="searchGroups"
120
+ :color-mode="false"
121
+ />
122
+
123
+ <UDashboardPanel :id="`${id}-main`">
124
+ <template #header>
125
+ <UDashboardNavbar
126
+ class="bg-default/75 backdrop-blur"
127
+ :ui="{ right: 'gap-2' }"
128
+ >
129
+ <template #left>
130
+ <UDashboardSidebarCollapse />
131
+ <slot name="navbar-left">
132
+ <p
133
+ data-slot="eyebrow"
134
+ class="flex min-w-0 items-center gap-1.5 text-sm"
135
+ >
136
+ <span
137
+ class="truncate"
138
+ :class="crumb ? 'text-muted' : 'text-highlighted font-medium'"
139
+ >{{ title }}</span>
140
+ <template v-if="crumb">
141
+ <UIcon
142
+ name="i-lucide-chevron-right"
143
+ class="text-dimmed size-4 shrink-0"
144
+ />
145
+ <span class="text-highlighted truncate font-medium">{{ crumb }}</span>
146
+ </template>
147
+ </p>
148
+ </slot>
149
+ </template>
150
+ <template #right>
151
+ <slot name="navbar-right">
152
+ <component
153
+ :is="languageSelect"
154
+ v-if="languageSelect && multiLocale"
155
+ />
156
+ </slot>
157
+ </template>
158
+ </UDashboardNavbar>
159
+ </template>
160
+
161
+ <template #body>
162
+ <div
163
+ class="mx-auto w-full"
164
+ :class="isWide ? '' : 'max-w-7xl'"
165
+ >
166
+ <slot />
167
+ </div>
168
+ </template>
169
+ </UDashboardPanel>
170
+ </UDashboardGroup>
171
+ </template>
@@ -0,0 +1,29 @@
1
+ import type { AppShellProps } from '../../types.js';
2
+ type __VLS_Slots = {
3
+ /** Replaces the brand link in the sidebar header. */
4
+ 'brand'?: (props: {
5
+ collapsed: boolean;
6
+ }) => unknown;
7
+ /** Between the search button and the navigation. */
8
+ 'sidebar-top'?: (props: {
9
+ collapsed: boolean;
10
+ }) => unknown;
11
+ /** Below the `backTo` link, e.g. the account menu. */
12
+ 'sidebar-footer'?: (props: {
13
+ collapsed: boolean;
14
+ }) => unknown;
15
+ /** Replaces the navbar eyebrow (the collapse button stays). */
16
+ 'navbar-left'?: () => unknown;
17
+ /** Right side of the navbar; defaults to `LanguageSelect` when there is more than one locale. */
18
+ 'navbar-right'?: () => unknown;
19
+ 'default'?: () => unknown;
20
+ };
21
+ declare const __VLS_base: import("vue").DefineComponent<AppShellProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<AppShellProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
23
+ declare const _default: typeof __VLS_export;
24
+ export default _default;
25
+ type __VLS_WithSlots<T, S> = T & {
26
+ new (): {
27
+ $slots: S;
28
+ };
29
+ };
@@ -0,0 +1,13 @@
1
+ declare var __VLS_10: string, __VLS_11: any;
2
+ type __VLS_Slots = {} & {
3
+ [K in NonNullable<typeof __VLS_10>]?: (props: typeof __VLS_11) => any;
4
+ };
5
+ declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
9
+ type __VLS_WithSlots<T, S> = T & {
10
+ new (): {
11
+ $slots: S;
12
+ };
13
+ };
@@ -0,0 +1,32 @@
1
+ <script setup>
2
+ defineOptions({ inheritAttrs: false });
3
+ const ROW_UI = {
4
+ root: "gap-x-8 gap-y-2 py-4 first:pt-0 last:pb-0 max-sm:flex-col max-sm:items-stretch",
5
+ wrapper: "sm:w-1/3 sm:max-w-64 sm:shrink-0",
6
+ container: "min-w-0 flex-1"
7
+ };
8
+ const attrs = useAttrs();
9
+ const ui = computed(() => {
10
+ const own = attrs.ui ?? {};
11
+ const slots = /* @__PURE__ */ new Set([...Object.keys(ROW_UI), ...Object.keys(own)]);
12
+ return Object.fromEntries([...slots].map((slot) => [slot, [ROW_UI[slot], own[slot]].filter(Boolean).join(" ")]));
13
+ });
14
+ </script>
15
+
16
+ <template>
17
+ <UFormField
18
+ v-bind="$attrs"
19
+ orientation="horizontal"
20
+ :ui="ui"
21
+ >
22
+ <template
23
+ v-for="(_, slot) in $slots"
24
+ #[slot]="slotProps"
25
+ >
26
+ <slot
27
+ :name="slot"
28
+ v-bind="slotProps ?? {}"
29
+ />
30
+ </template>
31
+ </UFormField>
32
+ </template>
@@ -0,0 +1,13 @@
1
+ declare var __VLS_10: string, __VLS_11: any;
2
+ type __VLS_Slots = {} & {
3
+ [K in NonNullable<typeof __VLS_10>]?: (props: typeof __VLS_11) => any;
4
+ };
5
+ declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
9
+ type __VLS_WithSlots<T, S> = T & {
10
+ new (): {
11
+ $slots: S;
12
+ };
13
+ };
@@ -0,0 +1,14 @@
1
+ import type { NavigationMenuItem } from '@nuxt/ui';
2
+ /**
3
+ * A full-width button naming the current section, with a menu of all sections. The narrow-screen counterpart of a
4
+ * vertical section navigation (`UNavigationMenu`), which does not fit beside the content on a phone. Items are links
5
+ * (`to`), and the one with `active` is the current section; `PageTabs` stays for tabs a page owns.
6
+ */
7
+ type __VLS_Props = {
8
+ items: NavigationMenuItem[];
9
+ /** Overrides the accessible name of the button. */
10
+ label?: string;
11
+ };
12
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -0,0 +1,41 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ items: { type: Array, required: true },
4
+ label: { type: String, required: false }
5
+ });
6
+ const t = useUiT("patterns");
7
+ const current = computed(() => props.items.find((item) => item.active) ?? props.items[0]);
8
+ const menu = computed(() => props.items.map((item) => ({
9
+ label: item.label,
10
+ icon: item.icon,
11
+ to: item.to,
12
+ target: item.target,
13
+ external: item.external,
14
+ active: item.active,
15
+ trailingIcon: item.active ? "i-lucide-check" : void 0
16
+ })));
17
+ const ariaLabel = computed(() => {
18
+ const name = props.label ?? t("sections", "Sections");
19
+ return current.value?.label ? `${name}: ${current.value.label}` : name;
20
+ });
21
+ </script>
22
+
23
+ <template>
24
+ <UDropdownMenu
25
+ v-if="items.length"
26
+ :items="menu"
27
+ :content="{ align: 'start' }"
28
+ :ui="{ content: 'w-(--reka-dropdown-menu-trigger-width)' }"
29
+ >
30
+ <UButton
31
+ color="neutral"
32
+ variant="outline"
33
+ block
34
+ :icon="current?.icon"
35
+ trailing-icon="i-lucide-chevrons-up-down"
36
+ :label="current?.label"
37
+ :aria-label="ariaLabel"
38
+ :ui="{ trailingIcon: 'ms-auto text-dimmed' }"
39
+ />
40
+ </UDropdownMenu>
41
+ </template>
@@ -0,0 +1,14 @@
1
+ import type { NavigationMenuItem } from '@nuxt/ui';
2
+ /**
3
+ * A full-width button naming the current section, with a menu of all sections. The narrow-screen counterpart of a
4
+ * vertical section navigation (`UNavigationMenu`), which does not fit beside the content on a phone. Items are links
5
+ * (`to`), and the one with `active` is the current section; `PageTabs` stays for tabs a page owns.
6
+ */
7
+ type __VLS_Props = {
8
+ items: NavigationMenuItem[];
9
+ /** Overrides the accessible name of the button. */
10
+ label?: string;
11
+ };
12
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -0,0 +1,20 @@
1
+ import type { BadgeProps } from '@nuxt/ui';
2
+ import type { StatusColor } from '../../types.js';
3
+ /**
4
+ * A status as a subtle badge: `<StatusBadge :status="row.status" />`. The colour comes from `statusColor` (green
5
+ * for done or live states, amber for states that need attention, red for failed, neutral otherwise); `colors`
6
+ * overrides single statuses (`{ archived: 'warning' }`). `label` defaults to the status. Renders nothing when
7
+ * there is neither a status nor a label.
8
+ */
9
+ type __VLS_Props = {
10
+ status: string | null | undefined;
11
+ /** The text shown; defaults to `status`. Pass a translated label here. */
12
+ label?: string;
13
+ /** Colour overrides keyed by lower-case status, on top of the defaults. */
14
+ colors?: Record<string, StatusColor>;
15
+ size?: BadgeProps['size'];
16
+ icon?: string;
17
+ };
18
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -0,0 +1,23 @@
1
+ <script setup>
2
+ import { statusColor } from "../../utils/status";
3
+ const props = defineProps({
4
+ status: { type: null, required: true },
5
+ label: { type: String, required: false },
6
+ colors: { type: Object, required: false },
7
+ size: { type: null, required: false },
8
+ icon: { type: String, required: false }
9
+ });
10
+ const text = computed(() => props.label ?? props.status ?? "");
11
+ const color = computed(() => statusColor(props.status, props.colors));
12
+ </script>
13
+
14
+ <template>
15
+ <UBadge
16
+ v-if="text"
17
+ variant="subtle"
18
+ :color="color"
19
+ :label="text"
20
+ :icon="icon"
21
+ :size="size"
22
+ />
23
+ </template>
@@ -0,0 +1,20 @@
1
+ import type { BadgeProps } from '@nuxt/ui';
2
+ import type { StatusColor } from '../../types.js';
3
+ /**
4
+ * A status as a subtle badge: `<StatusBadge :status="row.status" />`. The colour comes from `statusColor` (green
5
+ * for done or live states, amber for states that need attention, red for failed, neutral otherwise); `colors`
6
+ * overrides single statuses (`{ archived: 'warning' }`). `label` defaults to the status. Renders nothing when
7
+ * there is neither a status nor a label.
8
+ */
9
+ type __VLS_Props = {
10
+ status: string | null | undefined;
11
+ /** The text shown; defaults to `status`. Pass a translated label here. */
12
+ label?: string;
13
+ /** Colour overrides keyed by lower-case status, on top of the defaults. */
14
+ colors?: Record<string, StatusColor>;
15
+ size?: BadgeProps['size'];
16
+ icon?: string;
17
+ };
18
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -1,24 +1,29 @@
1
1
  /**
2
- * The surface a list table sits in: `<TableCard><UTable :ui="tableUi()" … /></TableCard>`. `#header` sits above
3
- * the table (filters, a title), `#footer` below it (`TablePagination`). A surface only, so the table keeps its
4
- * typed slots.
2
+ * The surface a list table sits in: `<TableCard><UTable :ui="tableUi()" … /></TableCard>`. `title`/`description`
3
+ * (or `#header`) and `#actions` form a header row like `SectionCard`'s, for tables that sit in a tab or a detail
4
+ * page. `#header` replaces the title block (filters above the table).
5
+ * `#footer` sits below the table (`TablePagination`). A surface only, so the table keeps its typed slots.
5
6
  *
6
7
  * `loading` is the first load (skeleton rows, nothing to show yet); `refreshing` is every load after that: the
7
8
  * current rows stay and are dimmed until the next ones arrive, so paging, sorting and filtering do not flash.
8
9
  */
9
10
  type __VLS_Props = {
11
+ title?: string;
12
+ description?: string;
10
13
  /** Shows skeleton rows instead of the default slot. */
11
14
  loading?: boolean;
12
15
  /** Dims the rows on screen while new ones load. */
13
16
  refreshing?: boolean;
14
17
  };
15
- declare var __VLS_1: {}, __VLS_18: {}, __VLS_20: {};
18
+ declare var __VLS_1: {}, __VLS_3: {}, __VLS_20: {}, __VLS_22: {};
16
19
  type __VLS_Slots = {} & {
17
20
  header?: (props: typeof __VLS_1) => any;
18
21
  } & {
19
- default?: (props: typeof __VLS_18) => any;
22
+ actions?: (props: typeof __VLS_3) => any;
20
23
  } & {
21
- footer?: (props: typeof __VLS_20) => any;
24
+ default?: (props: typeof __VLS_20) => any;
25
+ } & {
26
+ footer?: (props: typeof __VLS_22) => any;
22
27
  };
23
28
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
29
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,6 +1,8 @@
1
1
  <script setup>
2
2
  import { surfaceClass } from "../../utils/surface";
3
3
  defineProps({
4
+ title: { type: String, required: false },
5
+ description: { type: String, required: false },
4
6
  loading: { type: Boolean, required: false },
5
7
  refreshing: { type: Boolean, required: false }
6
8
  });
@@ -12,10 +14,33 @@ defineProps({
12
14
  :aria-busy="loading || refreshing ? 'true' : void 0"
13
15
  >
14
16
  <div
15
- v-if="$slots.header"
16
- class="border-default flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3"
17
+ v-if="title || description || $slots.header || $slots.actions"
18
+ class="border-default flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3.5"
17
19
  >
18
- <slot name="header" />
20
+ <slot name="header">
21
+ <div class="flex min-w-0 items-center gap-2">
22
+ <div class="min-w-0">
23
+ <h2
24
+ v-if="title"
25
+ class="text-highlighted text-sm font-semibold"
26
+ >
27
+ {{ title }}
28
+ </h2>
29
+ <p
30
+ v-if="description"
31
+ class="text-muted text-sm"
32
+ >
33
+ {{ description }}
34
+ </p>
35
+ </div>
36
+ </div>
37
+ </slot>
38
+ <div
39
+ v-if="$slots.actions"
40
+ class="flex items-center gap-2"
41
+ >
42
+ <slot name="actions" />
43
+ </div>
19
44
  </div>
20
45
  <div
21
46
  v-if="loading"
@@ -1,24 +1,29 @@
1
1
  /**
2
- * The surface a list table sits in: `<TableCard><UTable :ui="tableUi()" … /></TableCard>`. `#header` sits above
3
- * the table (filters, a title), `#footer` below it (`TablePagination`). A surface only, so the table keeps its
4
- * typed slots.
2
+ * The surface a list table sits in: `<TableCard><UTable :ui="tableUi()" … /></TableCard>`. `title`/`description`
3
+ * (or `#header`) and `#actions` form a header row like `SectionCard`'s, for tables that sit in a tab or a detail
4
+ * page. `#header` replaces the title block (filters above the table).
5
+ * `#footer` sits below the table (`TablePagination`). A surface only, so the table keeps its typed slots.
5
6
  *
6
7
  * `loading` is the first load (skeleton rows, nothing to show yet); `refreshing` is every load after that: the
7
8
  * current rows stay and are dimmed until the next ones arrive, so paging, sorting and filtering do not flash.
8
9
  */
9
10
  type __VLS_Props = {
11
+ title?: string;
12
+ description?: string;
10
13
  /** Shows skeleton rows instead of the default slot. */
11
14
  loading?: boolean;
12
15
  /** Dims the rows on screen while new ones load. */
13
16
  refreshing?: boolean;
14
17
  };
15
- declare var __VLS_1: {}, __VLS_18: {}, __VLS_20: {};
18
+ declare var __VLS_1: {}, __VLS_3: {}, __VLS_20: {}, __VLS_22: {};
16
19
  type __VLS_Slots = {} & {
17
20
  header?: (props: typeof __VLS_1) => any;
18
21
  } & {
19
- default?: (props: typeof __VLS_18) => any;
22
+ actions?: (props: typeof __VLS_3) => any;
20
23
  } & {
21
- footer?: (props: typeof __VLS_20) => any;
24
+ default?: (props: typeof __VLS_20) => any;
25
+ } & {
26
+ footer?: (props: typeof __VLS_22) => any;
22
27
  };
23
28
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
29
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,10 +1,12 @@
1
- export * from './useAction.ts.js';
2
- export * from './useConfirmDialog.ts.js';
3
- export * from './useCopyToClipboard.ts.js';
4
- export * from './useErrorToast.ts.js';
5
- export * from './useFormat.ts.js';
6
- export * from './useLocaleLink.ts.js';
7
- export * from './useNavLinks.ts.js';
8
- export * from './useTableQuery.ts.js';
9
- export * from './useTableSort.ts.js';
10
- export * from './useTranslatedLocales.ts.js';
1
+ export * from './useAction.js';
2
+ export * from './useBootstrap.js';
3
+ export * from './useCachedFetch.js';
4
+ export * from './useConfirmDialog.js';
5
+ export * from './useCopyToClipboard.js';
6
+ export * from './useErrorToast.js';
7
+ export * from './useFormat.js';
8
+ export * from './useLocaleLink.js';
9
+ export * from './useNavLinks.js';
10
+ export * from './useTableQuery.js';
11
+ export * from './useTableSort.js';
12
+ export * from './useTranslatedLocales.js';
@@ -1,10 +1,12 @@
1
- export * from "./useAction.ts";
2
- export * from "./useConfirmDialog.ts";
3
- export * from "./useCopyToClipboard.ts";
4
- export * from "./useErrorToast.ts";
5
- export * from "./useFormat.ts";
6
- export * from "./useLocaleLink.ts";
7
- export * from "./useNavLinks.ts";
8
- export * from "./useTableQuery.ts";
9
- export * from "./useTableSort.ts";
10
- export * from "./useTranslatedLocales.ts";
1
+ export * from "./useAction.js";
2
+ export * from "./useBootstrap.js";
3
+ export * from "./useCachedFetch.js";
4
+ export * from "./useConfirmDialog.js";
5
+ export * from "./useCopyToClipboard.js";
6
+ export * from "./useErrorToast.js";
7
+ export * from "./useFormat.js";
8
+ export * from "./useLocaleLink.js";
9
+ export * from "./useNavLinks.js";
10
+ export * from "./useTableQuery.js";
11
+ export * from "./useTableSort.js";
12
+ export * from "./useTranslatedLocales.js";
@@ -0,0 +1,33 @@
1
+ /** What the app knows about an area's last bootstrap: when it loaded and which keys it primed. */
2
+ export interface BootstrapState {
3
+ at: number;
4
+ keys: string[];
5
+ }
6
+ export interface LoadBootstrapOptions {
7
+ /**
8
+ * How long (ms) a bootstrap counts as fresh: entering the area again within it makes no request. Keep it at or
9
+ * below the `ttl` of the composables it primes, so they always find their entry fresh. Default one minute.
10
+ */
11
+ ttl?: number;
12
+ }
13
+ export declare const BOOTSTRAP_TTL_MS: number;
14
+ /** The last bootstrap of `scope` (`null` before the first, after `resetBootstrap` or when it failed). */
15
+ export declare const useBootstrapState: (scope: string) => any;
16
+ /**
17
+ * Load an area's user-scoped data in one request (`GET /api/auth/bootstrap?scope=<scope>`, served by `@frigear-nu/auth`)
18
+ * and prime the `useCachedFetch` entry of every key it returns, so the modules' composables (`useMyTickets()`,
19
+ * `useSubscription()`, ...) render without a request of their own. Call it from a route middleware of the area: it
20
+ * runs before the page's components set up, on the server and on every client navigation.
21
+ *
22
+ * - Server: the data goes into the payload, used by the server render and by hydration.
23
+ * - Hydration: the payload entries are remembered for later navigation; nothing is fetched.
24
+ * - Client navigation: fetches again only when the last bootstrap is older than `ttl`.
25
+ *
26
+ * Never throws: when the request fails (or leaves a key out) the composables fetch from their own endpoints.
27
+ */
28
+ export declare function loadBootstrap(scope: string, options?: LoadBootstrapOptions): Promise<void>;
29
+ /**
30
+ * Forget an area's bootstrap and the cache entries it primed, so the next visit loads them again. Call it when the
31
+ * data no longer belongs to the visitor (signing in or out).
32
+ */
33
+ export declare function resetBootstrap(scope: string): void;
@@ -0,0 +1,46 @@
1
+ import { clearCachedFetch, primeCachedFetch } from "./useCachedFetch.js";
2
+ export const BOOTSTRAP_TTL_MS = 60 * 1e3;
3
+ const BOOTSTRAP_URL = "/api/auth/bootstrap";
4
+ const stateKey = (scope) => `ui:bootstrap:${scope}`;
5
+ const inFlight = /* @__PURE__ */ new Map();
6
+ export const useBootstrapState = (scope) => useState(stateKey(scope), () => null);
7
+ export async function loadBootstrap(scope, options = {}) {
8
+ const nuxtApp = useNuxtApp();
9
+ const state = useBootstrapState(scope);
10
+ const ttl = options.ttl ?? BOOTSTRAP_TTL_MS;
11
+ if (import.meta.client) {
12
+ if (nuxtApp.isHydrating && nuxtApp.payload.serverRendered) {
13
+ if (!state.value) return;
14
+ for (const key of state.value.keys) {
15
+ const data = nuxtApp.payload.data[key];
16
+ if (data !== void 0) primeCachedFetch(key, data);
17
+ }
18
+ state.value = { ...state.value, at: Date.now() };
19
+ return;
20
+ }
21
+ if (state.value && Date.now() - state.value.at < ttl) return;
22
+ const pending = inFlight.get(scope);
23
+ if (pending) return pending;
24
+ }
25
+ const run = async () => {
26
+ try {
27
+ const fetcher = import.meta.server ? useRequestFetch() : $fetch;
28
+ const data = await fetcher(BOOTSTRAP_URL, { query: { scope } });
29
+ nuxtApp.runWithContext(() => {
30
+ for (const [key, value] of Object.entries(data)) primeCachedFetch(key, value);
31
+ });
32
+ state.value = { at: Date.now(), keys: Object.keys(data) };
33
+ } catch (error) {
34
+ if (import.meta.dev) console.warn(`[ui] bootstrap "${scope}" failed, the pages load their own data:`, error);
35
+ }
36
+ };
37
+ if (import.meta.server) return run();
38
+ const promise = run().finally(() => inFlight.delete(scope));
39
+ inFlight.set(scope, promise);
40
+ return promise;
41
+ }
42
+ export function resetBootstrap(scope) {
43
+ const state = useBootstrapState(scope);
44
+ if (state.value?.keys.length) clearCachedFetch(state.value.keys);
45
+ state.value = null;
46
+ }
@@ -0,0 +1,76 @@
1
+ import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';
2
+ import type { AsyncData, NuxtApp, NuxtError, UseFetchOptions } from 'nuxt/app';
3
+ /** One remembered response: what `useFetch` produced (after `transform`) and when. */
4
+ export interface CachedFetchEntry<T = unknown> {
5
+ at: number;
6
+ data: T;
7
+ }
8
+ export type CachedFetchStore = Record<string, CachedFetchEntry>;
9
+ export type CachedFetchOptions<ResT, DataT = ResT> = Omit<UseFetchOptions<ResT, DataT>, 'key' | 'default' | 'getCachedData'> & {
10
+ /** The fetch key. Required: it names the cache entry, so composables that share data must share the key. */
11
+ key: MaybeRefOrGetter<string>;
12
+ /**
13
+ * How long (ms) a remembered response is reused when a component mounts again (client navigation) without asking
14
+ * the server. Leave it out to reuse it until `refresh()`; `0` always refetches. Either way the remembered response
15
+ * stays on screen while the new one loads.
16
+ */
17
+ ttl?: number;
18
+ };
19
+ export type CachedFetchResult<DataT> = Omit<Awaited<AsyncData<DataT | undefined, NuxtError | undefined>>, 'pending'> & {
20
+ /**
21
+ * `true` only while there is nothing to show yet: the first load of this key. Background refreshes (a stale cache,
22
+ * `refresh()`) keep the last data on screen and leave it `false`. Named `loading` so it does not shadow Nuxt's
23
+ * deprecated `pending` (which is `status === 'pending'`).
24
+ */
25
+ loading: ComputedRef<boolean>;
26
+ };
27
+ /** The shared, per-app cache behind `useCachedFetch`. Only the client writes to it, so it adds nothing to the payload. */
28
+ export declare const useCachedFetchStore: () => Ref<CachedFetchStore>;
29
+ /**
30
+ * Remember a response for `key` as if `useCachedFetch` had fetched it, e.g. data loaded ahead of time (a warm-up, an
31
+ * aggregated request such as `loadBootstrap`). `data` must have the shape the composable's `transform` produces.
32
+ *
33
+ * On the server it goes into this request's payload under `key`, where `useCachedFetch` finds it during server
34
+ * rendering (no request of its own) and the client finds it while hydrating. Prime before the components using
35
+ * `key` set up (a route middleware), so they see it.
36
+ */
37
+ export declare function primeCachedFetch<T>(key: string, data: T): void;
38
+ /**
39
+ * Forget remembered responses so the next mount fetches again: one key, the keys a predicate accepts, or all of them.
40
+ * Call it when the data no longer belongs to the visitor (signing out).
41
+ */
42
+ export declare function clearCachedFetch(keys?: string | string[] | ((key: string) => boolean)): void;
43
+ /**
44
+ * Where `useCachedFetch` gets data without a request, in this order:
45
+ * 1. while hydrating, the server's response from the payload (and it is remembered for later mounts); on the server,
46
+ * data primed for this request (`primeCachedFetch`);
47
+ * 2. on a mount (`cause: 'initial'`), the remembered response while it is younger than `ttl`;
48
+ * 3. otherwise nothing, so `useFetch` fetches. Refreshes (`refresh()`, `refreshNuxtData`, watchers) always fetch.
49
+ *
50
+ * Exported for tests; use `useCachedFetch`.
51
+ */
52
+ export declare function cachedFetchData(key: string, nuxtApp: Pick<NuxtApp, 'isHydrating' | 'payload'>, cause: string, cache: CachedFetchStore, ttl: number | undefined, now?: number, server?: boolean): {
53
+ data: unknown;
54
+ remember: boolean;
55
+ } | undefined;
56
+ /**
57
+ * `useFetch` whose data survives client navigation and that is awaitable in pages.
58
+ *
59
+ * Nuxt drops `useFetch` data once the last component using a key unmounts, so going back to a page blanked it and
60
+ * showed the empty state until a refetch landed. `useCachedFetch` remembers each response by `key` (see
61
+ * `cachedFetchData` for the order) and shows the remembered one at once, refetching in the background when it is
62
+ * older than `ttl`.
63
+ *
64
+ * - `data` is `undefined` until the first response; there is no `default`, so a failed request never looks like an
65
+ * empty result. Check `error` (and `status`) and derive lists with `?? []`.
66
+ * - `loading` is `true` only while nothing is on screen yet: render a skeleton for it, never the empty state.
67
+ * - `await` it in pages so SSR renders with data; in a widget, use it without `await` and render from `loading`.
68
+ *
69
+ * ```ts
70
+ * const { data, error, loading, refresh } = await useCachedFetch<TicketDto[]>('/api/events/me/tickets', {
71
+ * key: 'events:tickets:mine',
72
+ * ttl: 60_000,
73
+ * })
74
+ * ```
75
+ */
76
+ export declare function useCachedFetch<ResT, DataT = ResT>(request: string | Ref<string> | (() => string), options: CachedFetchOptions<ResT, DataT>): CachedFetchResult<DataT> & Promise<CachedFetchResult<DataT>>;
@@ -0,0 +1,61 @@
1
+ const STATE_KEY = "ui:cached-fetch";
2
+ export const useCachedFetchStore = () => useState(STATE_KEY, () => ({}));
3
+ export function primeCachedFetch(key, data) {
4
+ if (import.meta.server) {
5
+ useNuxtApp().payload.data[key] = data;
6
+ return;
7
+ }
8
+ useCachedFetchStore().value[key] = markRaw({ at: Date.now(), data });
9
+ }
10
+ export function clearCachedFetch(keys) {
11
+ const cache = useCachedFetchStore();
12
+ const drops = keys === void 0 ? () => true : typeof keys === "function" ? keys : (key) => (Array.isArray(keys) ? keys : [keys]).includes(key);
13
+ cache.value = Object.fromEntries(Object.entries(cache.value).filter(([key]) => !drops(key)));
14
+ }
15
+ export function cachedFetchData(key, nuxtApp, cause, cache, ttl, now = Date.now(), server = !!import.meta.server) {
16
+ if ((nuxtApp.isHydrating || server && cause === "initial") && nuxtApp.payload.data[key] !== void 0) {
17
+ return { data: nuxtApp.payload.data[key], remember: true };
18
+ }
19
+ if (cause !== "initial") return void 0;
20
+ const entry = cache[key];
21
+ if (!entry) return void 0;
22
+ if (ttl !== void 0 && now - entry.at >= ttl) return void 0;
23
+ return { data: entry.data, remember: false };
24
+ }
25
+ export function useCachedFetch(request, options) {
26
+ const cache = useCachedFetchStore();
27
+ const { key, ttl, transform, ...rest } = options;
28
+ const remember = (name, data) => {
29
+ if (!import.meta.server) cache.value[name] = markRaw({ at: Date.now(), data });
30
+ };
31
+ const fetched = useFetch(request, {
32
+ ...rest,
33
+ key,
34
+ // No caller default: the remembered (possibly stale) response, so a refetch never blanks the screen, or nothing.
35
+ default: () => cache.value[toValue(key)]?.data,
36
+ getCachedData: (name, nuxtApp, ctx) => {
37
+ const hit = cachedFetchData(name, nuxtApp, ctx.cause, cache.value, ttl);
38
+ if (hit?.remember) remember(name, hit.data);
39
+ return hit?.data;
40
+ },
41
+ transform: async (input) => {
42
+ const output = transform ? await transform(input) : input;
43
+ remember(toValue(key), output);
44
+ return output;
45
+ }
46
+ });
47
+ const result = {
48
+ data: fetched.data,
49
+ status: fetched.status,
50
+ error: fetched.error,
51
+ refresh: fetched.refresh,
52
+ execute: fetched.execute,
53
+ clear: () => {
54
+ const name = toValue(key);
55
+ cache.value = Object.fromEntries(Object.entries(cache.value).filter(([entry]) => entry !== name));
56
+ fetched.clear();
57
+ },
58
+ loading: computed(() => fetched.status.value === "pending" && fetched.data.value === void 0)
59
+ };
60
+ return Object.assign(Promise.resolve(fetched).then(() => result), result);
61
+ }
@@ -45,6 +45,7 @@
45
45
  "edit": "Redigér",
46
46
  "delete": "Slet",
47
47
  "confirm": "Bekræft",
48
+ "sections": "Sektioner",
48
49
  "breadcrumb": "Brødkrummesti",
49
50
  "sortBy": "Sortér efter {column}",
50
51
  "sortedAsc": "Sortér efter {column}, nu stigende",
@@ -45,6 +45,7 @@
45
45
  "edit": "Edit",
46
46
  "delete": "Delete",
47
47
  "confirm": "Confirm",
48
+ "sections": "Sections",
48
49
  "breadcrumb": "Breadcrumb",
49
50
  "sortBy": "Sort by {column}",
50
51
  "sortedAsc": "Sort by {column}, now ascending",
@@ -1,4 +1,4 @@
1
- import type { NavigationMenuItem } from '@nuxt/ui';
1
+ import type { CommandPaletteGroup, NavigationMenuItem } from '@nuxt/ui';
2
2
  /** A row of `DescriptionList`. Without `value` the slot named after `key` (or `label`) renders it. */
3
3
  export interface DescriptionItem {
4
4
  key?: string;
@@ -32,3 +32,33 @@ export interface ActionOptions {
32
32
  /** Toast title shown when the action failed; defaults to "Something went wrong.". */
33
33
  error?: string;
34
34
  }
35
+ /** The sidebar footer link of `AppShell` (e.g. "Back to the site"). `to` is a raw path; the shell localises it. */
36
+ export interface AppShellBackTo {
37
+ to: string;
38
+ label: string;
39
+ /** Defaults to `i-lucide-arrow-left`. */
40
+ icon?: string;
41
+ }
42
+ /** Props of `AppShell`, the dashboard shell of the app areas (admin, account). */
43
+ export interface AppShellProps {
44
+ /** Unique per area: the cookie keys `dashboard-sidebar-<id>` (collapse, width) and the panel id `<id>-main`. */
45
+ id: string;
46
+ /** Brand label in the sidebar, first part of the navbar eyebrow and the default document title. */
47
+ title: string;
48
+ /** Raw path the brand links to; the shell localises it. */
49
+ home: string;
50
+ /** Icon of the brand tile. Defaults to `i-lucide-layout-dashboard`. */
51
+ icon?: string;
52
+ /** Navigation groups, one `UNavigationMenu` list each; `type: 'label'` rows head a group. */
53
+ navigation: NavigationMenuItem[][];
54
+ /** `aria-label` of the navigation (e.g. "Admin navigation"). */
55
+ navLabel: string;
56
+ /** Groups of the ⌘K palette. Without them there is no search button and no palette. */
57
+ searchGroups?: CommandPaletteGroup[];
58
+ /** Label of the search button. */
59
+ searchLabel?: string;
60
+ /** A link in the sidebar footer, above `#sidebar-footer`. */
61
+ backTo?: AppShellBackTo;
62
+ /** Full-width body instead of `max-w-7xl`; pages can also ask with `definePageMeta({ wide: true })`. */
63
+ wide?: boolean;
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frigear-nu/ui",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Shared UI components and composables for Frigear Nuxt applications, built on Nuxt UI.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.frigear.dev/modules/ui",
@@ -34,17 +34,17 @@
34
34
  "dist"
35
35
  ],
36
36
  "dependencies": {
37
- "@frigear-nu/module-kit": "1.1.1",
37
+ "@frigear-nu/module-kit": "1.1.3",
38
38
  "@nuxt/kit": "^4.6.1",
39
39
  "@vueuse/core": "^13.5.0",
40
40
  "defu": "^6.1.4"
41
41
  },
42
42
  "devDependencies": {
43
- "@frigear-nu/eslint-config": "0.1.1",
43
+ "@frigear-nu/eslint-config": "0.1.3",
44
44
  "@nuxt/module-builder": "^1.0.3",
45
45
  "@nuxt/schema": "^4.6.1",
46
46
  "@nuxt/test-utils": "^4.3.2",
47
- "@nuxt/ui": "^4.11.2",
47
+ "@nuxt/ui": "^4.12.0",
48
48
  "eslint": "^10.12.0",
49
49
  "monaco-editor": "^0.56.0",
50
50
  "nuxt": "^4.6.1",
@@ -55,7 +55,7 @@
55
55
  "vue-tsc": "^3.3.11"
56
56
  },
57
57
  "peerDependencies": {
58
- "@nuxt/ui": "^4.11.2",
58
+ "@nuxt/ui": "^4.12.0",
59
59
  "nuxt": "^4.6.1",
60
60
  "vue": "3.x",
61
61
  "monaco-editor": "^0.56.0",