@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.
Files changed (85) hide show
  1. package/dist/module.d.mts +4 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +9 -0
  4. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.d.vue.ts +15 -0
  5. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +39 -0
  6. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue.d.ts +15 -0
  7. package/dist/runtime/components/Dashboard/DashboardControls.d.vue.ts +57 -0
  8. package/dist/runtime/components/Dashboard/DashboardControls.vue +209 -0
  9. package/dist/runtime/components/Dashboard/DashboardControls.vue.d.ts +57 -0
  10. package/dist/runtime/components/Dashboard/DashboardGrid.d.vue.ts +16 -0
  11. package/dist/runtime/components/Dashboard/DashboardGrid.vue +87 -0
  12. package/dist/runtime/components/Dashboard/DashboardGrid.vue.d.ts +16 -0
  13. package/dist/runtime/components/Dashboard/DashboardNameDialog.d.vue.ts +18 -0
  14. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +61 -0
  15. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue.d.ts +18 -0
  16. package/dist/runtime/components/Dashboard/DashboardPickerDialog.d.vue.ts +14 -0
  17. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +100 -0
  18. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue.d.ts +14 -0
  19. package/dist/runtime/components/Dashboard/DashboardShareDialog.d.vue.ts +27 -0
  20. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +155 -0
  21. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue.d.ts +27 -0
  22. package/dist/runtime/components/Dashboard/DashboardTopBar.d.vue.ts +59 -0
  23. package/dist/runtime/components/Dashboard/DashboardTopBar.vue +79 -0
  24. package/dist/runtime/components/Dashboard/DashboardTopBar.vue.d.ts +59 -0
  25. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.d.vue.ts +42 -0
  26. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +153 -0
  27. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue.d.ts +42 -0
  28. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +27 -0
  29. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +80 -0
  30. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +27 -0
  31. package/dist/runtime/components/Dashboard/DashboardWidgetHost.d.vue.ts +6 -0
  32. package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue +30 -0
  33. package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue.d.ts +6 -0
  34. package/dist/runtime/components/Dashboard/DashboardWidgetIcon.d.vue.ts +8 -0
  35. package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue +13 -0
  36. package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue.d.ts +8 -0
  37. package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.d.vue.ts +23 -0
  38. package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue +140 -0
  39. package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue.d.ts +23 -0
  40. package/dist/runtime/components/Dashboard/DashboardWidgetPreview.d.vue.ts +22 -0
  41. package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue +124 -0
  42. package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue.d.ts +22 -0
  43. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +39 -3
  44. package/dist/runtime/components/DataTable/DataTable.vue +306 -32
  45. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +39 -3
  46. package/dist/runtime/components/DataTable/FacetChips.d.vue.ts +16 -0
  47. package/dist/runtime/components/DataTable/FacetChips.vue +72 -0
  48. package/dist/runtime/components/DataTable/FacetChips.vue.d.ts +16 -0
  49. package/dist/runtime/components/DataTable/FacetPanel.d.vue.ts +20 -0
  50. package/dist/runtime/components/DataTable/FacetPanel.vue +359 -0
  51. package/dist/runtime/components/DataTable/FacetPanel.vue.d.ts +20 -0
  52. package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +5 -1
  53. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +83 -63
  54. package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +5 -1
  55. package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +5 -1
  56. package/dist/runtime/components/DataTable/SortMenu.vue +40 -12
  57. package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +5 -1
  58. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +2 -2
  59. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +2 -2
  60. package/dist/runtime/components/Media/MediaUploader.d.vue.ts +2 -2
  61. package/dist/runtime/components/Media/MediaUploader.vue.d.ts +2 -2
  62. package/dist/runtime/components/Shared/SortHeader.vue +6 -2
  63. package/dist/runtime/components/Shared/TabBar.d.vue.ts +23 -7
  64. package/dist/runtime/components/Shared/TabBar.vue +3 -2
  65. package/dist/runtime/components/Shared/TabBar.vue.d.ts +23 -7
  66. package/dist/runtime/composables/useDashboards.d.ts +27 -0
  67. package/dist/runtime/composables/useDashboards.js +143 -0
  68. package/dist/runtime/composables/useSavedViews.d.ts +44 -0
  69. package/dist/runtime/composables/useSavedViews.js +44 -0
  70. package/dist/runtime/composables/useStickyListView.d.ts +60 -0
  71. package/dist/runtime/composables/useStickyListView.js +32 -0
  72. package/dist/runtime/lib/canonicalJson.d.ts +43 -0
  73. package/dist/runtime/lib/canonicalJson.js +38 -0
  74. package/dist/runtime/lib/dashboardIcons.d.ts +4 -0
  75. package/dist/runtime/lib/dashboardIcons.js +50 -0
  76. package/dist/runtime/lib/facets.d.ts +200 -0
  77. package/dist/runtime/lib/facets.js +171 -0
  78. package/dist/runtime/lib/gridTracks.d.ts +55 -0
  79. package/dist/runtime/lib/gridTracks.js +29 -0
  80. package/dist/runtime/lib/widgetRegistry.d.ts +124 -0
  81. package/dist/runtime/lib/widgetRegistry.js +66 -0
  82. package/dist/runtime/types/dashboard.d.ts +95 -0
  83. package/dist/runtime/types/dashboard.js +2 -0
  84. package/dist/types.d.mts +8 -0
  85. 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<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
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;