@revenexx/studio-shared 0.3.2 → 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 +36 -3
- package/dist/runtime/components/DataTable/DataTable.vue +252 -27
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +36 -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 +3 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
Input,
|
|
10
|
+
Label,
|
|
11
|
+
Select,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
SelectTrigger,
|
|
15
|
+
SelectValue,
|
|
16
|
+
Switch,
|
|
17
|
+
Textarea
|
|
18
|
+
} from "@revenexx/studio";
|
|
19
|
+
import { computed, ref, watch } from "vue";
|
|
20
|
+
import { useWidgetRegistry, widgetDescription, widgetTitle } from "../../lib/widgetRegistry";
|
|
21
|
+
const t = useT();
|
|
22
|
+
const registry = useWidgetRegistry();
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
open: { type: Boolean, required: true },
|
|
25
|
+
widget: { type: [Object, null], required: true },
|
|
26
|
+
hydrate: { type: Function, required: false }
|
|
27
|
+
});
|
|
28
|
+
const emit = defineEmits(["update:open", "apply"]);
|
|
29
|
+
const definition = computed(() => props.widget ? registry.get(props.widget.type) : void 0);
|
|
30
|
+
const heading = computed(() => widgetTitle(definition.value, t));
|
|
31
|
+
const subheading = computed(() => definition.value ? widgetDescription(definition.value, t) : "");
|
|
32
|
+
function fieldLabel(field) {
|
|
33
|
+
return field.label ?? (field.labelKey ? t(field.labelKey) : field.key);
|
|
34
|
+
}
|
|
35
|
+
function fieldHelp(field) {
|
|
36
|
+
return field.help ?? (field.helpKey ? t(field.helpKey) : "");
|
|
37
|
+
}
|
|
38
|
+
function optionLabel(option) {
|
|
39
|
+
return option.label ?? (option.labelKey ? t(option.labelKey) : option.value);
|
|
40
|
+
}
|
|
41
|
+
const title = ref("");
|
|
42
|
+
const draft = ref({});
|
|
43
|
+
watch(() => props.widget, (widget) => {
|
|
44
|
+
title.value = widget?.title ?? "";
|
|
45
|
+
if (!widget) {
|
|
46
|
+
draft.value = {};
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const config = { ...registry.defaultConfigFor(widget.type), ...widget.config ?? {} };
|
|
50
|
+
draft.value = props.hydrate ? props.hydrate(widget, config) : config;
|
|
51
|
+
}, { immediate: true });
|
|
52
|
+
function apply() {
|
|
53
|
+
if (!props.widget) return;
|
|
54
|
+
emit("apply", {
|
|
55
|
+
id: props.widget.id,
|
|
56
|
+
title: title.value.trim() || void 0,
|
|
57
|
+
config: { ...draft.value }
|
|
58
|
+
});
|
|
59
|
+
emit("update:open", false);
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<Dialog :open="open" @update:open="emit('update:open', $event)">
|
|
65
|
+
<DialogContent class="max-w-lg">
|
|
66
|
+
<DialogHeader>
|
|
67
|
+
<DialogTitle>{{ heading }}</DialogTitle>
|
|
68
|
+
<DialogDescription>{{ subheading }}</DialogDescription>
|
|
69
|
+
</DialogHeader>
|
|
70
|
+
|
|
71
|
+
<!-- `-mx-1 px-1 pb-1`: the scroll container clipped the focus ring off the
|
|
72
|
+
left edge and the bottom row. The padding gives the ring room; the
|
|
73
|
+
negative margin keeps the fields aligned with the dialog. -->
|
|
74
|
+
<div class="-mx-1 max-h-[60vh] space-y-4 overflow-y-auto px-1 pb-1">
|
|
75
|
+
<div class="space-y-1.5">
|
|
76
|
+
<Label for="widget-title">{{ t("dashboard.config.title.label") }}</Label>
|
|
77
|
+
<Input id="widget-title" v-model="title" class="bg-white" :placeholder="heading" />
|
|
78
|
+
<p class="text-xs text-muted-foreground">
|
|
79
|
+
{{ t("dashboard.config.title.help") }}
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div v-for="field in definition?.configFields ?? []" :key="field.key" class="space-y-1.5">
|
|
84
|
+
<Label :for="`cfg-${field.key}`">{{ fieldLabel(field) }}</Label>
|
|
85
|
+
|
|
86
|
+
<Textarea
|
|
87
|
+
v-if="field.type === 'textarea'"
|
|
88
|
+
:id="`cfg-${field.key}`"
|
|
89
|
+
v-model="draft[field.key]"
|
|
90
|
+
class="min-h-40 bg-white"
|
|
91
|
+
:placeholder="field.placeholderKey ? t(field.placeholderKey) : void 0"
|
|
92
|
+
rows="9"
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
<Select
|
|
96
|
+
v-else-if="field.type === 'select'"
|
|
97
|
+
v-model="draft[field.key]"
|
|
98
|
+
>
|
|
99
|
+
<SelectTrigger :id="`cfg-${field.key}`" class="bg-white">
|
|
100
|
+
<SelectValue :placeholder="field.placeholderKey ? t(field.placeholderKey) : t('dashboard.config.field.choose')" />
|
|
101
|
+
</SelectTrigger>
|
|
102
|
+
<SelectContent>
|
|
103
|
+
<SelectItem v-for="option in field.options ?? []" :key="option.value" :value="option.value">
|
|
104
|
+
{{ optionLabel(option) }}
|
|
105
|
+
</SelectItem>
|
|
106
|
+
</SelectContent>
|
|
107
|
+
</Select>
|
|
108
|
+
|
|
109
|
+
<Switch
|
|
110
|
+
v-else-if="field.type === 'boolean'"
|
|
111
|
+
v-model="draft[field.key]"
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
<!-- A control this dialog does not own: the surface renders it and
|
|
115
|
+
hands the value back through one `set`, so the draft stays the
|
|
116
|
+
single copy of what the operator is editing. -->
|
|
117
|
+
<slot
|
|
118
|
+
v-else-if="field.type === 'links' || field.type === 'filters' || field.type === 'sort'"
|
|
119
|
+
name="field"
|
|
120
|
+
:field="field"
|
|
121
|
+
:widget="widget"
|
|
122
|
+
:value="draft[field.key]"
|
|
123
|
+
:set="(value) => {
|
|
124
|
+
draft = { ...draft, [field.key]: value };
|
|
125
|
+
}"
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
<Input
|
|
129
|
+
v-else
|
|
130
|
+
:id="`cfg-${field.key}`"
|
|
131
|
+
v-model="draft[field.key]"
|
|
132
|
+
class="bg-white"
|
|
133
|
+
:type="field.type === 'number' ? 'number' : 'text'"
|
|
134
|
+
:placeholder="field.placeholderKey ? t(field.placeholderKey) : void 0"
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
<p v-if="fieldHelp(field)" class="text-xs text-muted-foreground">
|
|
138
|
+
{{ fieldHelp(field) }}
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div class="flex justify-end gap-2">
|
|
144
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" @click="emit('update:open', false)">
|
|
145
|
+
{{ t("dashboard.config.action.cancel") }}
|
|
146
|
+
</Button>
|
|
147
|
+
<Button size="sm" class="h-7 text-sm" @click="apply">
|
|
148
|
+
{{ t("dashboard.config.action.apply") }}
|
|
149
|
+
</Button>
|
|
150
|
+
</div>
|
|
151
|
+
</DialogContent>
|
|
152
|
+
</Dialog>
|
|
153
|
+
</template>
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WidgetDefinition } from '../../lib/widgetRegistry.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
definition?: WidgetDefinition;
|
|
4
|
+
title?: string;
|
|
5
|
+
editing?: boolean;
|
|
6
|
+
configurable?: boolean;
|
|
7
|
+
};
|
|
8
|
+
declare var __VLS_1: {}, __VLS_6: {};
|
|
9
|
+
type __VLS_Slots = {} & {
|
|
10
|
+
default?: (props: typeof __VLS_1) => any;
|
|
11
|
+
} & {
|
|
12
|
+
default?: (props: typeof __VLS_6) => any;
|
|
13
|
+
};
|
|
14
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
remove: () => any;
|
|
16
|
+
configure: () => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
|
+
onRemove?: (() => any) | undefined;
|
|
19
|
+
onConfigure?: (() => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
22
|
+
export default _default;
|
|
23
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
24
|
+
new (): {
|
|
25
|
+
$slots: S;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { CloseCircle, MenuDots, Settings } from "#solar-icons";
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import WidgetIcon from "./DashboardWidgetIcon.vue";
|
|
5
|
+
import { widgetTitle } from "../../lib/widgetRegistry";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
definition: { type: Object, required: false },
|
|
8
|
+
title: { type: String, required: false },
|
|
9
|
+
editing: { type: Boolean, required: false },
|
|
10
|
+
configurable: { type: Boolean, required: false }
|
|
11
|
+
});
|
|
12
|
+
defineEmits(["configure", "remove"]);
|
|
13
|
+
const t = useT();
|
|
14
|
+
const heading = computed(() => widgetTitle(props.definition, t, props.title));
|
|
15
|
+
const bare = computed(() => props.definition?.bare === true);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div class="relative h-full min-h-0" :class="editing ? 'select-none' : ''">
|
|
20
|
+
<!-- Bare widgets keep the card surface they already had; the frame stays
|
|
21
|
+
out of the way so the migration is a move, not a redesign. -->
|
|
22
|
+
<template v-if="bare">
|
|
23
|
+
<div class="h-full min-h-0 overflow-hidden">
|
|
24
|
+
<slot />
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<template v-else>
|
|
29
|
+
<div class="flex h-full min-h-0 flex-col rounded-lg border border-gray-400/10 outline-1 outline-white/30">
|
|
30
|
+
<div class="flex h-full min-h-0 flex-col overflow-hidden rounded-lg bg-white">
|
|
31
|
+
<div class="flex shrink-0 items-center justify-between gap-2 border-b border-black/[0.06] px-4 py-3">
|
|
32
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
33
|
+
<WidgetIcon :name="definition?.icon" :size="14" class="shrink-0 text-muted-foreground" />
|
|
34
|
+
<h3 class="truncate text-sm font-semibold text-[#0B0522]">
|
|
35
|
+
{{ heading }}
|
|
36
|
+
</h3>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="min-h-0 flex-1 overflow-auto">
|
|
40
|
+
<slot />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<template v-if="editing">
|
|
47
|
+
<!-- The whole card is the drag target, so a widget is grabbed wherever it
|
|
48
|
+
feels natural; the handle is the visual cue that this is possible. -->
|
|
49
|
+
<div class="widget-drag-handle absolute inset-0 z-10 cursor-move rounded-lg ring-2 ring-primary/20 ring-offset-0" />
|
|
50
|
+
<!-- The chips sit in the grid gutter rather than on the card. On the card
|
|
51
|
+
they covered the first line of every widget's own header, which is
|
|
52
|
+
exactly the line that says which widget it is. -->
|
|
53
|
+
<div class="absolute -top-2.5 left-2 z-20 flex items-center gap-1 rounded-md bg-white px-1.5 py-0.5 shadow-sm ring-1 ring-black/[0.08]">
|
|
54
|
+
<MenuDots :size="14" class="text-muted-foreground" />
|
|
55
|
+
<span class="max-w-[12rem] truncate text-[11px] font-medium text-muted-foreground">{{ heading }}</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="absolute -top-2.5 right-2 z-20 flex items-center gap-1">
|
|
58
|
+
<button
|
|
59
|
+
v-if="configurable"
|
|
60
|
+
type="button"
|
|
61
|
+
class="rounded-md bg-white p-1 text-muted-foreground shadow-sm ring-1 ring-black/[0.08] hover:text-foreground"
|
|
62
|
+
title="Configure"
|
|
63
|
+
@pointerdown.stop
|
|
64
|
+
@click.stop="$emit('configure')"
|
|
65
|
+
>
|
|
66
|
+
<Settings :size="14" />
|
|
67
|
+
</button>
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
class="rounded-md bg-white p-1 text-muted-foreground shadow-sm ring-1 ring-black/[0.08] hover:text-red-600"
|
|
71
|
+
title="Remove"
|
|
72
|
+
@pointerdown.stop
|
|
73
|
+
@click.stop="$emit('remove')"
|
|
74
|
+
>
|
|
75
|
+
<CloseCircle :size="14" />
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WidgetDefinition } from '../../lib/widgetRegistry.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
definition?: WidgetDefinition;
|
|
4
|
+
title?: string;
|
|
5
|
+
editing?: boolean;
|
|
6
|
+
configurable?: boolean;
|
|
7
|
+
};
|
|
8
|
+
declare var __VLS_1: {}, __VLS_6: {};
|
|
9
|
+
type __VLS_Slots = {} & {
|
|
10
|
+
default?: (props: typeof __VLS_1) => any;
|
|
11
|
+
} & {
|
|
12
|
+
default?: (props: typeof __VLS_6) => any;
|
|
13
|
+
};
|
|
14
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
remove: () => any;
|
|
16
|
+
configure: () => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
|
+
onRemove?: (() => any) | undefined;
|
|
19
|
+
onConfigure?: (() => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
22
|
+
export default _default;
|
|
23
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
24
|
+
new (): {
|
|
25
|
+
$slots: S;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DashboardWidget } from '../../types/dashboard.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
widget: DashboardWidget;
|
|
4
|
+
};
|
|
5
|
+
declare const _default: 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>;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useWidgetRegistry } from "../../lib/widgetRegistry";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
widget: { type: Object, required: true }
|
|
6
|
+
});
|
|
7
|
+
const t = useT();
|
|
8
|
+
const registry = useWidgetRegistry();
|
|
9
|
+
const definition = computed(() => registry.get(props.widget.type));
|
|
10
|
+
const resolved = computed(() => registry.componentFor(props.widget.type));
|
|
11
|
+
const config = computed(() => ({
|
|
12
|
+
...registry.defaultConfigFor(props.widget.type),
|
|
13
|
+
...props.widget.config ?? {}
|
|
14
|
+
}));
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<component :is="resolved" v-if="resolved" :config="config" class="h-full" />
|
|
19
|
+
<div
|
|
20
|
+
v-else-if="!definition"
|
|
21
|
+
class="flex h-full flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-black/10 px-4 text-center"
|
|
22
|
+
>
|
|
23
|
+
<p class="text-sm font-medium text-[#0B0522]">
|
|
24
|
+
{{ t("dashboard.widget.unknown.title") }}
|
|
25
|
+
</p>
|
|
26
|
+
<p class="text-xs text-muted-foreground">
|
|
27
|
+
{{ t("dashboard.widget.unknown.description", { type: widget.type }) }}
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DashboardWidget } from '../../types/dashboard.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
widget: DashboardWidget;
|
|
4
|
+
};
|
|
5
|
+
declare const _default: 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>;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
name?: string;
|
|
3
|
+
size?: number;
|
|
4
|
+
};
|
|
5
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
6
|
+
size: number;
|
|
7
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { dashboardIcon } from "../../lib/dashboardIcons";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
name: { type: String, required: false },
|
|
6
|
+
size: { type: Number, required: false, default: 16 }
|
|
7
|
+
});
|
|
8
|
+
const resolved = computed(() => dashboardIcon(props.name));
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<component :is="resolved" :size="size" />
|
|
13
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
name?: string;
|
|
3
|
+
size?: number;
|
|
4
|
+
};
|
|
5
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
6
|
+
size: number;
|
|
7
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WidgetDefinition } from '../../lib/widgetRegistry.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
/** Types already on the dashboard, so the card can say so. */
|
|
5
|
+
placedTypes?: string[];
|
|
6
|
+
/**
|
|
7
|
+
* The tenant's installed apps. A prop rather than a lookup: which apps a
|
|
8
|
+
* tenant has is the SURFACE's question — the home screen asks the commerce
|
|
9
|
+
* metrics store, the Commerce Studio already holds the manifests — and the
|
|
10
|
+
* library only needs the answer.
|
|
11
|
+
*/
|
|
12
|
+
installedApps?: string[];
|
|
13
|
+
};
|
|
14
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
"update:open": (value: boolean) => any;
|
|
16
|
+
add: (definition: WidgetDefinition) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
19
|
+
onAdd?: ((definition: WidgetDefinition) => any) | undefined;
|
|
20
|
+
}>, {
|
|
21
|
+
installedApps: string[];
|
|
22
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
23
|
+
export default _default;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input } from "@revenexx/studio";
|
|
3
|
+
import { AddSquare, Magnifier } from "#solar-icons";
|
|
4
|
+
import { computed, ref, watch } from "vue";
|
|
5
|
+
import WidgetIcon from "./DashboardWidgetIcon.vue";
|
|
6
|
+
import WidgetPreview from "./DashboardWidgetPreview.vue";
|
|
7
|
+
import { missingAppsFor, useWidgetRegistry, widgetDescription, widgetTitle } from "../../lib/widgetRegistry";
|
|
8
|
+
const t = useT();
|
|
9
|
+
const registry = useWidgetRegistry();
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
open: { type: Boolean, required: true },
|
|
12
|
+
placedTypes: { type: Array, required: false },
|
|
13
|
+
installedApps: { type: Array, required: false, default: () => [] }
|
|
14
|
+
});
|
|
15
|
+
const emit = defineEmits(["update:open", "add"]);
|
|
16
|
+
const query = ref("");
|
|
17
|
+
const installed = computed(() => new Set(props.installedApps));
|
|
18
|
+
watch(() => props.open, (open) => {
|
|
19
|
+
if (open) query.value = "";
|
|
20
|
+
});
|
|
21
|
+
const KNOWN_APPS = ["orders", "customers", "products", "carts", "channels", "payments", "markets"];
|
|
22
|
+
const appLabel = (name) => KNOWN_APPS.includes(name) ? t(`dashboard.app.${name}`) : name;
|
|
23
|
+
const groups = computed(() => {
|
|
24
|
+
const term = query.value.trim().toLowerCase();
|
|
25
|
+
return registry.byCategory().map((group) => ({
|
|
26
|
+
...group,
|
|
27
|
+
// Search runs over the translated text, so a German operator finds
|
|
28
|
+
// "Umsatz" and an English one finds "revenue" — matching the key would
|
|
29
|
+
// find neither.
|
|
30
|
+
widgets: group.widgets.filter((widget) => !term || title(widget).toLowerCase().includes(term) || description(widget).toLowerCase().includes(term) || t(group.labelKey).toLowerCase().includes(term))
|
|
31
|
+
})).filter((group) => group.widgets.length > 0);
|
|
32
|
+
});
|
|
33
|
+
const total = computed(() => groups.value.reduce((sum, g) => sum + g.widgets.length, 0));
|
|
34
|
+
function title(widget) {
|
|
35
|
+
return widgetTitle(widget, t);
|
|
36
|
+
}
|
|
37
|
+
function description(widget) {
|
|
38
|
+
return widgetDescription(widget, t);
|
|
39
|
+
}
|
|
40
|
+
function missing(widget) {
|
|
41
|
+
return missingAppsFor(widget, installed.value);
|
|
42
|
+
}
|
|
43
|
+
function placed(widget) {
|
|
44
|
+
return !!props.placedTypes?.includes(widget.type);
|
|
45
|
+
}
|
|
46
|
+
function unavailable(widget) {
|
|
47
|
+
return !!widget.comingSoon || missing(widget).length > 0;
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<Dialog :open="open" @update:open="emit('update:open', $event)">
|
|
53
|
+
<DialogContent class="!max-w-[min(96vw,72rem)] w-[min(96vw,72rem)]">
|
|
54
|
+
<DialogHeader>
|
|
55
|
+
<DialogTitle>{{ t("dashboard.library.title") }}</DialogTitle>
|
|
56
|
+
<DialogDescription>{{ t("dashboard.library.description") }}</DialogDescription>
|
|
57
|
+
</DialogHeader>
|
|
58
|
+
|
|
59
|
+
<div class="relative">
|
|
60
|
+
<Magnifier :size="14" class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
61
|
+
<Input v-model="query" :placeholder="t('dashboard.library.search_placeholder')" class="bg-white pl-8" />
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="max-h-[58vh] -mx-1 space-y-6 overflow-y-auto px-1 pb-1">
|
|
65
|
+
<p v-if="!total" class="py-10 text-center text-sm text-muted-foreground">
|
|
66
|
+
{{ t("dashboard.library.no_matches", { query }) }}
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
<section v-for="group in groups" :key="group.category">
|
|
70
|
+
<h4 class="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
71
|
+
{{ t(group.labelKey) }}
|
|
72
|
+
</h4>
|
|
73
|
+
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
74
|
+
<div
|
|
75
|
+
v-for="widget in group.widgets"
|
|
76
|
+
:key="widget.type"
|
|
77
|
+
class="group flex flex-col overflow-hidden rounded-lg border border-black/[0.06] bg-white transition"
|
|
78
|
+
:class="unavailable(widget) ? 'opacity-80' : 'hover:border-black/15 hover:shadow-sm'"
|
|
79
|
+
>
|
|
80
|
+
<div class="relative p-2">
|
|
81
|
+
<WidgetPreview :shape="widget.preview" :dimmed="unavailable(widget)" />
|
|
82
|
+
<span
|
|
83
|
+
v-if="widget.comingSoon"
|
|
84
|
+
class="absolute right-3 top-3 rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-primary"
|
|
85
|
+
>{{ t("dashboard.library.coming_soon") }}</span>
|
|
86
|
+
<span
|
|
87
|
+
v-else-if="placed(widget)"
|
|
88
|
+
class="absolute right-3 top-3 rounded bg-white/90 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground ring-1 ring-black/[0.06]"
|
|
89
|
+
>{{ t("dashboard.library.placed") }}</span>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="flex flex-1 flex-col px-3 pb-3">
|
|
93
|
+
<div class="flex items-start gap-2">
|
|
94
|
+
<span class="mt-0.5 flex size-6 shrink-0 items-center justify-center rounded bg-black/[0.04] text-muted-foreground">
|
|
95
|
+
<WidgetIcon :name="widget.icon" :size="13" />
|
|
96
|
+
</span>
|
|
97
|
+
<div class="min-w-0 flex-1">
|
|
98
|
+
<p class="truncate text-sm font-medium text-[#0B0522]">
|
|
99
|
+
{{ title(widget) }}
|
|
100
|
+
</p>
|
|
101
|
+
<p class="mt-0.5 line-clamp-2 text-xs leading-snug text-muted-foreground">
|
|
102
|
+
{{ description(widget) }}
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="mt-3 flex items-center justify-between gap-2">
|
|
108
|
+
<span v-if="widget.comingSoon" class="truncate text-[11px] text-muted-foreground">
|
|
109
|
+
{{ t("dashboard.library.coming_soon_hint") }}
|
|
110
|
+
</span>
|
|
111
|
+
<span v-else-if="missing(widget).length" class="truncate text-[11px] text-amber-700">
|
|
112
|
+
{{ t("dashboard.library.needs_apps", { apps: missing(widget).map(appLabel).join(" + ") }) }}
|
|
113
|
+
</span>
|
|
114
|
+
<span v-else class="text-[11px] text-muted-foreground">
|
|
115
|
+
{{ widget.defaultSize.w }}×{{ widget.defaultSize.h }}
|
|
116
|
+
</span>
|
|
117
|
+
<Button
|
|
118
|
+
size="sm"
|
|
119
|
+
variant="outline"
|
|
120
|
+
class="h-7 shrink-0 text-sm"
|
|
121
|
+
:disabled="unavailable(widget)"
|
|
122
|
+
@click="emit('add', widget)"
|
|
123
|
+
>
|
|
124
|
+
<AddSquare :size="13" class="mr-1" /> {{ t("dashboard.library.action.add") }}
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</section>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div class="flex justify-end">
|
|
134
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" @click="emit('update:open', false)">
|
|
135
|
+
{{ t("dashboard.library.action.done") }}
|
|
136
|
+
</Button>
|
|
137
|
+
</div>
|
|
138
|
+
</DialogContent>
|
|
139
|
+
</Dialog>
|
|
140
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WidgetDefinition } from '../../lib/widgetRegistry.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
/** Types already on the dashboard, so the card can say so. */
|
|
5
|
+
placedTypes?: string[];
|
|
6
|
+
/**
|
|
7
|
+
* The tenant's installed apps. A prop rather than a lookup: which apps a
|
|
8
|
+
* tenant has is the SURFACE's question — the home screen asks the commerce
|
|
9
|
+
* metrics store, the Commerce Studio already holds the manifests — and the
|
|
10
|
+
* library only needs the answer.
|
|
11
|
+
*/
|
|
12
|
+
installedApps?: string[];
|
|
13
|
+
};
|
|
14
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
"update:open": (value: boolean) => any;
|
|
16
|
+
add: (definition: WidgetDefinition) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
19
|
+
onAdd?: ((definition: WidgetDefinition) => any) | undefined;
|
|
20
|
+
}>, {
|
|
21
|
+
installedApps: string[];
|
|
22
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
23
|
+
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The picture on a widget's library card (PE-480).
|
|
3
|
+
*
|
|
4
|
+
* A schematic, not a screenshot and not the live widget. The Integration
|
|
5
|
+
* Studio's template gallery draws its preview from the workflow definition for
|
|
6
|
+
* the same reason: a maintained screenshot goes stale the first time the widget
|
|
7
|
+
* changes and nobody notices. Rendering the real widget instead would be
|
|
8
|
+
* accurate but would fire every widget's queries the moment the library opens.
|
|
9
|
+
*
|
|
10
|
+
* So each widget declares a `preview` shape, and this draws it — pure layout,
|
|
11
|
+
* no data, no requests. It says what the widget *looks* like, which is what a
|
|
12
|
+
* person picking from a list actually needs.
|
|
13
|
+
*/
|
|
14
|
+
import type { WidgetPreviewShape } from '../../lib/widgetRegistry.js';
|
|
15
|
+
type __VLS_Props = {
|
|
16
|
+
shape: WidgetPreviewShape;
|
|
17
|
+
dimmed?: boolean;
|
|
18
|
+
};
|
|
19
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
20
|
+
dimmed: boolean;
|
|
21
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
22
|
+
export default _default;
|