nuxt-ui-tools 1.6.4 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -0
- package/dist/runtime/dashboard/components/block/dashboard-skeleton.vue +12 -0
- package/dist/runtime/dashboard/components/dashboard-details.d.vue.ts +38 -0
- package/dist/runtime/dashboard/components/dashboard-details.vue +194 -0
- package/dist/runtime/dashboard/components/dashboard-details.vue.d.ts +38 -0
- package/dist/runtime/dashboard/components/dashboard-page.d.vue.ts +8 -0
- package/dist/runtime/dashboard/components/dashboard-page.vue +20 -3
- package/dist/runtime/dashboard/components/dashboard-page.vue.d.ts +8 -0
- package/dist/runtime/dashboard/components/dashboard-view-tabs.d.vue.ts +2 -5
- package/dist/runtime/dashboard/components/dashboard-view-tabs.vue +5 -1
- package/dist/runtime/dashboard/components/dashboard-view-tabs.vue.d.ts +2 -5
- package/dist/runtime/dashboard/composables/use-dashboard-api.js +4 -1
- package/dist/runtime/dashboard/composables/use-dashboard-views.d.ts +4 -1
- package/dist/runtime/dashboard/composables/use-dashboard-views.js +15 -1
- package/dist/runtime/dashboard/schema/index.d.ts +15 -1
- package/dist/runtime/dashboard/types/blocks.d.ts +27 -1
- package/dist/runtime/dashboard/types/runtime.d.ts +3 -0
- package/dist/runtime/dashboard/types/schema.d.ts +23 -4
- package/dist/runtime/dashboard/types/ui.d.ts +27 -0
- package/dist/runtime/dashboard/utils/schema.js +1 -0
- package/dist/runtime/form/composables/use-form-api.d.ts +8 -0
- package/dist/runtime/form/composables/use-form-api.js +4 -4
- package/dist/runtime/form/fields/choice-card/use-choice-card.js +4 -1
- package/dist/runtime/form/plugins/form-api.d.ts +10 -0
- package/dist/runtime/form/plugins/form-api.js +10 -0
- package/dist/runtime/i18n/locales/en.js +4 -0
- package/dist/runtime/i18n/locales/fr.js +4 -0
- package/dist/runtime/i18n/types.d.ts +6 -0
- package/dist/runtime/table/components/table/table-renderer.vue +1 -3
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -55,6 +55,7 @@ function getPublicComponents(runtimeDir, options) {
|
|
|
55
55
|
"ViewTabs",
|
|
56
56
|
"Stat",
|
|
57
57
|
"Stats",
|
|
58
|
+
"Details",
|
|
58
59
|
"Gauge",
|
|
59
60
|
"Tabs",
|
|
60
61
|
"Refresh",
|
|
@@ -423,6 +424,7 @@ const module$1 = defineNuxtModule({
|
|
|
423
424
|
setupImports(resolve("./runtime"));
|
|
424
425
|
setupComponents(resolve("./runtime"), options);
|
|
425
426
|
addPlugin(resolve("./runtime/query-prefetch/plugins/link-prefetch.client"));
|
|
427
|
+
addPlugin(resolve("./runtime/form/plugins/form-api"));
|
|
426
428
|
addVitePlugin({
|
|
427
429
|
enforce: "pre",
|
|
428
430
|
name: "nuxt-ui-tools:query-prefetch-macro",
|
|
@@ -196,6 +196,18 @@ const funnelWidths = computed(
|
|
|
196
196
|
</div>
|
|
197
197
|
</div>
|
|
198
198
|
|
|
199
|
+
<!-- Details: labels over values in a grid -->
|
|
200
|
+
<div
|
|
201
|
+
v-else-if="kind === 'details'"
|
|
202
|
+
class="grid gap-x-8 gap-y-4"
|
|
203
|
+
:style="{ gridTemplateColumns: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))` }"
|
|
204
|
+
>
|
|
205
|
+
<div v-for="(width, index) in widths" :key="index" class="flex flex-col gap-2">
|
|
206
|
+
<div class="nut-dash-ghost h-2.5 w-[32%] rounded-sm opacity-70" />
|
|
207
|
+
<div class="nut-dash-ghost h-3.5 rounded-sm" :style="{ width: `${width + 12}%` }" />
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
199
211
|
<!-- Gauge: an open arc and its scale -->
|
|
200
212
|
<div v-else-if="kind === 'gauge'" class="flex flex-col items-center gap-3 pt-1">
|
|
201
213
|
<div
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DashboardBlockBaseProps, DashboardBlockUi, DashboardDetailsItem, DashboardDetailsUi, DashboardSourceLike } from '../types/index.js';
|
|
2
|
+
declare const __VLS_export: <TData>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
3
|
+
props: import("vue").PublicProps & __VLS_PrettifyLocal<Omit<DashboardBlockBaseProps, "ui"> & {
|
|
4
|
+
source: DashboardSourceLike<TData>;
|
|
5
|
+
/** Fields read from the source data, in display order. */
|
|
6
|
+
items: readonly DashboardDetailsItem<TData & ({} | null)>[];
|
|
7
|
+
/** Column count, responsive: `"1 md:2 xl:3"`. Defaults to `"1 sm:2"`. */
|
|
8
|
+
columns?: string;
|
|
9
|
+
/** `stacked`: each label over its value. `inline`: the label beside its value, on one row. */
|
|
10
|
+
layout?: "stacked" | "inline";
|
|
11
|
+
ui?: DashboardBlockUi & DashboardDetailsUi;
|
|
12
|
+
}> & (typeof globalThis extends {
|
|
13
|
+
__VLS_PROPS_FALLBACK: infer P;
|
|
14
|
+
} ? P : {});
|
|
15
|
+
expose: (exposed: {}) => void;
|
|
16
|
+
attrs: any;
|
|
17
|
+
slots: {
|
|
18
|
+
[x: `item-${string}`]: ((props: {
|
|
19
|
+
data: TData & ({} | null);
|
|
20
|
+
item: DashboardDetailsItem<TData & ({} | null)>;
|
|
21
|
+
value: string | undefined;
|
|
22
|
+
}) => unknown) | undefined;
|
|
23
|
+
} & {
|
|
24
|
+
'header-right'?: () => unknown;
|
|
25
|
+
toolbar?: () => unknown;
|
|
26
|
+
footer?: () => unknown;
|
|
27
|
+
};
|
|
28
|
+
emit: {};
|
|
29
|
+
}>) => import("vue").VNode & {
|
|
30
|
+
__ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
|
|
31
|
+
};
|
|
32
|
+
declare const _default: typeof __VLS_export;
|
|
33
|
+
export default _default;
|
|
34
|
+
type __VLS_PrettifyLocal<T> = (T extends any ? {
|
|
35
|
+
[K in keyof T]: T[K];
|
|
36
|
+
} : {
|
|
37
|
+
[K in keyof T as K]: T[K];
|
|
38
|
+
}) & {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import UButton from "@nuxt/ui/components/Button.vue";
|
|
3
|
+
import { useClipboard } from "@vueuse/core";
|
|
4
|
+
import { computed } from "vue";
|
|
5
|
+
import { useUiToolsLocale } from "#ui-tools/i18n";
|
|
6
|
+
import { useResponsiveValue } from "#ui-tools/shared/composables/use-responsive-value";
|
|
7
|
+
import { isNumber } from "#ui-tools/shared/utils/predicate";
|
|
8
|
+
import { resolveTextValue } from "#ui-tools/shared/utils/render";
|
|
9
|
+
import { useDashboardFormat } from "../composables/use-dashboard-format";
|
|
10
|
+
import { useDashboardUi } from "../composables/use-dashboard-ui";
|
|
11
|
+
import { toDashboardCell } from "../utils/export";
|
|
12
|
+
import { resolveDashboardClasses } from "../utils/ui";
|
|
13
|
+
import DashboardSkeleton from "./block/dashboard-skeleton.vue";
|
|
14
|
+
import DashboardCard from "./dashboard-card.vue";
|
|
15
|
+
const {
|
|
16
|
+
source,
|
|
17
|
+
card = true,
|
|
18
|
+
menu = void 0,
|
|
19
|
+
freshness = void 0,
|
|
20
|
+
items,
|
|
21
|
+
columns = "1 sm:2",
|
|
22
|
+
layout = "stacked",
|
|
23
|
+
ui,
|
|
24
|
+
...block
|
|
25
|
+
} = defineProps({
|
|
26
|
+
title: { type: [String, Number, Function], required: false },
|
|
27
|
+
subtitle: { type: [String, Number, Function], required: false },
|
|
28
|
+
size: { type: String, required: false },
|
|
29
|
+
card: { type: Boolean, required: false },
|
|
30
|
+
activation: { type: String, required: false },
|
|
31
|
+
empty: { type: Object, required: false },
|
|
32
|
+
menu: { type: [Boolean, Array, Function], required: false },
|
|
33
|
+
actions: { type: Array, required: false },
|
|
34
|
+
filters: { type: Array, required: false },
|
|
35
|
+
freshness: { type: Boolean, required: false },
|
|
36
|
+
source: { type: Object, required: true },
|
|
37
|
+
items: { type: Array, required: true },
|
|
38
|
+
columns: { type: String, required: false },
|
|
39
|
+
layout: { type: String, required: false },
|
|
40
|
+
ui: { type: Object, required: false }
|
|
41
|
+
});
|
|
42
|
+
defineSlots();
|
|
43
|
+
const { t } = useUiToolsLocale();
|
|
44
|
+
const formats = useDashboardFormat();
|
|
45
|
+
const appUi = useDashboardUi();
|
|
46
|
+
const clipboard = useClipboard({ copiedDuring: 1600, legacy: true });
|
|
47
|
+
const layouts = {
|
|
48
|
+
inline: {
|
|
49
|
+
item: "grid grid-cols-[minmax(7rem,38%)_minmax(0,1fr)] items-baseline gap-x-4",
|
|
50
|
+
label: "text-[12.5px]"
|
|
51
|
+
},
|
|
52
|
+
stacked: { item: "flex flex-col gap-1", label: "text-xs" }
|
|
53
|
+
};
|
|
54
|
+
const classes = computed(
|
|
55
|
+
() => resolveDashboardClasses(
|
|
56
|
+
{
|
|
57
|
+
copy: "ms-0.5 -my-1 opacity-0 transition-opacity group-hover/detail:opacity-100 focus-visible:opacity-100 data-[copied]:opacity-100",
|
|
58
|
+
grid: layout === "inline" ? "grid gap-x-8 gap-y-2.5" : "grid gap-x-8 gap-y-4",
|
|
59
|
+
hint: "mt-0.5 text-[11.5px] text-muted",
|
|
60
|
+
item: `group/detail min-w-0 ${layouts[layout].item}`,
|
|
61
|
+
label: `min-w-0 truncate text-muted ${layouts[layout].label}`,
|
|
62
|
+
link: "h-auto min-w-0 p-0 text-[length:inherit] font-normal underline decoration-primary/35 underline-offset-[3px] transition-colors hover:decoration-primary",
|
|
63
|
+
placeholder: "text-dimmed",
|
|
64
|
+
value: "flex min-w-0 items-center gap-1 text-[13.5px] leading-snug break-words text-highlighted"
|
|
65
|
+
},
|
|
66
|
+
appUi.value.details,
|
|
67
|
+
ui
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
const gridColumns = useResponsiveValue(() => columns, "grid-cols");
|
|
71
|
+
const entries = computed(() => {
|
|
72
|
+
const data = source.data;
|
|
73
|
+
if (data === void 0 || data === null) return [];
|
|
74
|
+
return items.filter((item) => !item.hidden?.(data)).map((item) => {
|
|
75
|
+
const raw = item.value(data);
|
|
76
|
+
const text = raw === null || raw === void 0 ? void 0 : isNumber(raw) ? formats.resolve(item.format)(raw) : resolveTextValue(raw);
|
|
77
|
+
const value = text === "" ? void 0 : text;
|
|
78
|
+
const hint = item.hint?.(data);
|
|
79
|
+
return {
|
|
80
|
+
data,
|
|
81
|
+
hint: hint === null || hint === void 0 ? void 0 : resolveTextValue(hint),
|
|
82
|
+
item,
|
|
83
|
+
key: item.key,
|
|
84
|
+
label: resolveTextValue(item.label),
|
|
85
|
+
placeholder: item.placeholder === void 0 ? "\u2014" : resolveTextValue(item.placeholder),
|
|
86
|
+
span: item.span === "full" ? "1 / -1" : item.span ? `span ${item.span}` : void 0,
|
|
87
|
+
target: value === void 0 ? void 0 : item.to?.(data) ?? void 0,
|
|
88
|
+
value
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
function copy(value) {
|
|
93
|
+
void clipboard.copy(value);
|
|
94
|
+
}
|
|
95
|
+
function tabulate() {
|
|
96
|
+
return {
|
|
97
|
+
columns: [
|
|
98
|
+
{ key: "label", label: t("dashboard.table.label"), numeric: false },
|
|
99
|
+
{ key: "value", label: t("dashboard.table.value"), numeric: false }
|
|
100
|
+
],
|
|
101
|
+
rows: entries.value.map((entry) => [
|
|
102
|
+
toDashboardCell(entry.label),
|
|
103
|
+
toDashboardCell(entry.value ?? "")
|
|
104
|
+
])
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<DashboardCard
|
|
111
|
+
v-bind="block"
|
|
112
|
+
:card
|
|
113
|
+
:menu
|
|
114
|
+
:freshness
|
|
115
|
+
:ui
|
|
116
|
+
:source
|
|
117
|
+
:is-empty="entries.length === 0"
|
|
118
|
+
:tabulate
|
|
119
|
+
>
|
|
120
|
+
<template #skeleton>
|
|
121
|
+
<DashboardSkeleton
|
|
122
|
+
kind="details"
|
|
123
|
+
:count="items.length"
|
|
124
|
+
:columns="layout === 'inline' ? 1 : 2"
|
|
125
|
+
/>
|
|
126
|
+
</template>
|
|
127
|
+
<template v-if="$slots['header-right']" #header-right>
|
|
128
|
+
<slot name="header-right" />
|
|
129
|
+
</template>
|
|
130
|
+
<template v-if="$slots.toolbar" #toolbar>
|
|
131
|
+
<slot name="toolbar" />
|
|
132
|
+
</template>
|
|
133
|
+
<template v-if="$slots.footer" #footer>
|
|
134
|
+
<slot name="footer" />
|
|
135
|
+
</template>
|
|
136
|
+
|
|
137
|
+
<dl :class="classes.grid" :style="gridColumns ?? void 0" :data-layout="layout">
|
|
138
|
+
<div
|
|
139
|
+
v-for="entry in entries"
|
|
140
|
+
:key="entry.key"
|
|
141
|
+
:class="classes.item"
|
|
142
|
+
:style="entry.span ? { gridColumn: entry.span } : void 0"
|
|
143
|
+
:data-empty="entry.value === void 0 ? '' : void 0"
|
|
144
|
+
data-details-item
|
|
145
|
+
>
|
|
146
|
+
<dt :class="classes.label">{{ entry.label }}</dt>
|
|
147
|
+
<dd class="m-0 min-w-0">
|
|
148
|
+
<div
|
|
149
|
+
:class="[classes.value, entry.item.mono && 'font-mono text-[12.5px] tracking-tight']"
|
|
150
|
+
>
|
|
151
|
+
<slot
|
|
152
|
+
:name="`item-${entry.key}`"
|
|
153
|
+
:data="entry.data"
|
|
154
|
+
:item="entry.item"
|
|
155
|
+
:value="entry.value"
|
|
156
|
+
>
|
|
157
|
+
<span v-if="entry.value === void 0" :class="classes.placeholder">
|
|
158
|
+
{{ entry.placeholder }}
|
|
159
|
+
</span>
|
|
160
|
+
<UButton
|
|
161
|
+
v-else-if="entry.target !== void 0"
|
|
162
|
+
:to="entry.target"
|
|
163
|
+
:label="entry.value"
|
|
164
|
+
color="primary"
|
|
165
|
+
variant="link"
|
|
166
|
+
:class="classes.link"
|
|
167
|
+
data-details-link
|
|
168
|
+
/>
|
|
169
|
+
<span v-else class="min-w-0">{{ entry.value }}</span>
|
|
170
|
+
</slot>
|
|
171
|
+
<UButton
|
|
172
|
+
v-if="entry.item.copy && entry.value !== void 0"
|
|
173
|
+
:icon="
|
|
174
|
+
clipboard.copied.value && clipboard.text.value === entry.value ? 'i-lucide-check' : 'i-lucide-copy'
|
|
175
|
+
"
|
|
176
|
+
:aria-label="
|
|
177
|
+
clipboard.copied.value && clipboard.text.value === entry.value ? t('dashboard.details.copied') : t('dashboard.details.copy')
|
|
178
|
+
"
|
|
179
|
+
:data-copied="
|
|
180
|
+
clipboard.copied.value && clipboard.text.value === entry.value || void 0
|
|
181
|
+
"
|
|
182
|
+
color="neutral"
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="xs"
|
|
185
|
+
:class="classes.copy"
|
|
186
|
+
@click="copy(entry.value)"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
<p v-if="entry.hint" :class="classes.hint">{{ entry.hint }}</p>
|
|
190
|
+
</dd>
|
|
191
|
+
</div>
|
|
192
|
+
</dl>
|
|
193
|
+
</DashboardCard>
|
|
194
|
+
</template>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DashboardBlockBaseProps, DashboardBlockUi, DashboardDetailsItem, DashboardDetailsUi, DashboardSourceLike } from '../types/index.js';
|
|
2
|
+
declare const __VLS_export: <TData>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
3
|
+
props: import("vue").PublicProps & __VLS_PrettifyLocal<Omit<DashboardBlockBaseProps, "ui"> & {
|
|
4
|
+
source: DashboardSourceLike<TData>;
|
|
5
|
+
/** Fields read from the source data, in display order. */
|
|
6
|
+
items: readonly DashboardDetailsItem<TData & ({} | null)>[];
|
|
7
|
+
/** Column count, responsive: `"1 md:2 xl:3"`. Defaults to `"1 sm:2"`. */
|
|
8
|
+
columns?: string;
|
|
9
|
+
/** `stacked`: each label over its value. `inline`: the label beside its value, on one row. */
|
|
10
|
+
layout?: "stacked" | "inline";
|
|
11
|
+
ui?: DashboardBlockUi & DashboardDetailsUi;
|
|
12
|
+
}> & (typeof globalThis extends {
|
|
13
|
+
__VLS_PROPS_FALLBACK: infer P;
|
|
14
|
+
} ? P : {});
|
|
15
|
+
expose: (exposed: {}) => void;
|
|
16
|
+
attrs: any;
|
|
17
|
+
slots: {
|
|
18
|
+
[x: `item-${string}`]: ((props: {
|
|
19
|
+
data: TData & ({} | null);
|
|
20
|
+
item: DashboardDetailsItem<TData & ({} | null)>;
|
|
21
|
+
value: string | undefined;
|
|
22
|
+
}) => unknown) | undefined;
|
|
23
|
+
} & {
|
|
24
|
+
'header-right'?: () => unknown;
|
|
25
|
+
toolbar?: () => unknown;
|
|
26
|
+
footer?: () => unknown;
|
|
27
|
+
};
|
|
28
|
+
emit: {};
|
|
29
|
+
}>) => import("vue").VNode & {
|
|
30
|
+
__ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
|
|
31
|
+
};
|
|
32
|
+
declare const _default: typeof __VLS_export;
|
|
33
|
+
export default _default;
|
|
34
|
+
type __VLS_PrettifyLocal<T> = (T extends any ? {
|
|
35
|
+
[K in keyof T]: T[K];
|
|
36
|
+
} : {
|
|
37
|
+
[K in keyof T as K]: T[K];
|
|
38
|
+
}) & {};
|
|
@@ -33,6 +33,14 @@ declare const __VLS_export: <TDashboard extends DashboardPageTarget>(__VLS_props
|
|
|
33
33
|
}) => unknown;
|
|
34
34
|
title?: () => unknown;
|
|
35
35
|
description?: () => unknown;
|
|
36
|
+
/** Above the title, across the header: breadcrumbs or a back link. */
|
|
37
|
+
eyebrow?: () => unknown;
|
|
38
|
+
/** Before the title and its line: an avatar or a logo, for a page about one entity. */
|
|
39
|
+
leading?: () => unknown;
|
|
40
|
+
/** A full-width row closing the header: key facts of the entity the page is about. */
|
|
41
|
+
details?: () => unknown;
|
|
42
|
+
/** Between the header and the pinned tabs: a note or a notice about the whole page. */
|
|
43
|
+
banner?: () => unknown;
|
|
36
44
|
/** Replaces the page actions and the refresh control. */
|
|
37
45
|
actions?: () => unknown;
|
|
38
46
|
/** Replaces the view tabs. */
|
|
@@ -43,9 +43,13 @@ const classes = computed(
|
|
|
43
43
|
{
|
|
44
44
|
actions: "flex min-h-[34px] items-center gap-2",
|
|
45
45
|
body: "flex flex-col gap-4 px-4 pt-1 pb-12 lg:gap-6 lg:px-7 lg:pb-15",
|
|
46
|
-
description: "col-span-full mt-1 text-[12.5px] font-light text-muted md:text-[13.5px]",
|
|
46
|
+
description: "col-span-full mt-1 text-[12.5px] font-light text-muted in-data-[leading]:col-start-2 md:text-[13.5px]",
|
|
47
|
+
banner: "px-4 pt-4 lg:px-7 lg:pt-6",
|
|
48
|
+
details: "col-span-full mt-4 lg:mt-5",
|
|
49
|
+
eyebrow: "col-span-full mb-2 min-w-0",
|
|
47
50
|
filters: "px-4 py-3 lg:px-7 lg:py-4",
|
|
48
|
-
header: "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-4 bg-default px-4 pt-4 pb-3.5 lg:px-7 lg:pt-[22px] lg:pb-[18px]",
|
|
51
|
+
header: "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-4 bg-default px-4 pt-4 pb-3.5 data-[leading]:grid-cols-[auto_minmax(0,1fr)_auto] lg:px-7 lg:pt-[22px] lg:pb-[18px]",
|
|
52
|
+
leading: "row-span-2 flex items-center self-center",
|
|
49
53
|
root: "h-full overflow-auto bg-muted",
|
|
50
54
|
tabs: "bg-default px-2 lg:px-7",
|
|
51
55
|
title: "m-0 min-w-0 truncate text-2xl leading-[1.1] font-light tracking-[-0.04em] text-highlighted md:text-[30px]",
|
|
@@ -102,7 +106,13 @@ function actionUi(action) {
|
|
|
102
106
|
<template>
|
|
103
107
|
<!-- Without a dashboard (its schema threw during setup), render nothing: that error stays the one reported. -->
|
|
104
108
|
<div v-if="dashboard" :class="classes.root" data-dashboard-page>
|
|
105
|
-
<header :class="classes.header">
|
|
109
|
+
<header :class="classes.header" :data-leading="$slots.leading ? '' : void 0">
|
|
110
|
+
<div v-if="$slots.eyebrow" :class="classes.eyebrow">
|
|
111
|
+
<slot name="eyebrow" />
|
|
112
|
+
</div>
|
|
113
|
+
<div v-if="$slots.leading" :class="classes.leading">
|
|
114
|
+
<slot name="leading" />
|
|
115
|
+
</div>
|
|
106
116
|
<h1 :class="classes.title">
|
|
107
117
|
<slot name="title">{{ title === void 0 ? "" : resolveTextValue(title) }}</slot>
|
|
108
118
|
</h1>
|
|
@@ -133,8 +143,15 @@ function actionUi(action) {
|
|
|
133
143
|
</template>
|
|
134
144
|
</slot>
|
|
135
145
|
</p>
|
|
146
|
+
<div v-if="$slots.details" :class="classes.details">
|
|
147
|
+
<slot name="details" />
|
|
148
|
+
</div>
|
|
136
149
|
</header>
|
|
137
150
|
|
|
151
|
+
<div v-if="$slots.banner" :class="classes.banner">
|
|
152
|
+
<slot name="banner" />
|
|
153
|
+
</div>
|
|
154
|
+
|
|
138
155
|
<div ref="marker" aria-hidden="true" />
|
|
139
156
|
<div :class="classes.toolbar" :data-stuck="stuck || void 0">
|
|
140
157
|
<slot name="tabs">
|
|
@@ -33,6 +33,14 @@ declare const __VLS_export: <TDashboard extends DashboardPageTarget>(__VLS_props
|
|
|
33
33
|
}) => unknown;
|
|
34
34
|
title?: () => unknown;
|
|
35
35
|
description?: () => unknown;
|
|
36
|
+
/** Above the title, across the header: breadcrumbs or a back link. */
|
|
37
|
+
eyebrow?: () => unknown;
|
|
38
|
+
/** Before the title and its line: an avatar or a logo, for a page about one entity. */
|
|
39
|
+
leading?: () => unknown;
|
|
40
|
+
/** A full-width row closing the header: key facts of the entity the page is about. */
|
|
41
|
+
details?: () => unknown;
|
|
42
|
+
/** Between the header and the pinned tabs: a note or a notice about the whole page. */
|
|
43
|
+
banner?: () => unknown;
|
|
36
44
|
/** Replaces the page actions and the refresh control. */
|
|
37
45
|
actions?: () => unknown;
|
|
38
46
|
/** Replaces the view tabs. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DashboardViewController, DashboardViewTabsUi } from '../types/index.js';
|
|
1
|
+
import type { DashboardViewController, DashboardViewItem, DashboardViewTabsUi } from '../types/index.js';
|
|
2
2
|
declare const __VLS_export: <TKey extends string>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
3
3
|
props: import("vue").PublicProps & __VLS_PrettifyLocal<{
|
|
4
4
|
dashboard: {
|
|
@@ -13,10 +13,7 @@ declare const __VLS_export: <TKey extends string>(__VLS_props: NonNullable<Await
|
|
|
13
13
|
slots: {
|
|
14
14
|
/** Content of one tab. */
|
|
15
15
|
tab?: (props: {
|
|
16
|
-
item:
|
|
17
|
-
value: TKey;
|
|
18
|
-
label: string;
|
|
19
|
-
};
|
|
16
|
+
item: DashboardViewItem<TKey>;
|
|
20
17
|
active: boolean;
|
|
21
18
|
}) => unknown;
|
|
22
19
|
};
|
|
@@ -15,8 +15,9 @@ const strip = useTemplateRef("strip");
|
|
|
15
15
|
const classes = computed(
|
|
16
16
|
() => resolveDashboardClasses(
|
|
17
17
|
{
|
|
18
|
+
badge: "ms-1.5 inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-elevated px-1.5 text-[11px] leading-none font-semibold text-muted tabular-nums group-data-[active]/tab:bg-primary/12 group-data-[active]/tab:text-primary",
|
|
18
19
|
root: "flex min-w-0 gap-0.5 overflow-x-auto border-b border-default [scrollbar-width:none]",
|
|
19
|
-
tab: "relative inline-flex h-10 flex-none items-center px-3 text-sm font-medium whitespace-nowrap text-muted transition-colors outline-none hover:text-default focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset data-[active]:text-highlighted data-[active]:after:absolute data-[active]:after:inset-x-2 data-[active]:after:-bottom-px data-[active]:after:h-0.5 data-[active]:after:rounded-full data-[active]:after:bg-primary"
|
|
20
|
+
tab: "group/tab relative inline-flex h-10 flex-none items-center px-3 text-sm font-medium whitespace-nowrap text-muted transition-colors outline-none hover:text-default focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset data-[active]:text-highlighted data-[active]:after:absolute data-[active]:after:inset-x-2 data-[active]:after:-bottom-px data-[active]:after:h-0.5 data-[active]:after:rounded-full data-[active]:after:bg-primary"
|
|
20
21
|
},
|
|
21
22
|
appUi.value.viewTabs,
|
|
22
23
|
ui
|
|
@@ -60,6 +61,9 @@ watch(
|
|
|
60
61
|
>
|
|
61
62
|
<slot name="tab" :item :active="dashboard.view.current === item.value">
|
|
62
63
|
{{ item.label }}
|
|
64
|
+
<span v-if="item.badge !== void 0" :class="classes.badge" data-dashboard-view-badge>
|
|
65
|
+
{{ item.badge }}
|
|
66
|
+
</span>
|
|
63
67
|
</slot>
|
|
64
68
|
</button>
|
|
65
69
|
</nav>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DashboardViewController, DashboardViewTabsUi } from '../types/index.js';
|
|
1
|
+
import type { DashboardViewController, DashboardViewItem, DashboardViewTabsUi } from '../types/index.js';
|
|
2
2
|
declare const __VLS_export: <TKey extends string>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
3
3
|
props: import("vue").PublicProps & __VLS_PrettifyLocal<{
|
|
4
4
|
dashboard: {
|
|
@@ -13,10 +13,7 @@ declare const __VLS_export: <TKey extends string>(__VLS_props: NonNullable<Await
|
|
|
13
13
|
slots: {
|
|
14
14
|
/** Content of one tab. */
|
|
15
15
|
tab?: (props: {
|
|
16
|
-
item:
|
|
17
|
-
value: TKey;
|
|
18
|
-
label: string;
|
|
19
|
-
};
|
|
16
|
+
item: DashboardViewItem<TKey>;
|
|
20
17
|
active: boolean;
|
|
21
18
|
}) => unknown;
|
|
22
19
|
};
|
|
@@ -25,6 +25,9 @@ export function useDashboardApi(params) {
|
|
|
25
25
|
get label() {
|
|
26
26
|
return view.label();
|
|
27
27
|
},
|
|
28
|
+
get badge() {
|
|
29
|
+
return view.badge();
|
|
30
|
+
},
|
|
28
31
|
get opened() {
|
|
29
32
|
return view.opened.value;
|
|
30
33
|
}
|
|
@@ -91,7 +94,7 @@ export function useDashboardApi(params) {
|
|
|
91
94
|
},
|
|
92
95
|
get items() {
|
|
93
96
|
return views.views.flatMap(
|
|
94
|
-
(view) => view.enabled.value ? [{ label: view.label(), value: view.key }] : []
|
|
97
|
+
(view) => view.enabled.value ? [{ badge: view.badge(), label: view.label(), value: view.key }] : []
|
|
95
98
|
);
|
|
96
99
|
}
|
|
97
100
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComputedRef } from 'vue';
|
|
2
|
-
import type { DashboardSourceLike } from '../types/index.js';
|
|
2
|
+
import type { DashboardSourceLike, DashboardViewBadge } from '../types/index.js';
|
|
3
3
|
import type { DashboardRuntimeSchema } from '../types/runtime.js';
|
|
4
4
|
import type { DashboardEnvironment } from '../utils/environment.js';
|
|
5
5
|
import type { DashboardReadTracker } from '../utils/tracker.js';
|
|
@@ -23,6 +23,7 @@ export declare function useDashboardViews(params: {
|
|
|
23
23
|
current: import("vue").WritableComputedRef<string, string>;
|
|
24
24
|
keys: string[];
|
|
25
25
|
views: {
|
|
26
|
+
badge: () => string | number | undefined;
|
|
26
27
|
enabled: ComputedRef<boolean>;
|
|
27
28
|
input: import("../types/runtime.js").DashboardRuntimeScopeInput;
|
|
28
29
|
key: string;
|
|
@@ -54,5 +55,7 @@ export declare function useDashboardViews(params: {
|
|
|
54
55
|
};
|
|
55
56
|
}[];
|
|
56
57
|
};
|
|
58
|
+
/** A badge worth showing: a non-zero finite number or a non-empty text. */
|
|
59
|
+
export declare function resolveDashboardViewBadge(value: DashboardViewBadge): string | number | undefined;
|
|
57
60
|
/** No filter may take the URL key of the current view. */
|
|
58
61
|
export declare function assertDashboardViewUrlKeys(owner: string, urlKeys: readonly string[]): void;
|
|
@@ -36,6 +36,15 @@ export function useDashboardViews(params) {
|
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
38
|
const rootSources = new Map(params.root.members);
|
|
39
|
+
const rootData = Object.defineProperties(
|
|
40
|
+
{},
|
|
41
|
+
Object.fromEntries(
|
|
42
|
+
[...rootSources].map(([key, source]) => [key, { enumerable: true, get: () => source.data }])
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
const badges = computed(
|
|
46
|
+
() => schema.badges?.({ data: rootData, filters: params.root.filterScope.values }) ?? {}
|
|
47
|
+
);
|
|
39
48
|
const views = schema.views.map(([key, input]) => {
|
|
40
49
|
const enabled = conditions.get(key) ?? computed(() => true);
|
|
41
50
|
const opened = shallowRef(false);
|
|
@@ -60,10 +69,15 @@ export function useDashboardViews(params) {
|
|
|
60
69
|
});
|
|
61
70
|
assertDashboardViewUrlKeys(key, scope.filterScope.urlKeys);
|
|
62
71
|
const label = () => resolveTextValue(input.label, key);
|
|
63
|
-
|
|
72
|
+
const badge = () => resolveDashboardViewBadge(badges.value[key]);
|
|
73
|
+
return { badge, enabled, input, key, label, opened, scope };
|
|
64
74
|
});
|
|
65
75
|
return { current, keys, views };
|
|
66
76
|
}
|
|
77
|
+
export function resolveDashboardViewBadge(value) {
|
|
78
|
+
if (typeof value === "number") return Number.isFinite(value) && value !== 0 ? value : void 0;
|
|
79
|
+
return value ? value : void 0;
|
|
80
|
+
}
|
|
67
81
|
export function assertDashboardViewUrlKeys(owner, urlKeys) {
|
|
68
82
|
if (urlKeys.includes(DASHBOARD_VIEW_URL_KEY))
|
|
69
83
|
throw new Error(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LazyTextValue } from '../../shared/types/utils.js';
|
|
2
|
-
import type { DashboardCondition, DashboardDeriveMap, DashboardEmptyMap, DashboardFilterMap, DashboardKeyError, DashboardReservedKey, DashboardSchema, DashboardScopeGuard, DashboardScopeInput, DashboardSourceMap, DashboardView, DashboardViewMap } from '../types/index.js';
|
|
2
|
+
import type { DashboardCondition, DashboardDeriveMap, DashboardEmptyMap, DashboardFilterMap, DashboardKeyError, DashboardReservedKey, DashboardSchema, DashboardScopeGuard, DashboardScopeInput, DashboardSourceMap, DashboardView, DashboardBadgesContext, DashboardDerivedResources, DashboardViewBadges, DashboardViewMap } from '../types/index.js';
|
|
3
3
|
type DashboardViewsGuard<TQueries, TDerive, TViews> = [
|
|
4
4
|
Extract<keyof TViews, DashboardReservedKey | keyof TQueries | keyof TDerive>
|
|
5
5
|
] extends [never] ? unknown : {
|
|
@@ -47,6 +47,20 @@ export declare function defineDashboardSchema<const TFilters extends DashboardFi
|
|
|
47
47
|
views?: TViews;
|
|
48
48
|
/** View shown when the URL does not select one. Defaults to the first declared view. */
|
|
49
49
|
defaultView?: NoInfer<keyof TViews & string>;
|
|
50
|
+
/**
|
|
51
|
+
* Counts or short texts shown after the view tabs' labels, read from the root queries and
|
|
52
|
+
* derived values, so a tab can count what waits in it before it is opened. Read lazily: badges
|
|
53
|
+
* follow the data. `null`, `undefined`, `0`, and `''` show nothing.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* badges: ({ data }) => ({
|
|
58
|
+
* invoices: data.account?.unpaidInvoices,
|
|
59
|
+
* access: data.account?.pendingInvitations,
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
badges?: (context: DashboardBadgesContext<TFilters, TQueries & DashboardDerivedResources<TDerive>>) => DashboardViewBadges<NoInfer<keyof TViews & string>>;
|
|
50
64
|
} & DashboardScopeInput<TFilters, TQueries, TDerive> & DashboardScopeGuard<TQueries, TDerive> & DashboardViewsGuard<TQueries, TDerive, TViews>): DashboardSchema<NoInfer<TFilters>, NoInfer<TQueries>, NoInfer<TDerive>, NoInfer<TViews>>;
|
|
51
65
|
/**
|
|
52
66
|
* Declares one view (tab), self-contained: its label, its filters, its queries and derived values.
|
|
@@ -86,7 +86,7 @@ export interface DashboardReferenceLine {
|
|
|
86
86
|
position?: 'start' | 'end';
|
|
87
87
|
}
|
|
88
88
|
/** Shape of the loading skeleton, one per block family. */
|
|
89
|
-
export type DashboardSkeletonKind = 'stat' | 'bars' | 'lines' | 'donut' | 'rows' | 'hbars' | 'funnel' | 'stack' | 'paired' | 'feed' | 'table' | 'stats' | 'gauge';
|
|
89
|
+
export type DashboardSkeletonKind = 'stat' | 'bars' | 'lines' | 'donut' | 'rows' | 'hbars' | 'funnel' | 'stack' | 'paired' | 'feed' | 'table' | 'stats' | 'gauge' | 'details';
|
|
90
90
|
/**
|
|
91
91
|
* Built-in card menu actions: `table` switches the content to a data table, `csv` downloads that
|
|
92
92
|
* data, `expand` opens the card in a large dialog.
|
|
@@ -259,6 +259,32 @@ export interface DashboardStatsItem<TData> {
|
|
|
259
259
|
/** Badge next to the value ("Good", "At risk"). `null` hides it. */
|
|
260
260
|
status?: (data: TData) => DashboardStatus | null | undefined;
|
|
261
261
|
}
|
|
262
|
+
/** One field of `UiDashboardDetails`: a label over its value, read from the source data. */
|
|
263
|
+
export interface DashboardDetailsItem<TData> {
|
|
264
|
+
/** Stable identity, and the name of its slot: `#item-vatNumber`. */
|
|
265
|
+
key: string;
|
|
266
|
+
label: LazyTextValue;
|
|
267
|
+
/**
|
|
268
|
+
* Numbers go through `format`. `null`, `undefined`, and `''` read as empty: a muted dash, or
|
|
269
|
+
* `placeholder`.
|
|
270
|
+
*/
|
|
271
|
+
value: (data: TData) => LazyTextValue | number | null | undefined;
|
|
272
|
+
format?: DashboardValueFormat;
|
|
273
|
+
/** Text under the value (a date of change, a unit, a source). */
|
|
274
|
+
hint?: (data: TData) => LazyTextValue | null | undefined;
|
|
275
|
+
/** Shown when the value is empty. Defaults to a dash. */
|
|
276
|
+
placeholder?: LazyTextValue;
|
|
277
|
+
/** Monospace value, for identifiers and codes. */
|
|
278
|
+
mono?: boolean;
|
|
279
|
+
/** A copy button after a non-empty value, for identifiers people paste elsewhere. */
|
|
280
|
+
copy?: boolean;
|
|
281
|
+
/** Makes the value a link. */
|
|
282
|
+
to?: (data: TData) => RouteLocationRaw | null | undefined;
|
|
283
|
+
/** Columns the field spans: a number, or `'full'` for the whole row. */
|
|
284
|
+
span?: number | 'full';
|
|
285
|
+
/** Leaves the field out, e.g. a field that only applies to some records. */
|
|
286
|
+
hidden?: (data: TData) => boolean;
|
|
287
|
+
}
|
|
262
288
|
/** One tab of `UiDashboardTabs`. */
|
|
263
289
|
export interface DashboardTab<TValue extends string | number = string | number> {
|
|
264
290
|
value: TValue;
|
|
@@ -4,6 +4,7 @@ import type { LazyTextValue } from '../../shared/types/utils.js';
|
|
|
4
4
|
import type { DashboardOptionsMenuBindings } from './controls.js';
|
|
5
5
|
import type { DashboardFilterBuilder, DashboardFilterLike, DashboardOption } from './filters.js';
|
|
6
6
|
import type { DashboardCondition, DashboardSourceLike, DashboardStage } from './resource.js';
|
|
7
|
+
import type { DashboardViewBadge } from './schema.js';
|
|
7
8
|
/**
|
|
8
9
|
* Runtime (type-erased) view of the schema. The public schema types carry precise generics; the
|
|
9
10
|
* runtime reads the same objects through these shapes after one normalization step.
|
|
@@ -48,6 +49,8 @@ export interface DashboardRuntimeSchema extends DashboardRuntimeScopeInput {
|
|
|
48
49
|
defaultView?: string;
|
|
49
50
|
/** Views in declaration order. Each input is the object the schema declared. */
|
|
50
51
|
views: [key: string, view: DashboardRuntimeScopeInput][];
|
|
52
|
+
/** Badges of the view tabs, read from the root data. */
|
|
53
|
+
badges?: (context: DashboardRuntimeDeriveContext) => Record<string, DashboardViewBadge>;
|
|
51
54
|
}
|
|
52
55
|
/**
|
|
53
56
|
* Option list behind one filter control, static or remote. Both option composables return this
|
|
@@ -78,6 +78,7 @@ export interface DashboardSchema<TFilters extends DashboardFilterMap = Dashboard
|
|
|
78
78
|
readonly derive?: (context: never) => TDerive;
|
|
79
79
|
readonly views?: TViews;
|
|
80
80
|
readonly defaultView?: keyof TViews & string;
|
|
81
|
+
readonly badges?: (context: never) => DashboardViewBadges<keyof TViews & string>;
|
|
81
82
|
}
|
|
82
83
|
/** Structural constraint every schema returned by `defineDashboardSchema` satisfies. */
|
|
83
84
|
export interface DashboardSchemaLike {
|
|
@@ -89,6 +90,7 @@ export interface DashboardSchemaLike {
|
|
|
89
90
|
readonly derive?: (context: never) => DashboardDeriveMap;
|
|
90
91
|
readonly views?: DashboardViewMap;
|
|
91
92
|
readonly defaultView?: string;
|
|
93
|
+
readonly badges?: (context: never) => DashboardViewBadges<string>;
|
|
92
94
|
}
|
|
93
95
|
/** Structural constraint every view returned by `defineDashboardView` satisfies. */
|
|
94
96
|
export interface DashboardViewLike {
|
|
@@ -152,9 +154,29 @@ export interface DashboardScopeMembers<TFilters> {
|
|
|
152
154
|
/** Refetches every active query of this scope. */
|
|
153
155
|
refresh(): Promise<void>;
|
|
154
156
|
}
|
|
157
|
+
/** What a view's `badge` returns: a count or a short text; empty values show nothing. */
|
|
158
|
+
export type DashboardViewBadge = number | string | null | undefined;
|
|
159
|
+
/** Badges of a dashboard's views, by view key; a view left out shows none. */
|
|
160
|
+
export type DashboardViewBadges<TKey extends string> = {
|
|
161
|
+
readonly [K in TKey]?: DashboardViewBadge;
|
|
162
|
+
};
|
|
163
|
+
/** Context received by a schema's `badges`: the data of its root queries and derived values. */
|
|
164
|
+
export interface DashboardBadgesContext<TFilters, TSources> {
|
|
165
|
+
data: DashboardSourceData<TSources>;
|
|
166
|
+
filters: Readonly<DashboardFilterValues<TFilters>>;
|
|
167
|
+
}
|
|
168
|
+
/** One tab of a dashboard's views. */
|
|
169
|
+
export interface DashboardViewItem<TKey extends string> {
|
|
170
|
+
value: TKey;
|
|
171
|
+
label: string;
|
|
172
|
+
/** The view's badge (see the schema's `badges`), when it shows something. */
|
|
173
|
+
badge?: number | string;
|
|
174
|
+
}
|
|
155
175
|
export interface DashboardViewMeta<TKey extends string> {
|
|
156
176
|
readonly key: TKey;
|
|
157
177
|
readonly label: string;
|
|
178
|
+
/** The view's badge (see the schema's `badges`), when it shows something. */
|
|
179
|
+
readonly badge: number | string | undefined;
|
|
158
180
|
/** The view is the current one. */
|
|
159
181
|
readonly active: boolean;
|
|
160
182
|
/** The view's `enabled` condition holds. A disabled view has no tab and cannot be current. */
|
|
@@ -170,10 +192,7 @@ export interface DashboardViewController<TKey extends string> {
|
|
|
170
192
|
*/
|
|
171
193
|
current: TKey;
|
|
172
194
|
/** Enabled views in declaration order, ready for `UTabs` items. */
|
|
173
|
-
readonly items: readonly
|
|
174
|
-
value: TKey;
|
|
175
|
-
label: string;
|
|
176
|
-
}[];
|
|
195
|
+
readonly items: readonly DashboardViewItem<TKey>[];
|
|
177
196
|
}
|
|
178
197
|
/** Handle of one view: its queries, derived values and filters (`consumption.filters.year`). */
|
|
179
198
|
export type DashboardViewHandle<TView, TKey extends string> = Prettify<InferQueries<TView> & DashboardDerivedResources<InferDerive<TView>> & DashboardScopeMembers<InferFilters<TView>> & {
|
|
@@ -46,6 +46,21 @@ export interface DashboardStatUi {
|
|
|
46
46
|
/** Line under the progress bar: goal and completion. */
|
|
47
47
|
goal?: string;
|
|
48
48
|
}
|
|
49
|
+
export interface DashboardDetailsUi {
|
|
50
|
+
/** The description list: a grid of fields. */
|
|
51
|
+
grid?: string;
|
|
52
|
+
/** One field; carries `data-empty` while its value is empty. */
|
|
53
|
+
item?: string;
|
|
54
|
+
label?: string;
|
|
55
|
+
value?: string;
|
|
56
|
+
/** Dash or `placeholder` of an empty value. */
|
|
57
|
+
placeholder?: string;
|
|
58
|
+
hint?: string;
|
|
59
|
+
/** Link of an item with `to`. */
|
|
60
|
+
link?: string;
|
|
61
|
+
/** Copy button of an item with `copy`. */
|
|
62
|
+
copy?: string;
|
|
63
|
+
}
|
|
49
64
|
export interface DashboardStatsUi {
|
|
50
65
|
/** Grid of the figures. */
|
|
51
66
|
grid?: string;
|
|
@@ -219,12 +234,21 @@ export interface DashboardChipsUi {
|
|
|
219
234
|
export interface DashboardPageUi {
|
|
220
235
|
/** The page, and its scroll container: the tabs and filters stick to its top. */
|
|
221
236
|
root?: string;
|
|
237
|
+
/** Carries `data-leading` while the `#leading` slot is filled. */
|
|
222
238
|
header?: string;
|
|
239
|
+
/** Around the `#eyebrow` slot, above the title. */
|
|
240
|
+
eyebrow?: string;
|
|
241
|
+
/** Around the `#leading` slot (an avatar or a logo), beside the title and its line. */
|
|
242
|
+
leading?: string;
|
|
223
243
|
title?: string;
|
|
224
244
|
/** Today's date and when the data was fetched, under the title. */
|
|
225
245
|
description?: string;
|
|
226
246
|
/** Page actions and the refresh control. */
|
|
227
247
|
actions?: string;
|
|
248
|
+
/** Around the `#banner` slot, between the header and the pinned band. */
|
|
249
|
+
banner?: string;
|
|
250
|
+
/** Around the `#details` slot: a full-width row closing the header (key facts of an entity). */
|
|
251
|
+
details?: string;
|
|
228
252
|
/** Pinned band of the tabs and the filter bar. Carries `data-stuck` once content scrolls under it. */
|
|
229
253
|
toolbar?: string;
|
|
230
254
|
tabs?: string;
|
|
@@ -237,6 +261,8 @@ export interface DashboardViewTabsUi {
|
|
|
237
261
|
/** The strip: it scrolls sideways when the tabs overflow. */
|
|
238
262
|
root?: string;
|
|
239
263
|
tab?: string;
|
|
264
|
+
/** Count or text of a view's `badge`, after the tab label. */
|
|
265
|
+
badge?: string;
|
|
240
266
|
}
|
|
241
267
|
/**
|
|
242
268
|
* App-wide dashboard theming, read from `appConfig.nuxtUiTools.dashboard`.
|
|
@@ -258,6 +284,7 @@ export interface DashboardUiConfig {
|
|
|
258
284
|
grid?: DashboardGridUi;
|
|
259
285
|
stat?: DashboardStatUi;
|
|
260
286
|
stats?: DashboardStatsUi;
|
|
287
|
+
details?: DashboardDetailsUi;
|
|
261
288
|
gauge?: DashboardGaugeUi;
|
|
262
289
|
legend?: DashboardLegendUi;
|
|
263
290
|
total?: DashboardTotalUi;
|
|
@@ -16,6 +16,7 @@ export function resolveDashboardRuntimeSchema(schema) {
|
|
|
16
16
|
const views = Object.entries(input.views ?? {});
|
|
17
17
|
return {
|
|
18
18
|
autoRefresh: input.autoRefresh,
|
|
19
|
+
badges: input.badges,
|
|
19
20
|
defaultView: input.defaultView ?? views[0]?.[0],
|
|
20
21
|
derive: input.derive,
|
|
21
22
|
filters: input.filters,
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { InjectionKey } from 'vue';
|
|
1
2
|
import type { FormApiController } from '../types/index.js';
|
|
3
|
+
export declare const formApiKey: InjectionKey<FormApiController>;
|
|
4
|
+
/**
|
|
5
|
+
* Provides the form API to a subtree. Inside a Nuxt app it reuses the app-wide instance the module
|
|
6
|
+
* plugin exposes as `useNuxtApp().$formApi`, so forms opened from actions outside setup render in
|
|
7
|
+
* this `<NutFormProvider>`.
|
|
8
|
+
*/
|
|
2
9
|
export declare function provideFormApi(): FormApiController;
|
|
3
10
|
export declare function useFormApi(): FormApiController;
|
|
11
|
+
export declare function createFormApi(): FormApiController;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { computed, inject, provide, ref } from "vue";
|
|
2
2
|
import { isRecord } from "../utils/path.js";
|
|
3
3
|
import { isFunction, isString } from "../utils/predicate.js";
|
|
4
|
-
const formApiKey = Symbol("nuxt-ui-tools-form-api");
|
|
4
|
+
export const formApiKey = Symbol("nuxt-ui-tools-form-api");
|
|
5
5
|
export function provideFormApi() {
|
|
6
|
-
const api = createFormApi();
|
|
6
|
+
const api = inject(formApiKey, null) ?? createFormApi();
|
|
7
7
|
provide(formApiKey, api);
|
|
8
8
|
return api;
|
|
9
9
|
}
|
|
10
10
|
export function useFormApi() {
|
|
11
|
-
const api = inject(formApiKey);
|
|
11
|
+
const api = inject(formApiKey, null);
|
|
12
12
|
if (!api) {
|
|
13
13
|
throw new Error("Form API is not provided. Wrap your app with <NutFormProvider>.");
|
|
14
14
|
}
|
|
15
15
|
return api;
|
|
16
16
|
}
|
|
17
|
-
function createFormApi() {
|
|
17
|
+
export function createFormApi() {
|
|
18
18
|
const formInstances = ref([]);
|
|
19
19
|
const controllers = /* @__PURE__ */ new Map();
|
|
20
20
|
const runtimeControls = /* @__PURE__ */ new Map();
|
|
@@ -32,6 +32,9 @@ export function useChoiceCard(params) {
|
|
|
32
32
|
corner.value ? "relative" : void 0,
|
|
33
33
|
current?.item
|
|
34
34
|
),
|
|
35
|
+
// The card label renders the option icon itself (inline or as a tile): Nuxt UI's own copy
|
|
36
|
+
// would show it twice.
|
|
37
|
+
icon: mergeFormUiClass("hidden", current?.icon),
|
|
35
38
|
// A tile stacks above the title, and the description below it, with gaps.
|
|
36
39
|
label: mergeFormUiClass(
|
|
37
40
|
tile.value ? "flex flex-col items-start gap-2.5 font-semibold" : void 0,
|
|
@@ -39,7 +42,7 @@ export function useChoiceCard(params) {
|
|
|
39
42
|
),
|
|
40
43
|
// A hidden indicator centers the text: keep it aligned, and clear of an inline corner check.
|
|
41
44
|
wrapper: mergeFormUiClass(
|
|
42
|
-
tile.value ? "flex flex-col gap-1.5" : void 0,
|
|
45
|
+
tile.value ? "flex flex-col items-start gap-1.5" : void 0,
|
|
43
46
|
[
|
|
44
47
|
indicator.value === "hidden" ? "ms-0 text-start" : "",
|
|
45
48
|
corner.value && !tile.value ? "pe-7" : ""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One form API for the whole app: components reach it with `useFormApi()`, and code running outside
|
|
3
|
+
* setup (actions, stores) with `useNuxtApp().$formApi`. `<NutFormProvider>` renders its overlays.
|
|
4
|
+
*/
|
|
5
|
+
declare const _default: import("#app/nuxt").Plugin<{
|
|
6
|
+
formApi: import("..").FormApiController;
|
|
7
|
+
}> & import("#app/nuxt").ObjectPlugin<{
|
|
8
|
+
formApi: import("..").FormApiController;
|
|
9
|
+
}>;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineNuxtPlugin } from "#app";
|
|
2
|
+
import { createFormApi, formApiKey } from "../composables/use-form-api.js";
|
|
3
|
+
export default defineNuxtPlugin({
|
|
4
|
+
name: "nuxt-ui-tools:form-api",
|
|
5
|
+
setup(nuxtApp) {
|
|
6
|
+
const formApi = createFormApi();
|
|
7
|
+
nuxtApp.vueApp.provide(formApiKey, formApi);
|
|
8
|
+
return { provide: { formApi } };
|
|
9
|
+
}
|
|
10
|
+
});
|
|
@@ -512,6 +512,12 @@ export interface UiToolsDashboardMessages {
|
|
|
512
512
|
};
|
|
513
513
|
/** Accessible name of a row's `⋮` menu. */
|
|
514
514
|
rowActions: string;
|
|
515
|
+
details: {
|
|
516
|
+
/** Accessible name of an item's copy button. */
|
|
517
|
+
copy: string;
|
|
518
|
+
/** Announced once the value is on the clipboard. */
|
|
519
|
+
copied: string;
|
|
520
|
+
};
|
|
515
521
|
alerts: {
|
|
516
522
|
empty: string;
|
|
517
523
|
severity: {
|
|
@@ -730,9 +730,7 @@ defineExpose({ resetColumnSizing });
|
|
|
730
730
|
</tbody>
|
|
731
731
|
|
|
732
732
|
<tfoot
|
|
733
|
-
v-if="
|
|
734
|
-
footerRows.length > 0 && !isFirstLoad && !error && (hasDeclarativeSummaries || !empty)
|
|
735
|
-
"
|
|
733
|
+
v-if="footerRows.length > 0 && rows.length > 0 && !isFirstLoad && !error"
|
|
736
734
|
:class="mergeDataListUiClass('nut-dl-table__foot', void 0, ui?.tfoot)"
|
|
737
735
|
>
|
|
738
736
|
<tr v-for="summaryRow in footerRows" :key="summaryRow" class="nut-dl-table__foot-row">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ui-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Nuxt module and runtime toolkit for UI-heavy Nuxt applications",
|
|
5
5
|
"homepage": "https://github.com/ChronicStone/nuxt-ui-tools#readme",
|
|
6
6
|
"bugs": "https://github.com/ChronicStone/nuxt-ui-tools/issues",
|