@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,72 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { CloseCircle } from "#solar-icons";
|
|
4
|
+
import { dateTimeFormat } from "../../lib/formatBridge";
|
|
5
|
+
import { appliedFacets, clearFacetValue } from "../../lib/facets";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
facets: { type: Array, required: true },
|
|
8
|
+
modelValue: { type: Object, required: true },
|
|
9
|
+
showFacet: { type: Boolean, required: false }
|
|
10
|
+
});
|
|
11
|
+
const emit = defineEmits(["update:modelValue"]);
|
|
12
|
+
const t = useT();
|
|
13
|
+
function formatRange(range) {
|
|
14
|
+
const fmt = dateTimeFormat({ dateStyle: "medium" });
|
|
15
|
+
const one = (value) => {
|
|
16
|
+
if (!value) return null;
|
|
17
|
+
const date = new Date(value);
|
|
18
|
+
return Number.isNaN(date.getTime()) ? value : fmt.format(date);
|
|
19
|
+
};
|
|
20
|
+
const from = one(range.from);
|
|
21
|
+
const to = one(range.to);
|
|
22
|
+
if (from && to) return `${from} \u2013 ${to}`;
|
|
23
|
+
if (from) return t("shared.facetchips.range.since", { date: from });
|
|
24
|
+
return t("shared.facetchips.range.until", { date: to ?? "" });
|
|
25
|
+
}
|
|
26
|
+
const chips = computed(() => appliedFacets(props.facets, props.modelValue, {
|
|
27
|
+
range: formatRange,
|
|
28
|
+
text: (value) => `\u201C${value}\u201D`
|
|
29
|
+
}));
|
|
30
|
+
function remove(chip) {
|
|
31
|
+
emit("update:modelValue", clearFacetValue(props.modelValue, chip.facetKey, chip.value));
|
|
32
|
+
}
|
|
33
|
+
function clearAll() {
|
|
34
|
+
emit("update:modelValue", {});
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<div
|
|
40
|
+
v-if="chips.length"
|
|
41
|
+
class="flex flex-wrap items-center gap-1.5 border-t border-black/[0.04] bg-white px-4 py-2.5"
|
|
42
|
+
role="group"
|
|
43
|
+
:aria-label="t('shared.facetchips.aria')"
|
|
44
|
+
>
|
|
45
|
+
<span
|
|
46
|
+
v-for="chip in chips"
|
|
47
|
+
:key="chip.id"
|
|
48
|
+
class="inline-flex items-center gap-1.5 rounded-full bg-primary/10 py-0.5 pr-1 pl-2.5 text-xs font-medium text-primary"
|
|
49
|
+
>
|
|
50
|
+
<span v-if="showFacet" class="font-normal opacity-60">{{ chip.facetLabel }}</span>
|
|
51
|
+
<span class="max-w-60 truncate">{{ chip.label }}</span>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
class="cursor-pointer rounded-full opacity-60 transition-opacity hover:opacity-100"
|
|
55
|
+
:aria-label="t('shared.facetchips.remove', { facet: chip.facetLabel, value: chip.label })"
|
|
56
|
+
@click="remove(chip)"
|
|
57
|
+
>
|
|
58
|
+
<CloseCircle :size="13" weight="Bold" />
|
|
59
|
+
</button>
|
|
60
|
+
</span>
|
|
61
|
+
<!-- Only past two. With one chip beside it, "clear all" is a second control
|
|
62
|
+
for what the cross next to it already does. -->
|
|
63
|
+
<button
|
|
64
|
+
v-if="chips.length > 1"
|
|
65
|
+
type="button"
|
|
66
|
+
class="ml-1 cursor-pointer text-xs text-muted-foreground underline decoration-from-font underline-offset-2 hover:text-foreground"
|
|
67
|
+
@click="clearAll"
|
|
68
|
+
>
|
|
69
|
+
{{ t("shared.facetchips.clearAll") }}
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Facet, FacetSelection } from '../../lib/facets.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
facets: Facet[];
|
|
4
|
+
modelValue: FacetSelection;
|
|
5
|
+
/**
|
|
6
|
+
* Say which facet each chip belongs to. On where "Draft" could be a status or
|
|
7
|
+
* a lifecycle; off where the values speak for themselves.
|
|
8
|
+
*/
|
|
9
|
+
showFacet?: boolean;
|
|
10
|
+
};
|
|
11
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:modelValue": (value: FacetSelection) => any;
|
|
13
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
14
|
+
"onUpdate:modelValue"?: ((value: FacetSelection) => any) | undefined;
|
|
15
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
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 _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
10
|
+
apply: (value: FacetSelection) => any;
|
|
11
|
+
"update:modelValue": (value: FacetSelection) => any;
|
|
12
|
+
reset: () => any;
|
|
13
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
14
|
+
onApply?: ((value: FacetSelection) => any) | undefined;
|
|
15
|
+
"onUpdate:modelValue"?: ((value: FacetSelection) => any) | undefined;
|
|
16
|
+
onReset?: (() => any) | undefined;
|
|
17
|
+
}>, {
|
|
18
|
+
size: "sm" | "default";
|
|
19
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
export default _default;
|
|
@@ -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,20 @@
|
|
|
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 _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
10
|
+
apply: (value: FacetSelection) => any;
|
|
11
|
+
"update:modelValue": (value: FacetSelection) => any;
|
|
12
|
+
reset: () => any;
|
|
13
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
14
|
+
onApply?: ((value: FacetSelection) => any) | undefined;
|
|
15
|
+
"onUpdate:modelValue"?: ((value: FacetSelection) => any) | undefined;
|
|
16
|
+
onReset?: (() => any) | undefined;
|
|
17
|
+
}>, {
|
|
18
|
+
size: "sm" | "default";
|
|
19
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
export default _default;
|
|
@@ -1,6 +1,10 @@
|
|
|
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 _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
7
|
+
declare const _default: 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>;
|
|
6
10
|
export default _default;
|