@revenexx/studio-shared 0.3.2 → 0.4.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.d.mts +6 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -1
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.d.vue.ts +16 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +39 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue.d.ts +16 -0
- package/dist/runtime/components/Dashboard/DashboardControls.d.vue.ts +58 -0
- package/dist/runtime/components/Dashboard/DashboardControls.vue +209 -0
- package/dist/runtime/components/Dashboard/DashboardControls.vue.d.ts +58 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.d.vue.ts +17 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.vue +87 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.vue.d.ts +17 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.d.vue.ts +19 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +61 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.vue.d.ts +19 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.d.vue.ts +15 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +100 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue.d.ts +15 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.d.vue.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +155 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.vue.d.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.d.vue.ts +64 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.vue +79 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.vue.d.ts +64 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.d.vue.ts +44 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +153 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue.d.ts +44 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +80 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.d.vue.ts +7 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue +30 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue.d.ts +7 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.d.vue.ts +9 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue +13 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue.d.ts +9 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.d.vue.ts +24 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue +140 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue.d.ts +24 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.d.vue.ts +23 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue +124 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue.d.ts +23 -0
- package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +2 -1
- package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +2 -1
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +105 -28
- package/dist/runtime/components/DataTable/DataTable.vue +403 -38
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +105 -28
- package/dist/runtime/components/DataTable/FacetChips.d.vue.ts +17 -0
- package/dist/runtime/components/DataTable/FacetChips.vue +72 -0
- package/dist/runtime/components/DataTable/FacetChips.vue.d.ts +17 -0
- package/dist/runtime/components/DataTable/FacetPanel.d.vue.ts +21 -0
- package/dist/runtime/components/DataTable/FacetPanel.vue +359 -0
- package/dist/runtime/components/DataTable/FacetPanel.vue.d.ts +21 -0
- package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +6 -5
- package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +6 -5
- package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +6 -1
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue +83 -63
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +6 -1
- package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +9 -3
- package/dist/runtime/components/DataTable/SortMenu.vue +40 -12
- package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +9 -3
- package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +4 -2
- package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +4 -2
- package/dist/runtime/components/Form/FieldCell.d.vue.ts +32 -0
- package/dist/runtime/components/Form/FieldCell.vue +46 -0
- package/dist/runtime/components/Form/FieldCell.vue.d.ts +32 -0
- package/dist/runtime/components/Form/FieldGrid.d.vue.ts +13 -0
- package/dist/runtime/components/Form/FieldGrid.vue +11 -0
- package/dist/runtime/components/Form/FieldGrid.vue.d.ts +13 -0
- package/dist/runtime/components/Form/FormContextCard.d.vue.ts +33 -0
- package/dist/runtime/components/Form/FormContextCard.vue +56 -0
- package/dist/runtime/components/Form/FormContextCard.vue.d.ts +33 -0
- package/dist/runtime/components/Form/FormLayout.d.vue.ts +47 -0
- package/dist/runtime/components/Form/FormLayout.vue +169 -0
- package/dist/runtime/components/Form/FormLayout.vue.d.ts +47 -0
- package/dist/runtime/components/Form/FormSection.d.vue.ts +25 -0
- package/dist/runtime/components/Form/FormSection.vue +48 -0
- package/dist/runtime/components/Form/FormSection.vue.d.ts +25 -0
- package/dist/runtime/components/Form/SaveBar.d.vue.ts +44 -0
- package/dist/runtime/components/Form/SaveBar.vue +71 -0
- package/dist/runtime/components/Form/SaveBar.vue.d.ts +44 -0
- package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +2 -1
- package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +2 -1
- package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +4 -3
- package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +4 -3
- package/dist/runtime/components/Media/MediaUploader.d.vue.ts +4 -3
- package/dist/runtime/components/Media/MediaUploader.vue.d.ts +4 -3
- package/dist/runtime/components/Shared/AppIcon.d.vue.ts +21 -0
- package/dist/runtime/components/Shared/AppIcon.vue +47 -0
- package/dist/runtime/components/Shared/AppIcon.vue.d.ts +21 -0
- package/dist/runtime/components/Shared/BuildLogsFlyout.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/DetailSheet.d.vue.ts +60 -0
- package/dist/runtime/components/Shared/DetailSheet.vue +207 -0
- package/dist/runtime/components/Shared/DetailSheet.vue.d.ts +60 -0
- package/dist/runtime/components/Shared/FloatingPanel.d.vue.ts +7 -6
- package/dist/runtime/components/Shared/FloatingPanel.vue.d.ts +7 -6
- package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/GitDeploymentSettings.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/KpiTile.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/KpiTile.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/ListSurface.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/ListSurface.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/PageHeader.d.vue.ts +7 -6
- package/dist/runtime/components/Shared/PageHeader.vue.d.ts +7 -6
- package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +8 -7
- package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +8 -7
- package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/SortHeader.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/SortHeader.vue +6 -2
- package/dist/runtime/components/Shared/SortHeader.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/StateCard.d.vue.ts +6 -5
- package/dist/runtime/components/Shared/StateCard.vue.d.ts +6 -5
- package/dist/runtime/components/Shared/StudioCard.d.vue.ts +5 -4
- package/dist/runtime/components/Shared/StudioCard.vue.d.ts +5 -4
- package/dist/runtime/components/Shared/TabBar.d.vue.ts +9 -10
- package/dist/runtime/components/Shared/TabBar.vue +26 -7
- package/dist/runtime/components/Shared/TabBar.vue.d.ts +9 -10
- package/dist/runtime/components/Shared/ViewTab.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/ViewTab.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/Wizard.d.vue.ts +10 -7
- package/dist/runtime/components/Shared/Wizard.vue.d.ts +10 -7
- package/dist/runtime/composables/useDashboards.d.ts +27 -0
- package/dist/runtime/composables/useDashboards.js +143 -0
- package/dist/runtime/composables/useDetailSheet.d.ts +20 -0
- package/dist/runtime/composables/useDetailSheet.js +50 -0
- package/dist/runtime/composables/usePlatformTenant.d.ts +6 -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/activeTenant.d.ts +58 -0
- package/dist/runtime/lib/activeTenant.js +45 -0
- package/dist/runtime/lib/appIcon.d.ts +48 -0
- package/dist/runtime/lib/appIcon.js +27 -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/detailSheet.d.ts +43 -0
- package/dist/runtime/lib/detailSheet.js +34 -0
- package/dist/runtime/lib/facets.d.ts +200 -0
- package/dist/runtime/lib/facets.js +171 -0
- package/dist/runtime/lib/formLayout.d.ts +94 -0
- package/dist/runtime/lib/formLayout.js +70 -0
- package/dist/runtime/lib/formLayoutContext.d.ts +26 -0
- package/dist/runtime/lib/formLayoutContext.js +1 -0
- package/dist/runtime/lib/gridTracks.d.ts +55 -0
- package/dist/runtime/lib/gridTracks.js +29 -0
- package/dist/runtime/lib/rowMove.d.ts +38 -0
- package/dist/runtime/lib/rowMove.js +20 -0
- package/dist/runtime/lib/solarIconMap.d.ts +15 -0
- package/dist/runtime/lib/solarIconMap.js +280 -0
- package/dist/runtime/lib/solarIcons.d.ts +37 -0
- package/dist/runtime/lib/solarIcons.js +25 -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 +12 -0
- package/package.json +3 -2
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { AltArrowDown, AltArrowRight, Filter, Magnifier } from "#solar-icons";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Checkbox,
|
|
7
|
+
Input,
|
|
8
|
+
Popover,
|
|
9
|
+
PopoverContent,
|
|
10
|
+
PopoverTrigger,
|
|
11
|
+
ToolbarFilterTrigger
|
|
12
|
+
} from "@revenexx/studio";
|
|
13
|
+
import {
|
|
14
|
+
allNodes,
|
|
15
|
+
copySelection,
|
|
16
|
+
countInFacet,
|
|
17
|
+
countSelected,
|
|
18
|
+
expandTo,
|
|
19
|
+
flattenNodes,
|
|
20
|
+
initialExpanded,
|
|
21
|
+
isEmptyFacetValue,
|
|
22
|
+
sameSelection,
|
|
23
|
+
setFacetValue,
|
|
24
|
+
toggleFacetValue
|
|
25
|
+
} from "../../lib/facets";
|
|
26
|
+
const props = defineProps({
|
|
27
|
+
facets: { type: Array, required: true },
|
|
28
|
+
modelValue: { type: Object, required: true },
|
|
29
|
+
size: { type: String, required: false, default: "sm" }
|
|
30
|
+
});
|
|
31
|
+
const emit = defineEmits(["update:modelValue", "apply", "reset"]);
|
|
32
|
+
const t = useT();
|
|
33
|
+
const open = ref(false);
|
|
34
|
+
const draft = ref(copySelection(props.modelValue));
|
|
35
|
+
const search = ref("");
|
|
36
|
+
const panelFacets = computed(() => props.facets);
|
|
37
|
+
const activeKey = ref("");
|
|
38
|
+
const activeFacet = computed(() => panelFacets.value.find((f) => f.key === activeKey.value) ?? panelFacets.value[0]);
|
|
39
|
+
watch([panelFacets, open], () => {
|
|
40
|
+
if (!panelFacets.value.some((f) => f.key === activeKey.value)) {
|
|
41
|
+
activeKey.value = panelFacets.value[0]?.key ?? "";
|
|
42
|
+
}
|
|
43
|
+
}, { immediate: true });
|
|
44
|
+
watch(open, (isOpen) => {
|
|
45
|
+
if (isOpen) draft.value = copySelection(props.modelValue);
|
|
46
|
+
else search.value = "";
|
|
47
|
+
});
|
|
48
|
+
watch(() => props.modelValue, (value) => {
|
|
49
|
+
if (!open.value) draft.value = copySelection(value);
|
|
50
|
+
}, { deep: true });
|
|
51
|
+
const appliedCount = computed(() => countSelected(props.facets, props.modelValue));
|
|
52
|
+
const dirty = computed(() => !sameSelection(draft.value, props.modelValue));
|
|
53
|
+
const query = computed(() => search.value.trim().toLowerCase());
|
|
54
|
+
function matches(facet, label) {
|
|
55
|
+
if (!query.value) return true;
|
|
56
|
+
if (facet.label.toLowerCase().includes(query.value)) return true;
|
|
57
|
+
return label.toLowerCase().includes(query.value);
|
|
58
|
+
}
|
|
59
|
+
function visibleOptions(facet) {
|
|
60
|
+
return (facet.options ?? []).filter((option) => matches(facet, option.label));
|
|
61
|
+
}
|
|
62
|
+
const visibleFacets = computed(() => {
|
|
63
|
+
if (!query.value) return panelFacets.value;
|
|
64
|
+
return panelFacets.value.filter((facet) => {
|
|
65
|
+
if (facet.label.toLowerCase().includes(query.value)) return true;
|
|
66
|
+
if (facet.kind === "check") return visibleOptions(facet).length > 0;
|
|
67
|
+
if (facet.kind === "tree") return visibleNodes(facet).length > 0;
|
|
68
|
+
return false;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
function isTicked(key, value) {
|
|
72
|
+
const current = draft.value[key];
|
|
73
|
+
return Array.isArray(current) && current.includes(value);
|
|
74
|
+
}
|
|
75
|
+
function tick(key, value, on) {
|
|
76
|
+
const single = props.facets.find((f) => f.key === key)?.single;
|
|
77
|
+
draft.value = toggleFacetValue(draft.value, key, value, on, { single });
|
|
78
|
+
}
|
|
79
|
+
function textValue(key) {
|
|
80
|
+
const current = draft.value[key];
|
|
81
|
+
return typeof current === "string" ? current : "";
|
|
82
|
+
}
|
|
83
|
+
function setText(key, value) {
|
|
84
|
+
draft.value = setFacetValue(draft.value, key, value);
|
|
85
|
+
}
|
|
86
|
+
function range(key) {
|
|
87
|
+
const current = draft.value[key];
|
|
88
|
+
return current && typeof current === "object" && !Array.isArray(current) ? current : {};
|
|
89
|
+
}
|
|
90
|
+
function setBound(key, bound, value) {
|
|
91
|
+
draft.value = setFacetValue(draft.value, key, { ...range(key), [bound]: value });
|
|
92
|
+
}
|
|
93
|
+
const expanded = ref({});
|
|
94
|
+
function expandedFor(facet) {
|
|
95
|
+
const existing = expanded.value[facet.key];
|
|
96
|
+
if (existing) return existing;
|
|
97
|
+
const seed = initialExpanded(facet.nodes ?? []);
|
|
98
|
+
const applied = props.modelValue[facet.key];
|
|
99
|
+
if (Array.isArray(applied)) {
|
|
100
|
+
for (const value of applied) {
|
|
101
|
+
for (const id of expandTo(facet.nodes ?? [], value)) seed.add(id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
expanded.value[facet.key] = seed;
|
|
105
|
+
return seed;
|
|
106
|
+
}
|
|
107
|
+
function toggleBranch(facet, value) {
|
|
108
|
+
const set = expandedFor(facet);
|
|
109
|
+
const next = new Set(set);
|
|
110
|
+
if (next.has(value)) next.delete(value);
|
|
111
|
+
else next.add(value);
|
|
112
|
+
expanded.value = { ...expanded.value, [facet.key]: next };
|
|
113
|
+
}
|
|
114
|
+
function isExpanded(facet, value) {
|
|
115
|
+
return expandedFor(facet).has(value);
|
|
116
|
+
}
|
|
117
|
+
function visibleNodes(facet) {
|
|
118
|
+
if (query.value) {
|
|
119
|
+
return allNodes(facet.nodes ?? []).filter((node) => node.label.toLowerCase().includes(query.value)).map((node) => ({ node, depth: 0 }));
|
|
120
|
+
}
|
|
121
|
+
return flattenNodes(facet.nodes ?? [], expandedFor(facet));
|
|
122
|
+
}
|
|
123
|
+
function apply() {
|
|
124
|
+
const next = copySelection(draft.value);
|
|
125
|
+
emit("update:modelValue", next);
|
|
126
|
+
emit("apply", next);
|
|
127
|
+
open.value = false;
|
|
128
|
+
}
|
|
129
|
+
function reset() {
|
|
130
|
+
draft.value = {};
|
|
131
|
+
emit("update:modelValue", {});
|
|
132
|
+
emit("reset");
|
|
133
|
+
open.value = false;
|
|
134
|
+
}
|
|
135
|
+
const nothingToReset = computed(() => Object.keys(draft.value).every((key) => isEmptyFacetValue(draft.value[key])) && appliedCount.value === 0);
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
139
|
+
<Popover v-model:open="open">
|
|
140
|
+
<PopoverTrigger as-child>
|
|
141
|
+
<ToolbarFilterTrigger
|
|
142
|
+
icon-only
|
|
143
|
+
:active="appliedCount > 0"
|
|
144
|
+
:count="appliedCount"
|
|
145
|
+
:label="t('shared.facetpanel.trigger.label')"
|
|
146
|
+
:size="size"
|
|
147
|
+
>
|
|
148
|
+
<template #icon>
|
|
149
|
+
<Filter :size="14" weight="BoldDuotone" />
|
|
150
|
+
</template>
|
|
151
|
+
</ToolbarFilterTrigger>
|
|
152
|
+
</PopoverTrigger>
|
|
153
|
+
|
|
154
|
+
<PopoverContent
|
|
155
|
+
align="start"
|
|
156
|
+
class="w-[34rem] max-w-[calc(100vw-2rem)] overflow-hidden p-0"
|
|
157
|
+
>
|
|
158
|
+
<div class="relative border-b border-border">
|
|
159
|
+
<Magnifier
|
|
160
|
+
:size="14"
|
|
161
|
+
class="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-muted-foreground"
|
|
162
|
+
/>
|
|
163
|
+
<input
|
|
164
|
+
v-model="search"
|
|
165
|
+
type="search"
|
|
166
|
+
:placeholder="t('shared.facetpanel.search.placeholder')"
|
|
167
|
+
:aria-label="t('shared.facetpanel.search.placeholder')"
|
|
168
|
+
class="h-10 w-full bg-transparent pr-3 pl-9 text-sm outline-hidden placeholder:text-muted-foreground [&::-webkit-search-cancel-button]:hidden"
|
|
169
|
+
>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<!-- Facets left, the one being edited right. Fixed size at two facets or
|
|
173
|
+
twenty; the rail's counts are what is left of "everything at once". -->
|
|
174
|
+
<div class="flex h-72">
|
|
175
|
+
<div
|
|
176
|
+
class="w-44 shrink-0 overflow-y-auto border-r border-border p-1.5"
|
|
177
|
+
role="tablist"
|
|
178
|
+
:aria-label="t('shared.facetpanel.rail.aria')"
|
|
179
|
+
>
|
|
180
|
+
<button
|
|
181
|
+
v-for="facet in visibleFacets"
|
|
182
|
+
:key="facet.key"
|
|
183
|
+
type="button"
|
|
184
|
+
role="tab"
|
|
185
|
+
:aria-selected="facet.key === activeFacet?.key"
|
|
186
|
+
class="flex w-full cursor-pointer items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors"
|
|
187
|
+
:class="facet.key === activeFacet?.key ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground hover:bg-muted hover:text-foreground'"
|
|
188
|
+
@click="activeKey = facet.key"
|
|
189
|
+
>
|
|
190
|
+
<span class="min-w-0 flex-1 truncate">{{ facet.label }}</span>
|
|
191
|
+
<span
|
|
192
|
+
v-if="countInFacet(draft, facet.key)"
|
|
193
|
+
class="flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[0.625rem] font-semibold tabular-nums"
|
|
194
|
+
:class="facet.key === activeFacet?.key ? 'bg-primary text-primary-foreground' : 'bg-primary/15 text-primary'"
|
|
195
|
+
>{{ countInFacet(draft, facet.key) }}</span>
|
|
196
|
+
</button>
|
|
197
|
+
<p v-if="!visibleFacets.length" class="px-2.5 py-4 text-center text-sm text-muted-foreground">
|
|
198
|
+
{{ t("shared.facetpanel.empty.noFacet") }}
|
|
199
|
+
</p>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div
|
|
203
|
+
v-if="activeFacet"
|
|
204
|
+
:key="activeFacet.key"
|
|
205
|
+
role="tabpanel"
|
|
206
|
+
class="min-w-0 flex-1 overflow-y-auto p-3"
|
|
207
|
+
>
|
|
208
|
+
<!-- Loading its options. A skeleton rather than an empty list, which
|
|
209
|
+
otherwise reads as "this dimension has no values". -->
|
|
210
|
+
<div v-if="activeFacet.loading" class="space-y-1.5 p-1">
|
|
211
|
+
<div v-for="i in 4" :key="i" class="h-6 animate-pulse rounded-md bg-muted" />
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<!-- A set of values. Rows sit close and the hover band separates
|
|
215
|
+
them, which also makes each a comfortable target. -->
|
|
216
|
+
<div v-else-if="activeFacet.kind === 'check'" class="space-y-px">
|
|
217
|
+
<label
|
|
218
|
+
v-for="option in visibleOptions(activeFacet)"
|
|
219
|
+
:key="option.value"
|
|
220
|
+
class="flex items-start gap-2.5 rounded-md px-2 py-1.5 text-sm hover:bg-muted"
|
|
221
|
+
:class="option.disabled ? 'pointer-events-none opacity-45' : 'cursor-pointer'"
|
|
222
|
+
>
|
|
223
|
+
<Checkbox
|
|
224
|
+
class="mt-0.5"
|
|
225
|
+
:model-value="isTicked(activeFacet.key, option.value)"
|
|
226
|
+
:disabled="option.disabled"
|
|
227
|
+
@update:model-value="(v) => tick(activeFacet.key, option.value, v === true)"
|
|
228
|
+
/>
|
|
229
|
+
<!-- Wraps rather than truncates: a label reading "Net 30 days,
|
|
230
|
+
2% early settlement" is unusable cut off, and the pane cannot
|
|
231
|
+
grow to fit it without becoming the layout it replaced. -->
|
|
232
|
+
<span class="min-w-0 flex-1">{{ option.label }}</span>
|
|
233
|
+
<span
|
|
234
|
+
v-if="option.count !== void 0"
|
|
235
|
+
class="shrink-0 tabular-nums text-muted-foreground"
|
|
236
|
+
>{{ option.count }}</span>
|
|
237
|
+
</label>
|
|
238
|
+
<p v-if="!visibleOptions(activeFacet).length" class="px-1.5 py-6 text-center text-sm text-muted-foreground">
|
|
239
|
+
{{ query ? t("shared.facetpanel.empty.noMatch", { query: search }) : t("shared.facetpanel.empty.noOption") }}
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<!-- A hierarchy. The caret opens a branch; the box picks it. Two
|
|
244
|
+
targets, because "show me what is under Tools" and "narrow to
|
|
245
|
+
Tools" are different instructions and one control for both makes
|
|
246
|
+
every browse click a filter change. -->
|
|
247
|
+
<div v-else-if="activeFacet.kind === 'tree'" class="space-y-px">
|
|
248
|
+
<div
|
|
249
|
+
v-for="{ node, depth } in visibleNodes(activeFacet)"
|
|
250
|
+
:key="node.value"
|
|
251
|
+
class="flex items-start gap-1 rounded-md pr-2 text-sm hover:bg-muted"
|
|
252
|
+
:style="{ paddingLeft: `${depth * 18}px` }"
|
|
253
|
+
>
|
|
254
|
+
<!--
|
|
255
|
+
A chevron, at Bold weight.
|
|
256
|
+
|
|
257
|
+
Weight is the whole point here. The `▸` character it started as
|
|
258
|
+
rendered like a stray tick above the baseline; the Linear icon
|
|
259
|
+
that replaced it was two hairlines that read as a smudge beside
|
|
260
|
+
the checkbox. Bold gives the arrow enough body to be seen as a
|
|
261
|
+
control at this size without becoming the filled block a duotone
|
|
262
|
+
turns into at 15px — which would outweigh the checkbox next to
|
|
263
|
+
it, and the checkbox is what actually changes the list.
|
|
264
|
+
|
|
265
|
+
Only a branch gets one. A leaf keeps the spacer: an arrow with
|
|
266
|
+
nothing under it is a promise the row cannot keep, and the
|
|
267
|
+
absence is the cheapest way to see at a glance which rows go
|
|
268
|
+
deeper.
|
|
269
|
+
-->
|
|
270
|
+
<button
|
|
271
|
+
v-if="node.children?.length && !query"
|
|
272
|
+
type="button"
|
|
273
|
+
class="mt-1 flex size-5 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:text-primary"
|
|
274
|
+
:aria-expanded="isExpanded(activeFacet, node.value)"
|
|
275
|
+
:aria-label="isExpanded(activeFacet, node.value) ? t('shared.facetpanel.tree.collapse', { label: node.label }) : t('shared.facetpanel.tree.expand', { label: node.label })"
|
|
276
|
+
@click="toggleBranch(activeFacet, node.value)"
|
|
277
|
+
>
|
|
278
|
+
<AltArrowDown v-if="isExpanded(activeFacet, node.value)" :size="14" weight="Bold" />
|
|
279
|
+
<AltArrowRight v-else :size="14" weight="Bold" />
|
|
280
|
+
</button>
|
|
281
|
+
<span v-else class="mt-1 size-5 shrink-0" />
|
|
282
|
+
<label class="flex min-w-0 flex-1 cursor-pointer items-start gap-2.5 py-1.5">
|
|
283
|
+
<Checkbox
|
|
284
|
+
class="mt-0.5"
|
|
285
|
+
:model-value="isTicked(activeFacet.key, node.value)"
|
|
286
|
+
@update:model-value="(v) => tick(activeFacet.key, node.value, v === true)"
|
|
287
|
+
/>
|
|
288
|
+
<span class="min-w-0 flex-1">{{ node.label }}</span>
|
|
289
|
+
<span
|
|
290
|
+
v-if="node.count !== void 0"
|
|
291
|
+
class="shrink-0 tabular-nums text-muted-foreground"
|
|
292
|
+
>{{ node.count }}</span>
|
|
293
|
+
</label>
|
|
294
|
+
</div>
|
|
295
|
+
<p v-if="!visibleNodes(activeFacet).length" class="px-1.5 py-6 text-center text-sm text-muted-foreground">
|
|
296
|
+
{{ query ? t("shared.facetpanel.empty.noMatch", { query: search }) : t("shared.facetpanel.empty.noOption") }}
|
|
297
|
+
</p>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<!-- A typed string. Still a facet, still committed with the rest —
|
|
301
|
+
which is the whole reason it is not a second box in the toolbar. -->
|
|
302
|
+
<div v-else-if="activeFacet.kind === 'text'" class="p-1">
|
|
303
|
+
<Input
|
|
304
|
+
:model-value="textValue(activeFacet.key)"
|
|
305
|
+
:placeholder="activeFacet.placeholder || activeFacet.label"
|
|
306
|
+
:aria-label="activeFacet.label"
|
|
307
|
+
class="h-8 text-sm"
|
|
308
|
+
@update:model-value="(v) => setText(activeFacet.key, String(v))"
|
|
309
|
+
@keydown.enter="apply"
|
|
310
|
+
/>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<!-- A bounded range. Either side alone is a real answer — "everything
|
|
314
|
+
since the migration" has no upper bound. -->
|
|
315
|
+
<div v-else-if="activeFacet.kind === 'date-range'" class="flex items-center gap-2 p-1">
|
|
316
|
+
<input
|
|
317
|
+
type="date"
|
|
318
|
+
:value="range(activeFacet.key).from ?? ''"
|
|
319
|
+
:aria-label="t('shared.facetpanel.range.from', { label: activeFacet.label })"
|
|
320
|
+
class="h-8 w-full rounded-md border border-input bg-background px-2 text-sm shadow-xs"
|
|
321
|
+
@input="setBound(activeFacet.key, 'from', $event.target.value)"
|
|
322
|
+
>
|
|
323
|
+
<span class="text-xs text-muted-foreground">–</span>
|
|
324
|
+
<input
|
|
325
|
+
type="date"
|
|
326
|
+
:value="range(activeFacet.key).to ?? ''"
|
|
327
|
+
:aria-label="t('shared.facetpanel.range.to', { label: activeFacet.label })"
|
|
328
|
+
class="h-8 w-full rounded-md border border-input bg-background px-2 text-sm shadow-xs"
|
|
329
|
+
@input="setBound(activeFacet.key, 'to', $event.target.value)"
|
|
330
|
+
>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<p v-if="activeFacet.hint && !activeFacet.loading" class="px-1.5 pt-3 text-xs text-muted-foreground">
|
|
334
|
+
{{ activeFacet.hint }}
|
|
335
|
+
</p>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<!--
|
|
340
|
+
The commit, in a bar of its own so it sits somewhere fixed rather than
|
|
341
|
+
floating after the last facet. A ghost and a primary at the same height
|
|
342
|
+
as every other button in the product — the two flush square-cornered
|
|
343
|
+
rectangles this pattern is usually drawn with read as a different
|
|
344
|
+
product, and did so at the one place where the decision gets made.
|
|
345
|
+
|
|
346
|
+
The dot is the only thing saying the draft differs from what the list is
|
|
347
|
+
showing.
|
|
348
|
+
-->
|
|
349
|
+
<div class="flex items-center justify-between gap-2 border-t border-border bg-muted/30 px-3 py-2.5">
|
|
350
|
+
<Button variant="ghost" size="sm" :disabled="nothingToReset" @click="reset">
|
|
351
|
+
{{ t("shared.facetpanel.action.reset") }}
|
|
352
|
+
</Button>
|
|
353
|
+
<Button size="sm" @click="apply">
|
|
354
|
+
{{ t("shared.facetpanel.action.apply") }}<span v-if="dirty" class="ml-1 opacity-70">•</span>
|
|
355
|
+
</Button>
|
|
356
|
+
</div>
|
|
357
|
+
</PopoverContent>
|
|
358
|
+
</Popover>
|
|
359
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Facet, FacetSelection } from '../../lib/facets.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** Every dimension this list can be narrowed by. All of them live here. */
|
|
4
|
+
facets: Facet[];
|
|
5
|
+
/** What is applied right now. */
|
|
6
|
+
modelValue: FacetSelection;
|
|
7
|
+
size?: 'sm' | 'default';
|
|
8
|
+
};
|
|
9
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
10
|
+
reset: () => any;
|
|
11
|
+
"update:modelValue": (value: FacetSelection) => any;
|
|
12
|
+
apply: (value: FacetSelection) => any;
|
|
13
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
14
|
+
onReset?: (() => any) | undefined;
|
|
15
|
+
"onUpdate:modelValue"?: ((value: FacetSelection) => any) | undefined;
|
|
16
|
+
onApply?: ((value: FacetSelection) => any) | undefined;
|
|
17
|
+
}>, {
|
|
18
|
+
size: "sm" | "default";
|
|
19
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
@@ -6,15 +6,16 @@ type __VLS_Props = {
|
|
|
6
6
|
/** How many are set, for the badge. */
|
|
7
7
|
count?: number;
|
|
8
8
|
};
|
|
9
|
-
declare var
|
|
9
|
+
declare var __VLS_32: {};
|
|
10
10
|
type __VLS_Slots = {} & {
|
|
11
|
-
default?: (props: typeof
|
|
11
|
+
default?: (props: typeof __VLS_32) => any;
|
|
12
12
|
};
|
|
13
|
-
declare const
|
|
14
|
-
active: boolean;
|
|
13
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
15
14
|
width: string;
|
|
15
|
+
active: boolean;
|
|
16
16
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
17
|
-
declare const
|
|
17
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
18
|
+
declare const _default: typeof __VLS_export;
|
|
18
19
|
export default _default;
|
|
19
20
|
type __VLS_WithSlots<T, S> = T & {
|
|
20
21
|
new (): {
|
|
@@ -6,15 +6,16 @@ type __VLS_Props = {
|
|
|
6
6
|
/** How many are set, for the badge. */
|
|
7
7
|
count?: number;
|
|
8
8
|
};
|
|
9
|
-
declare var
|
|
9
|
+
declare var __VLS_32: {};
|
|
10
10
|
type __VLS_Slots = {} & {
|
|
11
|
-
default?: (props: typeof
|
|
11
|
+
default?: (props: typeof __VLS_32) => any;
|
|
12
12
|
};
|
|
13
|
-
declare const
|
|
14
|
-
active: boolean;
|
|
13
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
15
14
|
width: string;
|
|
15
|
+
active: boolean;
|
|
16
16
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
17
|
-
declare const
|
|
17
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
18
|
+
declare const _default: typeof __VLS_export;
|
|
18
19
|
export default _default;
|
|
19
20
|
type __VLS_WithSlots<T, S> = T & {
|
|
20
21
|
new (): {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { DataTableSavedViews } from './types.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
config: DataTableSavedViews;
|
|
4
|
+
/** Matches the row it sits in; `sm` is the 32px square its neighbours are. */
|
|
5
|
+
size?: 'sm' | 'default';
|
|
4
6
|
};
|
|
5
|
-
declare const
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
8
|
+
size: "sm" | "default";
|
|
9
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
10
|
+
declare const _default: typeof __VLS_export;
|
|
6
11
|
export default _default;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import {
|
|
2
|
+
import { Bookmark, CheckCircle, Diskette, TrashBinMinimalistic } from "#solar-icons";
|
|
3
3
|
import {
|
|
4
4
|
Button,
|
|
5
5
|
Dialog,
|
|
@@ -11,51 +11,33 @@ import {
|
|
|
11
11
|
DropdownMenu,
|
|
12
12
|
DropdownMenuContent,
|
|
13
13
|
DropdownMenuItem,
|
|
14
|
+
DropdownMenuLabel,
|
|
14
15
|
DropdownMenuSeparator,
|
|
15
16
|
DropdownMenuTrigger,
|
|
16
17
|
Input
|
|
17
18
|
} from "@revenexx/studio";
|
|
19
|
+
import { sameJson } from "../../lib/canonicalJson";
|
|
18
20
|
const props = defineProps({
|
|
19
|
-
config: { type: Object, required: true }
|
|
21
|
+
config: { type: Object, required: true },
|
|
22
|
+
size: { type: String, required: false, default: "sm" }
|
|
20
23
|
});
|
|
21
24
|
const t = useT();
|
|
25
|
+
const store = useSavedViews();
|
|
22
26
|
const views = ref([]);
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
if (typeof window === "undefined") return [];
|
|
26
|
-
const raw = window.localStorage.getItem(props.config.storageKey);
|
|
27
|
-
if (!raw) return [];
|
|
28
|
-
const parsed = JSON.parse(raw);
|
|
29
|
-
if (!Array.isArray(parsed)) return [];
|
|
30
|
-
return parsed.filter((v) => !!v && typeof v === "object" && typeof v.name === "string");
|
|
31
|
-
} catch {
|
|
32
|
-
return [];
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function persist() {
|
|
36
|
-
try {
|
|
37
|
-
if (typeof window === "undefined") return;
|
|
38
|
-
window.localStorage.setItem(props.config.storageKey, JSON.stringify(views.value));
|
|
39
|
-
} catch {
|
|
40
|
-
}
|
|
27
|
+
async function load() {
|
|
28
|
+
views.value = await store.list(props.config.storageKey);
|
|
41
29
|
}
|
|
42
|
-
onMounted(
|
|
43
|
-
|
|
44
|
-
});
|
|
45
|
-
watch(() => props.config.storageKey, () => {
|
|
46
|
-
views.value = read();
|
|
47
|
-
});
|
|
30
|
+
onMounted(load);
|
|
31
|
+
watch(() => props.config.storageKey, load);
|
|
48
32
|
const allLabel = computed(() => props.config.allLabel ?? t("shared.datatable.savedviews.all.default"));
|
|
49
|
-
|
|
50
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
51
|
-
}
|
|
52
|
-
const isDefault = computed(() => same(props.config.snapshot(), props.config.defaultSnapshot()));
|
|
33
|
+
const isDefault = computed(() => sameJson(props.config.snapshot(), props.config.defaultSnapshot()));
|
|
53
34
|
function isActive(v) {
|
|
54
|
-
return
|
|
35
|
+
return sameJson(v.snapshot, props.config.snapshot());
|
|
55
36
|
}
|
|
37
|
+
const appliedView = computed(() => views.value.find(isActive) ?? null);
|
|
38
|
+
const hasAppliedView = computed(() => appliedView.value !== null);
|
|
56
39
|
const activeLabel = computed(() => {
|
|
57
|
-
|
|
58
|
-
if (hit) return hit.name;
|
|
40
|
+
if (appliedView.value) return appliedView.value.name;
|
|
59
41
|
return isDefault.value ? allLabel.value : t("shared.datatable.savedviews.custom");
|
|
60
42
|
});
|
|
61
43
|
function applyDefault() {
|
|
@@ -66,25 +48,31 @@ function applyView(v) {
|
|
|
66
48
|
}
|
|
67
49
|
const saveOpen = ref(false);
|
|
68
50
|
const saveName = ref("");
|
|
51
|
+
const saving = ref(false);
|
|
69
52
|
function openSave() {
|
|
70
|
-
saveName.value = "";
|
|
53
|
+
saveName.value = appliedView.value?.name ?? "";
|
|
71
54
|
saveOpen.value = true;
|
|
72
55
|
}
|
|
73
|
-
function save() {
|
|
56
|
+
async function save() {
|
|
74
57
|
const name = saveName.value.trim();
|
|
75
|
-
if (!name) return;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
58
|
+
if (!name || saving.value) return;
|
|
59
|
+
saving.value = true;
|
|
60
|
+
try {
|
|
61
|
+
const entry = await store.save(props.config.storageKey, name, props.config.snapshot());
|
|
62
|
+
const at = views.value.findIndex((v) => v.name === name);
|
|
63
|
+
if (at >= 0) views.value.splice(at, 1, entry);
|
|
64
|
+
else views.value = [...views.value, entry].sort((a, b) => a.name.localeCompare(b.name));
|
|
65
|
+
saveOpen.value = false;
|
|
66
|
+
} finally {
|
|
67
|
+
saving.value = false;
|
|
68
|
+
}
|
|
82
69
|
}
|
|
83
70
|
const deleteTarget = ref(null);
|
|
84
|
-
function remove() {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
71
|
+
async function remove() {
|
|
72
|
+
const target = deleteTarget.value;
|
|
73
|
+
if (!target) return;
|
|
74
|
+
await store.remove(props.config.storageKey, target.id);
|
|
75
|
+
views.value = views.value.filter((v) => v.id !== target.id);
|
|
88
76
|
deleteTarget.value = null;
|
|
89
77
|
}
|
|
90
78
|
</script>
|
|
@@ -93,34 +81,57 @@ function remove() {
|
|
|
93
81
|
<DropdownMenu>
|
|
94
82
|
<DropdownMenuTrigger as-child>
|
|
95
83
|
<Button
|
|
96
|
-
variant="outline"
|
|
97
|
-
size="sm"
|
|
98
|
-
|
|
99
|
-
:
|
|
100
|
-
:
|
|
84
|
+
:variant="hasAppliedView ? 'default' : 'outline'"
|
|
85
|
+
:size="size === 'sm' ? 'icon-sm' : 'icon'"
|
|
86
|
+
:data-active="hasAppliedView"
|
|
87
|
+
:aria-label="t('shared.datatable.savedviews.trigger.aria', { name: activeLabel })"
|
|
88
|
+
:title="t('shared.datatable.savedviews.trigger.aria', { name: activeLabel })"
|
|
101
89
|
>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
90
|
+
<!--
|
|
91
|
+
Violet like the filter trigger and the column picker beside it, and
|
|
92
|
+
filled the same way once a stored view is in effect — the button
|
|
93
|
+
carries that state rather than a second mark inside it. On the filled
|
|
94
|
+
face the icon inherits the button's own foreground, or it would be
|
|
95
|
+
violet on violet.
|
|
96
|
+
-->
|
|
97
|
+
<span class="flex shrink-0 items-center" :class="hasAppliedView ? '' : 'text-primary'">
|
|
98
|
+
<Bookmark :class="size === 'sm' ? 'size-3.5' : 'size-4'" weight="BoldDuotone" />
|
|
99
|
+
</span>
|
|
105
100
|
</Button>
|
|
106
101
|
</DropdownMenuTrigger>
|
|
107
|
-
|
|
102
|
+
<!--
|
|
103
|
+
`align="start"`, like every other menu in this row. It was `end`, so this
|
|
104
|
+
was the one control whose menu grew to the LEFT of its trigger while the
|
|
105
|
+
market switcher's and the column picker's grew right — a menu that opens
|
|
106
|
+
the other way is one a reader has to re-find every time.
|
|
107
|
+
-->
|
|
108
|
+
<DropdownMenuContent align="start" class="w-60">
|
|
109
|
+
<!-- Named, like the market switcher's "Showing" and the column picker's
|
|
110
|
+
"Columns": a menu of bare rows does not say what it is a menu OF. -->
|
|
111
|
+
<DropdownMenuLabel>{{ t("shared.datatable.savedviews.heading") }}</DropdownMenuLabel>
|
|
112
|
+
<!--
|
|
113
|
+
A tick, not just violet text. The mark says WHICH row is in effect, and
|
|
114
|
+
a reader who cannot separate the two hues gets nothing from a colour —
|
|
115
|
+
the same reason the market switcher puts a check on its active option.
|
|
116
|
+
-->
|
|
108
117
|
<DropdownMenuItem class="gap-2" @select="applyDefault">
|
|
109
|
-
<span class="flex-1 truncate">{{ allLabel }}</span>
|
|
110
|
-
<
|
|
118
|
+
<span class="flex-1 truncate" :class="isDefault ? 'font-medium text-primary' : ''">{{ allLabel }}</span>
|
|
119
|
+
<CheckCircle v-if="isDefault" :size="14" weight="Bold" class="shrink-0 text-primary" />
|
|
111
120
|
</DropdownMenuItem>
|
|
112
121
|
<template v-if="views.length">
|
|
113
122
|
<DropdownMenuSeparator />
|
|
114
123
|
<DropdownMenuItem
|
|
115
124
|
v-for="v in views"
|
|
116
|
-
:key="v.
|
|
125
|
+
:key="v.id"
|
|
117
126
|
class="gap-2"
|
|
127
|
+
:aria-current="isActive(v) ? 'true' : void 0"
|
|
118
128
|
@select="applyView(v)"
|
|
119
129
|
>
|
|
120
|
-
<span class="flex-1 truncate">{{ v.name }}</span>
|
|
130
|
+
<span class="min-w-0 flex-1 truncate" :class="isActive(v) ? 'font-medium text-primary' : ''">{{ v.name }}</span>
|
|
131
|
+
<CheckCircle v-if="isActive(v)" :size="14" weight="Bold" class="shrink-0 text-primary" />
|
|
121
132
|
<span
|
|
122
133
|
role="button"
|
|
123
|
-
class="text-muted-foreground transition-colors hover:text-rose-600"
|
|
134
|
+
class="shrink-0 text-muted-foreground transition-colors hover:text-rose-600"
|
|
124
135
|
:aria-label="t('shared.datatable.savedviews.delete.aria', { name: v.name })"
|
|
125
136
|
@click.stop="deleteTarget = v"
|
|
126
137
|
@pointerdown.stop.prevent
|
|
@@ -130,9 +141,18 @@ function remove() {
|
|
|
130
141
|
</DropdownMenuItem>
|
|
131
142
|
</template>
|
|
132
143
|
<DropdownMenuSeparator />
|
|
133
|
-
<DropdownMenuItem @select="openSave">
|
|
134
|
-
|
|
144
|
+
<DropdownMenuItem class="gap-2" @select="openSave">
|
|
145
|
+
<Diskette :size="14" class="shrink-0 text-muted-foreground" />
|
|
146
|
+
<span class="flex-1">{{ t("shared.datatable.savedviews.action.savecurrent") }}</span>
|
|
135
147
|
</DropdownMenuItem>
|
|
148
|
+
<!--
|
|
149
|
+
Where the last read or write actually landed. One line, and only when
|
|
150
|
+
the answer is the surprising one: a control that claims nothing is
|
|
151
|
+
better than one that claims a sync it did not get.
|
|
152
|
+
-->
|
|
153
|
+
<p v-if="!store.durable" class="px-2 pt-1.5 pb-1 text-xs text-muted-foreground">
|
|
154
|
+
{{ t("shared.datatable.savedviews.local.note") }}
|
|
155
|
+
</p>
|
|
136
156
|
</DropdownMenuContent>
|
|
137
157
|
</DropdownMenu>
|
|
138
158
|
|
|
@@ -158,7 +178,7 @@ function remove() {
|
|
|
158
178
|
<Button variant="outline" @click="saveOpen = false">
|
|
159
179
|
{{ t("shared.datatable.savedviews.savedialog.action.cancel") }}
|
|
160
180
|
</Button>
|
|
161
|
-
<Button :disabled="!saveName.trim()" @click="save">
|
|
181
|
+
<Button :disabled="!saveName.trim() || saving" @click="save">
|
|
162
182
|
{{ t("shared.datatable.savedviews.savedialog.action.save") }}
|
|
163
183
|
</Button>
|
|
164
184
|
</DialogFooter>
|