@revenexx/studio-shared 0.3.1 → 0.3.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/dist/module.d.mts +4 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +9 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.d.vue.ts +15 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +39 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue.d.ts +15 -0
- package/dist/runtime/components/Dashboard/DashboardControls.d.vue.ts +57 -0
- package/dist/runtime/components/Dashboard/DashboardControls.vue +209 -0
- package/dist/runtime/components/Dashboard/DashboardControls.vue.d.ts +57 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.d.vue.ts +16 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.vue +87 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.vue.d.ts +16 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +61 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.d.vue.ts +14 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +100 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue.d.ts +14 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.d.vue.ts +27 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +155 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.vue.d.ts +27 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.d.vue.ts +59 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.vue +79 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.vue.d.ts +59 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.d.vue.ts +42 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +153 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue.d.ts +42 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +27 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +80 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +27 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.d.vue.ts +6 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue +30 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue.d.ts +6 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.d.vue.ts +8 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue +13 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue.d.ts +8 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.d.vue.ts +23 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue +140 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue.d.ts +23 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.d.vue.ts +22 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue +124 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue.d.ts +22 -0
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +39 -3
- package/dist/runtime/components/DataTable/DataTable.vue +306 -32
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +39 -3
- package/dist/runtime/components/DataTable/FacetChips.d.vue.ts +16 -0
- package/dist/runtime/components/DataTable/FacetChips.vue +72 -0
- package/dist/runtime/components/DataTable/FacetChips.vue.d.ts +16 -0
- package/dist/runtime/components/DataTable/FacetPanel.d.vue.ts +20 -0
- package/dist/runtime/components/DataTable/FacetPanel.vue +359 -0
- package/dist/runtime/components/DataTable/FacetPanel.vue.d.ts +20 -0
- package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +5 -1
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue +83 -63
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +5 -1
- package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +5 -1
- package/dist/runtime/components/DataTable/SortMenu.vue +40 -12
- package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +5 -1
- package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +2 -2
- package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +2 -2
- package/dist/runtime/components/Media/MediaUploader.d.vue.ts +2 -2
- package/dist/runtime/components/Media/MediaUploader.vue.d.ts +2 -2
- package/dist/runtime/components/Shared/SortHeader.vue +6 -2
- package/dist/runtime/components/Shared/TabBar.d.vue.ts +23 -7
- package/dist/runtime/components/Shared/TabBar.vue +3 -2
- package/dist/runtime/components/Shared/TabBar.vue.d.ts +23 -7
- package/dist/runtime/composables/useDashboards.d.ts +27 -0
- package/dist/runtime/composables/useDashboards.js +143 -0
- package/dist/runtime/composables/useSavedViews.d.ts +44 -0
- package/dist/runtime/composables/useSavedViews.js +44 -0
- package/dist/runtime/composables/useStickyListView.d.ts +60 -0
- package/dist/runtime/composables/useStickyListView.js +32 -0
- package/dist/runtime/lib/canonicalJson.d.ts +43 -0
- package/dist/runtime/lib/canonicalJson.js +38 -0
- package/dist/runtime/lib/dashboardIcons.d.ts +4 -0
- package/dist/runtime/lib/dashboardIcons.js +50 -0
- package/dist/runtime/lib/facets.d.ts +200 -0
- package/dist/runtime/lib/facets.js +171 -0
- package/dist/runtime/lib/gridTracks.d.ts +55 -0
- package/dist/runtime/lib/gridTracks.js +29 -0
- package/dist/runtime/lib/widgetRegistry.d.ts +124 -0
- package/dist/runtime/lib/widgetRegistry.js +66 -0
- package/dist/runtime/types/dashboard.d.ts +95 -0
- package/dist/runtime/types/dashboard.js +2 -0
- package/dist/types.d.mts +8 -0
- package/package.json +4 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
open: boolean;
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
label: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
confirmLabel: string;
|
|
8
|
+
/** Pre-filled value — a rename starts from the current name. */
|
|
9
|
+
initial?: string;
|
|
10
|
+
};
|
|
11
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:open": (value: boolean) => any;
|
|
13
|
+
submit: (name: string) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
15
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
16
|
+
onSubmit?: ((name: string) => any) | undefined;
|
|
17
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
|
+
export default _default;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Dashboard } from '../../types/dashboard.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
dashboards: Dashboard[];
|
|
5
|
+
currentId?: string | null;
|
|
6
|
+
};
|
|
7
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
|
+
"update:open": (value: boolean) => any;
|
|
9
|
+
select: (id: string) => any;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
11
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
12
|
+
onSelect?: ((id: string) => any) | undefined;
|
|
13
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input } from "@revenexx/studio";
|
|
3
|
+
import { Global, Lock, Magnifier, Pen, Star } from "#solar-icons";
|
|
4
|
+
import { computed, ref } from "vue";
|
|
5
|
+
import { dateTimeFormat } from "../../lib/formatBridge";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
open: { type: Boolean, required: true },
|
|
8
|
+
dashboards: { type: Array, required: true },
|
|
9
|
+
currentId: { type: [String, null], required: false }
|
|
10
|
+
});
|
|
11
|
+
const emit = defineEmits(["update:open", "select"]);
|
|
12
|
+
const t = useT();
|
|
13
|
+
function formatDateTime(value) {
|
|
14
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
15
|
+
return Number.isNaN(date.getTime()) ? "" : dateTimeFormat({ dateStyle: "medium", timeStyle: "short" }).format(date);
|
|
16
|
+
}
|
|
17
|
+
const query = ref("");
|
|
18
|
+
watch(() => props.open, (open) => {
|
|
19
|
+
if (open) query.value = "";
|
|
20
|
+
});
|
|
21
|
+
const groups = computed(() => {
|
|
22
|
+
const term = query.value.trim().toLowerCase();
|
|
23
|
+
const match = (d) => !term || d.name.toLowerCase().includes(term) || (d.ownerName ?? "").toLowerCase().includes(term);
|
|
24
|
+
return [
|
|
25
|
+
{ key: "mine", label: t("dashboard.header.group.mine"), items: props.dashboards.filter((d) => d.isOwner && match(d)) },
|
|
26
|
+
{ key: "shared", label: t("dashboard.header.group.shared"), items: props.dashboards.filter((d) => !d.isOwner && match(d)) }
|
|
27
|
+
].filter((group) => group.items.length > 0);
|
|
28
|
+
});
|
|
29
|
+
const total = computed(() => groups.value.reduce((sum, g) => sum + g.items.length, 0));
|
|
30
|
+
function pick(id) {
|
|
31
|
+
emit("select", id);
|
|
32
|
+
emit("update:open", false);
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<Dialog :open="open" @update:open="emit('update:open', $event)">
|
|
38
|
+
<DialogContent class="max-w-2xl">
|
|
39
|
+
<DialogHeader>
|
|
40
|
+
<DialogTitle>{{ t("dashboard.picker.title") }}</DialogTitle>
|
|
41
|
+
<DialogDescription>{{ t("dashboard.picker.description", { count: dashboards.length }) }}</DialogDescription>
|
|
42
|
+
</DialogHeader>
|
|
43
|
+
|
|
44
|
+
<div class="relative">
|
|
45
|
+
<Magnifier :size="14" class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
46
|
+
<Input v-model="query" :placeholder="t('dashboard.picker.search_placeholder')" class="bg-white pl-8" />
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="max-h-[55vh] -mx-1 space-y-4 overflow-y-auto px-1 pb-1">
|
|
50
|
+
<p v-if="!total" class="py-10 text-center text-sm text-muted-foreground">
|
|
51
|
+
{{ t("dashboard.picker.no_matches", { query }) }}
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
<section v-for="group in groups" :key="group.key">
|
|
55
|
+
<h4 class="mb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
56
|
+
{{ group.label }}
|
|
57
|
+
</h4>
|
|
58
|
+
<ul class="flex flex-col gap-1">
|
|
59
|
+
<li v-for="dashboard in group.items" :key="dashboard.id">
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
class="flex w-full items-center gap-3 rounded-md border px-3 py-2 text-left transition"
|
|
63
|
+
:class="dashboard.id === currentId ? 'border-primary/40 bg-primary/[0.04]' : 'border-transparent hover:border-black/[0.08] hover:bg-black/[0.03]'"
|
|
64
|
+
@click="pick(dashboard.id)"
|
|
65
|
+
>
|
|
66
|
+
<span class="min-w-0 flex-1">
|
|
67
|
+
<span class="flex items-center gap-1.5">
|
|
68
|
+
<span class="truncate text-sm font-medium text-[#0B0522]">{{ dashboard.name }}</span>
|
|
69
|
+
<span
|
|
70
|
+
v-if="dashboard.isDefault"
|
|
71
|
+
class="shrink-0 rounded bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-700"
|
|
72
|
+
>{{ t("dashboard.header.menu.default_badge") }}</span>
|
|
73
|
+
</span>
|
|
74
|
+
<span class="mt-0.5 block truncate text-xs text-muted-foreground">
|
|
75
|
+
<template v-if="dashboard.ownerName">{{ t("dashboard.picker.owner", { name: dashboard.ownerName }) }} · </template>
|
|
76
|
+
{{ t("dashboard.picker.updated", { at: formatDateTime(dashboard.updatedAt) }) }}
|
|
77
|
+
</span>
|
|
78
|
+
</span>
|
|
79
|
+
|
|
80
|
+
<!-- How it is shared, at a glance: private, read-only or open. -->
|
|
81
|
+
<span class="flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
|
|
82
|
+
<template v-if="dashboard.visibility === 'private'">
|
|
83
|
+
<Lock :size="12" /> {{ t("dashboard.share.private.title") }}
|
|
84
|
+
</template>
|
|
85
|
+
<template v-else-if="dashboard.tenantPermission === 'edit'">
|
|
86
|
+
<Pen :size="12" /> {{ t("dashboard.share.permission.edit.title") }}
|
|
87
|
+
</template>
|
|
88
|
+
<template v-else>
|
|
89
|
+
<Global :size="12" /> {{ t("dashboard.share.permission.view.title") }}
|
|
90
|
+
</template>
|
|
91
|
+
</span>
|
|
92
|
+
<Star v-if="dashboard.id === currentId" :size="12" class="shrink-0 text-primary opacity-0" />
|
|
93
|
+
</button>
|
|
94
|
+
</li>
|
|
95
|
+
</ul>
|
|
96
|
+
</section>
|
|
97
|
+
</div>
|
|
98
|
+
</DialogContent>
|
|
99
|
+
</Dialog>
|
|
100
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Dashboard } from '../../types/dashboard.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
dashboards: Dashboard[];
|
|
5
|
+
currentId?: string | null;
|
|
6
|
+
};
|
|
7
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
|
+
"update:open": (value: boolean) => any;
|
|
9
|
+
select: (id: string) => any;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
11
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
12
|
+
onSelect?: ((id: string) => any) | undefined;
|
|
13
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Dashboard, DashboardTenantPermission, DashboardVisibility } from '../../types/dashboard.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
dashboard: Dashboard | null;
|
|
5
|
+
/**
|
|
6
|
+
* Where this surface's dashboards live. The link is `path?dashboard=slug`,
|
|
7
|
+
* and a Commerce dashboard opened at `/` would land on the home screen and
|
|
8
|
+
* silently resolve to something else.
|
|
9
|
+
*/
|
|
10
|
+
path?: string;
|
|
11
|
+
};
|
|
12
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
13
|
+
"update:open": (value: boolean) => any;
|
|
14
|
+
change: (payload: {
|
|
15
|
+
visibility: DashboardVisibility;
|
|
16
|
+
tenantPermission: DashboardTenantPermission;
|
|
17
|
+
}) => any;
|
|
18
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
19
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
20
|
+
onChange?: ((payload: {
|
|
21
|
+
visibility: DashboardVisibility;
|
|
22
|
+
tenantPermission: DashboardTenantPermission;
|
|
23
|
+
}) => any) | undefined;
|
|
24
|
+
}>, {
|
|
25
|
+
path: string;
|
|
26
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
27
|
+
export default _default;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@revenexx/studio";
|
|
3
|
+
import { CheckCircle, Copy, Global, Lock, Pen } from "#solar-icons";
|
|
4
|
+
import { computed, ref, watch } from "vue";
|
|
5
|
+
import { dateTimeFormat } from "../../lib/formatBridge";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
open: { type: Boolean, required: true },
|
|
8
|
+
dashboard: { type: [Object, null], required: true },
|
|
9
|
+
path: { type: String, required: false, default: "/" }
|
|
10
|
+
});
|
|
11
|
+
const emit = defineEmits(["update:open", "change"]);
|
|
12
|
+
const t = useT();
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
function modeOf(dashboard) {
|
|
15
|
+
if (!dashboard || dashboard.visibility !== "tenant") return "private";
|
|
16
|
+
return dashboard.tenantPermission === "edit" ? "tenant-edit" : "tenant-view";
|
|
17
|
+
}
|
|
18
|
+
const MODE_VALUES = {
|
|
19
|
+
"private": { visibility: "private", tenantPermission: "view" },
|
|
20
|
+
"tenant-view": { visibility: "tenant", tenantPermission: "view" },
|
|
21
|
+
"tenant-edit": { visibility: "tenant", tenantPermission: "edit" }
|
|
22
|
+
};
|
|
23
|
+
const choice = ref("private");
|
|
24
|
+
watch(() => props.open, (open) => {
|
|
25
|
+
if (open) choice.value = modeOf(props.dashboard);
|
|
26
|
+
});
|
|
27
|
+
function formatDateTime(value) {
|
|
28
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
29
|
+
return Number.isNaN(date.getTime()) ? "" : dateTimeFormat({ dateStyle: "medium", timeStyle: "short" }).format(date);
|
|
30
|
+
}
|
|
31
|
+
const options = computed(() => [
|
|
32
|
+
{
|
|
33
|
+
value: "private",
|
|
34
|
+
icon: Lock,
|
|
35
|
+
title: t("dashboard.share.private.title"),
|
|
36
|
+
description: t("dashboard.share.private.description")
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: "tenant-view",
|
|
40
|
+
icon: Global,
|
|
41
|
+
title: t("dashboard.share.mode.view.title"),
|
|
42
|
+
description: t("dashboard.share.mode.view.description")
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: "tenant-edit",
|
|
46
|
+
icon: Pen,
|
|
47
|
+
title: t("dashboard.share.mode.edit.title"),
|
|
48
|
+
description: t("dashboard.share.mode.edit.description")
|
|
49
|
+
}
|
|
50
|
+
]);
|
|
51
|
+
const link = computed(() => {
|
|
52
|
+
if (!props.dashboard || !import.meta.client) return "";
|
|
53
|
+
const href = router.resolve({ path: props.path, query: { dashboard: props.dashboard.slug } }).href;
|
|
54
|
+
return new URL(href, window.location.origin).toString();
|
|
55
|
+
});
|
|
56
|
+
const copied = ref(false);
|
|
57
|
+
let copiedTimer = null;
|
|
58
|
+
async function copyLink() {
|
|
59
|
+
try {
|
|
60
|
+
await navigator.clipboard.writeText(link.value);
|
|
61
|
+
copied.value = true;
|
|
62
|
+
if (copiedTimer) clearTimeout(copiedTimer);
|
|
63
|
+
copiedTimer = setTimeout(() => {
|
|
64
|
+
copied.value = false;
|
|
65
|
+
}, 2e3);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
onUnmounted(() => {
|
|
70
|
+
if (copiedTimer) clearTimeout(copiedTimer);
|
|
71
|
+
});
|
|
72
|
+
const meta = computed(() => {
|
|
73
|
+
const d = props.dashboard;
|
|
74
|
+
if (!d) return "";
|
|
75
|
+
const parts = [];
|
|
76
|
+
if (d.ownerName) parts.push(t("dashboard.share.meta.created", { name: d.ownerName }));
|
|
77
|
+
parts.push(d.updatedByName ? t("dashboard.share.meta.updated", { name: d.updatedByName, at: formatDateTime(d.updatedAt) }) : t("dashboard.share.meta.updated_unknown", { at: formatDateTime(d.updatedAt) }));
|
|
78
|
+
if (!parts[0].match(/^[A-ZÄÖÜ]/)) parts[0] = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
|
|
79
|
+
return parts.join(" \xB7 ");
|
|
80
|
+
});
|
|
81
|
+
const dirty = computed(() => choice.value !== modeOf(props.dashboard));
|
|
82
|
+
function apply() {
|
|
83
|
+
if (dirty.value) emit("change", MODE_VALUES[choice.value]);
|
|
84
|
+
emit("update:open", false);
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<Dialog :open="open" @update:open="emit('update:open', $event)">
|
|
90
|
+
<DialogContent class="max-w-lg">
|
|
91
|
+
<DialogHeader>
|
|
92
|
+
<DialogTitle>{{ t("dashboard.share.title", { name: dashboard?.name ?? "" }) }}</DialogTitle>
|
|
93
|
+
<DialogDescription>{{ t("dashboard.share.description") }}</DialogDescription>
|
|
94
|
+
</DialogHeader>
|
|
95
|
+
|
|
96
|
+
<div class="space-y-2">
|
|
97
|
+
<button
|
|
98
|
+
v-for="option in options"
|
|
99
|
+
:key="option.value"
|
|
100
|
+
type="button"
|
|
101
|
+
class="flex w-full items-start gap-3 rounded-lg border p-3 text-left transition"
|
|
102
|
+
:class="choice === option.value ? 'border-primary/40 bg-primary/[0.04]' : 'border-black/[0.06] hover:border-black/15 hover:bg-black/[0.02]'"
|
|
103
|
+
@click="choice = option.value"
|
|
104
|
+
>
|
|
105
|
+
<span
|
|
106
|
+
class="mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-md"
|
|
107
|
+
:class="choice === option.value ? 'bg-primary/10 text-primary' : 'bg-black/[0.04] text-muted-foreground'"
|
|
108
|
+
>
|
|
109
|
+
<component :is="option.icon" :size="15" />
|
|
110
|
+
</span>
|
|
111
|
+
<span class="min-w-0 flex-1">
|
|
112
|
+
<span class="block text-sm font-medium text-[#0B0522]">{{ option.title }}</span>
|
|
113
|
+
<span class="mt-0.5 block text-xs leading-snug text-muted-foreground">{{ option.description }}</span>
|
|
114
|
+
</span>
|
|
115
|
+
<CheckCircle v-if="choice === option.value" :size="16" class="mt-0.5 shrink-0 text-primary" />
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<!-- The link works for anyone who can already see the dashboard; it is a
|
|
120
|
+
convenience, not a grant. Sharing is what grants. -->
|
|
121
|
+
<div v-if="choice !== 'private'" class="flex items-center gap-2 rounded-md border border-black/[0.06] bg-black/[0.02] px-2 py-1.5">
|
|
122
|
+
<input
|
|
123
|
+
:value="link"
|
|
124
|
+
readonly
|
|
125
|
+
class="min-w-0 flex-1 truncate bg-transparent text-xs text-muted-foreground outline-none"
|
|
126
|
+
@focus="$event.target.select()"
|
|
127
|
+
>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
class="flex shrink-0 items-center gap-1 rounded px-1.5 py-1 text-xs font-medium text-primary hover:bg-primary/10"
|
|
131
|
+
@click="copyLink"
|
|
132
|
+
>
|
|
133
|
+
<component :is="copied ? CheckCircle : Copy" :size="13" />
|
|
134
|
+
{{ copied ? t("dashboard.share.link.copied") : t("dashboard.share.link.copy") }}
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- Who made it and who touched it last. Names are captured at write
|
|
139
|
+
time; the cockpit has no user directory to resolve a subject id
|
|
140
|
+
against, and a lookup per row would not be worth it. -->
|
|
141
|
+
<p v-if="meta" class="border-t border-black/[0.06] pt-3 text-xs text-muted-foreground">
|
|
142
|
+
{{ meta }}
|
|
143
|
+
</p>
|
|
144
|
+
|
|
145
|
+
<DialogFooter>
|
|
146
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" @click="emit('update:open', false)">
|
|
147
|
+
{{ t("dashboard.dialog.action.cancel") }}
|
|
148
|
+
</Button>
|
|
149
|
+
<Button size="sm" class="h-7 text-sm" :disabled="!dirty" @click="apply">
|
|
150
|
+
{{ t("dashboard.share.action") }}
|
|
151
|
+
</Button>
|
|
152
|
+
</DialogFooter>
|
|
153
|
+
</DialogContent>
|
|
154
|
+
</Dialog>
|
|
155
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Dashboard, DashboardTenantPermission, DashboardVisibility } from '../../types/dashboard.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
dashboard: Dashboard | null;
|
|
5
|
+
/**
|
|
6
|
+
* Where this surface's dashboards live. The link is `path?dashboard=slug`,
|
|
7
|
+
* and a Commerce dashboard opened at `/` would land on the home screen and
|
|
8
|
+
* silently resolve to something else.
|
|
9
|
+
*/
|
|
10
|
+
path?: string;
|
|
11
|
+
};
|
|
12
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
13
|
+
"update:open": (value: boolean) => any;
|
|
14
|
+
change: (payload: {
|
|
15
|
+
visibility: DashboardVisibility;
|
|
16
|
+
tenantPermission: DashboardTenantPermission;
|
|
17
|
+
}) => any;
|
|
18
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
19
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
20
|
+
onChange?: ((payload: {
|
|
21
|
+
visibility: DashboardVisibility;
|
|
22
|
+
tenantPermission: DashboardTenantPermission;
|
|
23
|
+
}) => any) | undefined;
|
|
24
|
+
}>, {
|
|
25
|
+
path: string;
|
|
26
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
27
|
+
export default _default;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Dashboard } from '../../types/dashboard.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
dashboards: Dashboard[];
|
|
4
|
+
current: Dashboard | null;
|
|
5
|
+
editing: boolean;
|
|
6
|
+
dirty: boolean;
|
|
7
|
+
saving: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* The person this row greets. A prop rather than a lookup: who is signed in
|
|
10
|
+
* is the HOST's question, and a published module has no user directory.
|
|
11
|
+
*/
|
|
12
|
+
userName?: string;
|
|
13
|
+
/** The line above the greeting. Defaults to today's date. */
|
|
14
|
+
eyebrow?: string;
|
|
15
|
+
/** The big line. Defaults to the greeting. */
|
|
16
|
+
heading?: string;
|
|
17
|
+
};
|
|
18
|
+
declare var __VLS_19: any;
|
|
19
|
+
type __VLS_Slots = {} & {
|
|
20
|
+
scope?: (props: typeof __VLS_19) => any;
|
|
21
|
+
};
|
|
22
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
23
|
+
edit: () => any;
|
|
24
|
+
select: (id: string) => any;
|
|
25
|
+
save: () => any;
|
|
26
|
+
discard: () => any;
|
|
27
|
+
addWidget: () => any;
|
|
28
|
+
rename: () => any;
|
|
29
|
+
share: () => any;
|
|
30
|
+
browse: () => any;
|
|
31
|
+
create: () => any;
|
|
32
|
+
duplicate: () => any;
|
|
33
|
+
remove: () => any;
|
|
34
|
+
setDefault: () => any;
|
|
35
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
36
|
+
onEdit?: (() => any) | undefined;
|
|
37
|
+
onSelect?: ((id: string) => any) | undefined;
|
|
38
|
+
onSave?: (() => any) | undefined;
|
|
39
|
+
onDiscard?: (() => any) | undefined;
|
|
40
|
+
onAddWidget?: (() => any) | undefined;
|
|
41
|
+
onRename?: (() => any) | undefined;
|
|
42
|
+
onShare?: (() => any) | undefined;
|
|
43
|
+
onBrowse?: (() => any) | undefined;
|
|
44
|
+
onCreate?: (() => any) | undefined;
|
|
45
|
+
onDuplicate?: (() => any) | undefined;
|
|
46
|
+
onRemove?: (() => any) | undefined;
|
|
47
|
+
onSetDefault?: (() => any) | undefined;
|
|
48
|
+
}>, {
|
|
49
|
+
heading: string;
|
|
50
|
+
userName: string;
|
|
51
|
+
eyebrow: string;
|
|
52
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
53
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
54
|
+
export default _default;
|
|
55
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
56
|
+
new (): {
|
|
57
|
+
$slots: S;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import DashboardControls from "./DashboardControls.vue";
|
|
4
|
+
import { dateTimeFormat } from "../../lib/formatBridge";
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
dashboards: { type: Array, required: true },
|
|
7
|
+
current: { type: [Object, null], required: true },
|
|
8
|
+
editing: { type: Boolean, required: true },
|
|
9
|
+
dirty: { type: Boolean, required: true },
|
|
10
|
+
saving: { type: Boolean, required: true },
|
|
11
|
+
userName: { type: String, required: false, default: "" },
|
|
12
|
+
eyebrow: { type: String, required: false, default: "" },
|
|
13
|
+
heading: { type: String, required: false, default: "" }
|
|
14
|
+
});
|
|
15
|
+
const emit = defineEmits(["select", "edit", "save", "discard", "addWidget", "rename", "share", "browse", "create", "duplicate", "remove", "setDefault"]);
|
|
16
|
+
const t = useT();
|
|
17
|
+
const greeting = computed(() => {
|
|
18
|
+
const hour = (/* @__PURE__ */ new Date()).getHours();
|
|
19
|
+
if (hour < 11) return t("dashboard.topbar.greeting.morning");
|
|
20
|
+
if (hour < 17) return t("dashboard.topbar.greeting.day");
|
|
21
|
+
return t("dashboard.topbar.greeting.evening");
|
|
22
|
+
});
|
|
23
|
+
const fullName = computed(() => props.userName.trim());
|
|
24
|
+
const dateLabel = computed(() => props.eyebrow || dateTimeFormat({ weekday: "long", day: "numeric", month: "long" }).format(/* @__PURE__ */ new Date()));
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<div class="relative z-30 mb-3 rounded-lg border border-gray-400/10 outline-1 outline-white/30">
|
|
29
|
+
<div class="relative flex flex-wrap items-center justify-between gap-4 rounded-lg bg-gradient-to-br from-white via-violet-50/50 to-white px-5 py-5">
|
|
30
|
+
<!-- Decoration, in its own clipped layer. Clipping the row itself would
|
|
31
|
+
cut off the dashboard menu when it opens. Hidden from assistive tech,
|
|
32
|
+
and the drift stops for anyone who asked the system for less motion. -->
|
|
33
|
+
<div class="pointer-events-none absolute inset-0 overflow-hidden rounded-lg" aria-hidden="true">
|
|
34
|
+
<div class="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/60 to-transparent motion-safe:animate-[dashboard-shimmer_9s_ease-in-out_infinite]" />
|
|
35
|
+
<div class="absolute -right-16 -top-20 size-56 rounded-full bg-gradient-to-br from-violet-300/25 to-amber-200/20 blur-3xl" />
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="relative min-w-0">
|
|
39
|
+
<div class="text-mono uppercase text-muted-foreground">
|
|
40
|
+
{{ dateLabel }}
|
|
41
|
+
</div>
|
|
42
|
+
<h1 class="mt-1 truncate text-2xl font-semibold text-[#0B0522]">
|
|
43
|
+
<template v-if="heading">{{ heading }}</template>
|
|
44
|
+
<template v-else>
|
|
45
|
+
{{ greeting }}<template v-if="fullName">, <span class="text-primary">{{ fullName }}</span></template>.
|
|
46
|
+
</template>
|
|
47
|
+
</h1>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<DashboardControls
|
|
51
|
+
:dashboards="dashboards"
|
|
52
|
+
:current="current"
|
|
53
|
+
:editing="editing"
|
|
54
|
+
:dirty="dirty"
|
|
55
|
+
:saving="saving"
|
|
56
|
+
@select="emit('select', $event)"
|
|
57
|
+
@edit="emit('edit')"
|
|
58
|
+
@save="emit('save')"
|
|
59
|
+
@discard="emit('discard')"
|
|
60
|
+
@add-widget="emit('addWidget')"
|
|
61
|
+
@rename="emit('rename')"
|
|
62
|
+
@share="emit('share')"
|
|
63
|
+
@browse="emit('browse')"
|
|
64
|
+
@create="emit('create')"
|
|
65
|
+
@duplicate="emit('duplicate')"
|
|
66
|
+
@remove="emit('remove')"
|
|
67
|
+
@set-default="emit('setDefault')"
|
|
68
|
+
>
|
|
69
|
+
<template #scope="slotProps">
|
|
70
|
+
<slot name="scope" v-bind="slotProps" />
|
|
71
|
+
</template>
|
|
72
|
+
</DashboardControls>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
@keyframes dashboard-shimmer{0%,60%{transform:translateX(-100%)}to{transform:translateX(200%)}}
|
|
79
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Dashboard } from '../../types/dashboard.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
dashboards: Dashboard[];
|
|
4
|
+
current: Dashboard | null;
|
|
5
|
+
editing: boolean;
|
|
6
|
+
dirty: boolean;
|
|
7
|
+
saving: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* The person this row greets. A prop rather than a lookup: who is signed in
|
|
10
|
+
* is the HOST's question, and a published module has no user directory.
|
|
11
|
+
*/
|
|
12
|
+
userName?: string;
|
|
13
|
+
/** The line above the greeting. Defaults to today's date. */
|
|
14
|
+
eyebrow?: string;
|
|
15
|
+
/** The big line. Defaults to the greeting. */
|
|
16
|
+
heading?: string;
|
|
17
|
+
};
|
|
18
|
+
declare var __VLS_19: any;
|
|
19
|
+
type __VLS_Slots = {} & {
|
|
20
|
+
scope?: (props: typeof __VLS_19) => any;
|
|
21
|
+
};
|
|
22
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
23
|
+
edit: () => any;
|
|
24
|
+
select: (id: string) => any;
|
|
25
|
+
save: () => any;
|
|
26
|
+
discard: () => any;
|
|
27
|
+
addWidget: () => any;
|
|
28
|
+
rename: () => any;
|
|
29
|
+
share: () => any;
|
|
30
|
+
browse: () => any;
|
|
31
|
+
create: () => any;
|
|
32
|
+
duplicate: () => any;
|
|
33
|
+
remove: () => any;
|
|
34
|
+
setDefault: () => any;
|
|
35
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
36
|
+
onEdit?: (() => any) | undefined;
|
|
37
|
+
onSelect?: ((id: string) => any) | undefined;
|
|
38
|
+
onSave?: (() => any) | undefined;
|
|
39
|
+
onDiscard?: (() => any) | undefined;
|
|
40
|
+
onAddWidget?: (() => any) | undefined;
|
|
41
|
+
onRename?: (() => any) | undefined;
|
|
42
|
+
onShare?: (() => any) | undefined;
|
|
43
|
+
onBrowse?: (() => any) | undefined;
|
|
44
|
+
onCreate?: (() => any) | undefined;
|
|
45
|
+
onDuplicate?: (() => any) | undefined;
|
|
46
|
+
onRemove?: (() => any) | undefined;
|
|
47
|
+
onSetDefault?: (() => any) | undefined;
|
|
48
|
+
}>, {
|
|
49
|
+
heading: string;
|
|
50
|
+
userName: string;
|
|
51
|
+
eyebrow: string;
|
|
52
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
53
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
54
|
+
export default _default;
|
|
55
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
56
|
+
new (): {
|
|
57
|
+
$slots: S;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DashboardWidget } from '../../types/dashboard.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
widget: DashboardWidget | null;
|
|
5
|
+
/**
|
|
6
|
+
* Config the surface wants materialised before the form opens — the cockpit
|
|
7
|
+
* resolves its quick-link defaults into rows here, so the editor starts from
|
|
8
|
+
* what is on screen rather than from an empty list the widget is not showing.
|
|
9
|
+
*/
|
|
10
|
+
hydrate?: (widget: DashboardWidget, config: Record<string, unknown>) => Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
declare var __VLS_66: {
|
|
13
|
+
field: any;
|
|
14
|
+
widget: any;
|
|
15
|
+
value: any;
|
|
16
|
+
set: (value: unknown) => void;
|
|
17
|
+
};
|
|
18
|
+
type __VLS_Slots = {} & {
|
|
19
|
+
field?: (props: typeof __VLS_66) => any;
|
|
20
|
+
};
|
|
21
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
22
|
+
"update:open": (value: boolean) => any;
|
|
23
|
+
apply: (payload: {
|
|
24
|
+
id: string;
|
|
25
|
+
title?: string;
|
|
26
|
+
config: Record<string, unknown>;
|
|
27
|
+
}) => any;
|
|
28
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
29
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
30
|
+
onApply?: ((payload: {
|
|
31
|
+
id: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
config: Record<string, unknown>;
|
|
34
|
+
}) => any) | undefined;
|
|
35
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
36
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
37
|
+
export default _default;
|
|
38
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
39
|
+
new (): {
|
|
40
|
+
$slots: S;
|
|
41
|
+
};
|
|
42
|
+
};
|