@frigear-nu/ui 0.1.2 → 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.
package/README.md CHANGED
@@ -7,6 +7,39 @@ 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 |
@@ -21,6 +54,7 @@ English fallbacks; every one of them can be overridden by a prop.
21
54
  | `ListCard` | A short divided list with a "View all" link |
22
55
  | `DescriptionList` | Label/value rows; empty values read "Not set"; slot per `key` |
23
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` |
24
58
  | `FormSlideover` | Create/edit form in a slideover with Cancel + submit (`form="<formId>"`); works with `useOverlay` |
25
59
  | `ConfirmDialog` / `useConfirmDialog()` | Confirmation before destructive actions |
26
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,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,12 +1,12 @@
1
- export * from './useAction.ts.js';
2
- export * from './useBootstrap.ts.js';
3
- export * from './useCachedFetch.ts.js';
4
- export * from './useConfirmDialog.ts.js';
5
- export * from './useCopyToClipboard.ts.js';
6
- export * from './useErrorToast.ts.js';
7
- export * from './useFormat.ts.js';
8
- export * from './useLocaleLink.ts.js';
9
- export * from './useNavLinks.ts.js';
10
- export * from './useTableQuery.ts.js';
11
- export * from './useTableSort.ts.js';
12
- 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,12 +1,12 @@
1
- export * from "./useAction.ts";
2
- export * from "./useBootstrap.ts";
3
- export * from "./useCachedFetch.ts";
4
- export * from "./useConfirmDialog.ts";
5
- export * from "./useCopyToClipboard.ts";
6
- export * from "./useErrorToast.ts";
7
- export * from "./useFormat.ts";
8
- export * from "./useLocaleLink.ts";
9
- export * from "./useNavLinks.ts";
10
- export * from "./useTableQuery.ts";
11
- export * from "./useTableSort.ts";
12
- 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";
@@ -14,7 +14,7 @@ export declare const BOOTSTRAP_TTL_MS: number;
14
14
  /** The last bootstrap of `scope` (`null` before the first, after `resetBootstrap` or when it failed). */
15
15
  export declare const useBootstrapState: (scope: string) => any;
16
16
  /**
17
- * Load an area's user-scoped data in one request (`GET /api/_bootstrap?scope=<scope>`, served by `@frigear-nu/auth`)
17
+ * Load an area's user-scoped data in one request (`GET /api/auth/bootstrap?scope=<scope>`, served by `@frigear-nu/auth`)
18
18
  * and prime the `useCachedFetch` entry of every key it returns, so the modules' composables (`useMyTickets()`,
19
19
  * `useSubscription()`, ...) render without a request of their own. Call it from a route middleware of the area: it
20
20
  * runs before the page's components set up, on the server and on every client navigation.
@@ -1,5 +1,6 @@
1
1
  import { clearCachedFetch, primeCachedFetch } from "./useCachedFetch.js";
2
2
  export const BOOTSTRAP_TTL_MS = 60 * 1e3;
3
+ const BOOTSTRAP_URL = "/api/auth/bootstrap";
3
4
  const stateKey = (scope) => `ui:bootstrap:${scope}`;
4
5
  const inFlight = /* @__PURE__ */ new Map();
5
6
  export const useBootstrapState = (scope) => useState(stateKey(scope), () => null);
@@ -24,7 +25,7 @@ export async function loadBootstrap(scope, options = {}) {
24
25
  const run = async () => {
25
26
  try {
26
27
  const fetcher = import.meta.server ? useRequestFetch() : $fetch;
27
- const data = await fetcher("/api/_bootstrap", { query: { scope } });
28
+ const data = await fetcher(BOOTSTRAP_URL, { query: { scope } });
28
29
  nuxtApp.runWithContext(() => {
29
30
  for (const [key, value] of Object.entries(data)) primeCachedFetch(key, value);
30
31
  });
@@ -67,7 +67,7 @@ export declare function cachedFetchData(key: string, nuxtApp: Pick<NuxtApp, 'isH
67
67
  * - `await` it in pages so SSR renders with data; in a widget, use it without `await` and render from `loading`.
68
68
  *
69
69
  * ```ts
70
- * const { data, error, loading, refresh } = await useCachedFetch<TicketDto[]>('/api/events/tickets/me', {
70
+ * const { data, error, loading, refresh } = await useCachedFetch<TicketDto[]>('/api/events/me/tickets', {
71
71
  * key: 'events:tickets:mine',
72
72
  * ttl: 60_000,
73
73
  * })
@@ -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.2",
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,13 +34,13 @@
34
34
  "dist"
35
35
  ],
36
36
  "dependencies": {
37
- "@frigear-nu/module-kit": "1.1.2",
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.2",
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",