@leavepulse/ui 0.74.2 → 0.76.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/README.md +44 -0
- package/dist/assets/{LpShortcutsDialog-BUp_BX4s.css → LpShortcutsDialog-C6uzPeMf.css} +7 -7
- package/dist/assets/{LpTable-DHvorDiU.css → LpTable-Bv-EcQs7.css} +19 -19
- package/dist/assets/LpTabs-CJ0rYQbx.css +18 -0
- package/dist/chunks/{LpAddressInput-Al3HT5ey.js → LpAddressInput-De_LTYM7.js} +1 -1
- package/dist/chunks/{LpAppShell-CpSSm5WT.js → LpAppShell-Cpy_t1U_.js} +1 -1
- package/dist/chunks/{LpConfirmDialog-DY2VjDji.js → LpConfirmDialog-CphJR6Q9.js} +4 -2
- package/dist/chunks/{LpConfirmHost-DvSZTJ4N.js → LpConfirmHost-QSJ0zl63.js} +1 -1
- package/dist/chunks/{LpDrawer-RRamxbLV.js → LpDrawer-DQZixOqJ.js} +13 -0
- package/dist/chunks/{LpModal-BnjZ2iem.js → LpModal-C0tGg_D_.js} +13 -0
- package/dist/chunks/{LpModalNav-DQpV2kdn.js → LpModalNav-iDDtT9c1.js} +2 -2
- package/dist/chunks/{LpSidebar-BoNLKgh-.js → LpSidebar-D6zO8vLm.js} +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpConfirmDialog.vue.d.ts +4 -0
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpDrawer.vue.d.ts +16 -0
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpModal.vue.d.ts +9 -0
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.d.ts +4 -0
- package/dist/components/LpShortcutsDialog.vue.js +6 -4
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpTable.vue.d.ts +37 -0
- package/dist/components/LpTable.vue.js +34 -15
- package/dist/components/LpTabs.vue.d.ts +9 -0
- package/dist/components/LpTabs.vue.js +18 -4
- package/dist/components/tableFilter.d.ts +28 -0
- package/dist/components/tableFilter.js +43 -0
- package/dist/composables/overlayHistory.d.ts +33 -0
- package/dist/composables/overlayHistory.js +237 -0
- package/dist/composables/useOverlayHistory.d.ts +45 -0
- package/dist/composables/useOverlayHistory.js +84 -0
- package/dist/composables/useTableFilterChoice.d.ts +16 -0
- package/dist/composables/useTableFilterChoice.js +34 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +18 -8
- package/package.json +2 -1
- package/src/components/LpConfirmDialog.vue +5 -1
- package/src/components/LpDrawer.vue +21 -0
- package/src/components/LpModal.vue +21 -1
- package/src/components/LpShortcutsDialog.vue +5 -1
- package/src/components/LpTable.vue +86 -23
- package/src/components/LpTabs.vue +26 -2
- package/src/components/tableFilter.ts +66 -0
- package/src/composables/overlayHistory.ts +381 -0
- package/src/composables/useOverlayHistory.ts +173 -0
- package/src/composables/useTableFilterChoice.ts +51 -0
- package/src/index.ts +6 -0
- package/dist/assets/LpTabs-CDchzoRL.css +0 -18
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { inject, computed, toValue, watch, onBeforeUnmount, provide, nextTick } from "vue";
|
|
2
|
+
import { isLive, pushEntry, dropEntry, setHistoryRouter } from "./overlayHistory.js";
|
|
3
|
+
const MODE = /* @__PURE__ */ Symbol("lp-overlay-history");
|
|
4
|
+
const SCOPE = /* @__PURE__ */ Symbol("lp-overlay-history-scope");
|
|
5
|
+
function provideOverlayHistory(mode = true, options = {}) {
|
|
6
|
+
provide(MODE, mode);
|
|
7
|
+
if (options.router) setHistoryRouter(options.router);
|
|
8
|
+
}
|
|
9
|
+
function useOverlayHistory(options) {
|
|
10
|
+
const inherited = inject(MODE, false);
|
|
11
|
+
const mode = computed(() => options.mode() ?? toValue(inherited));
|
|
12
|
+
const entry = {
|
|
13
|
+
alive: true,
|
|
14
|
+
back: () => {
|
|
15
|
+
void options.close().then(
|
|
16
|
+
() => nextTick(() => {
|
|
17
|
+
if (entry.alive && options.open() && !isLive(entry)) pushEntry(entry);
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
},
|
|
21
|
+
forward: () => {
|
|
22
|
+
if (mode.value !== "reopen") return false;
|
|
23
|
+
options.reopen();
|
|
24
|
+
void nextTick(() => {
|
|
25
|
+
if (!options.open() && isLive(entry)) dropEntry(entry);
|
|
26
|
+
});
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
navigated: () => {
|
|
30
|
+
if (options.open()) options.navigated();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
watch(
|
|
34
|
+
() => [options.open(), mode.value !== false],
|
|
35
|
+
([open, enabled]) => {
|
|
36
|
+
if (open && enabled) {
|
|
37
|
+
if (!isLive(entry)) pushEntry(entry);
|
|
38
|
+
} else if (isLive(entry)) {
|
|
39
|
+
dropEntry(entry);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{ immediate: true }
|
|
43
|
+
);
|
|
44
|
+
onBeforeUnmount(() => {
|
|
45
|
+
entry.alive = false;
|
|
46
|
+
if (isLive(entry)) dropEntry(entry);
|
|
47
|
+
});
|
|
48
|
+
provide(SCOPE, { active: () => mode.value !== false && options.open() && isLive(entry) });
|
|
49
|
+
}
|
|
50
|
+
function useStepHistory(options) {
|
|
51
|
+
const scope = inject(SCOPE, null);
|
|
52
|
+
const enabled = () => {
|
|
53
|
+
const own = options.mode();
|
|
54
|
+
if (own === false) return false;
|
|
55
|
+
if (scope) return scope.active();
|
|
56
|
+
return own === true;
|
|
57
|
+
};
|
|
58
|
+
const entries = [];
|
|
59
|
+
function record(from, to) {
|
|
60
|
+
if (from === to || !enabled()) return;
|
|
61
|
+
const entry = {
|
|
62
|
+
alive: true,
|
|
63
|
+
back: () => options.set(from),
|
|
64
|
+
forward: () => {
|
|
65
|
+
if (!enabled() || !options.has(to)) return false;
|
|
66
|
+
options.set(to);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
entries.splice(0, entries.length, ...entries.filter(isLive), entry);
|
|
71
|
+
pushEntry(entry);
|
|
72
|
+
}
|
|
73
|
+
onBeforeUnmount(() => {
|
|
74
|
+
for (const entry of entries) entry.alive = false;
|
|
75
|
+
const first = entries.find(isLive);
|
|
76
|
+
if (first) dropEntry(first);
|
|
77
|
+
});
|
|
78
|
+
return { record };
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
provideOverlayHistory,
|
|
82
|
+
useOverlayHistory,
|
|
83
|
+
useStepHistory
|
|
84
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type MaybeRefOrGetter, type Ref, type WritableComputedRef } from "vue";
|
|
2
|
+
import { type TableFilters } from "../components/tableFilter";
|
|
3
|
+
/**
|
|
4
|
+
* One column of LpTable's header filters as a single choice, for a control
|
|
5
|
+
* above the table (LpSegmented, LpSelect, a row of stat tiles) that filters
|
|
6
|
+
* the same thing as the column's funnel. Both then read and write one state:
|
|
7
|
+
* picking a choice sets the funnel, and narrowing the funnel to a choice's
|
|
8
|
+
* values lights that choice up.
|
|
9
|
+
*
|
|
10
|
+
* `choices` maps each choice to the values it shows; `null` is the choice
|
|
11
|
+
* that shows everything. `values` is the column's vocabulary, by default every
|
|
12
|
+
* value the choices name. The ref reads `undefined` while the funnel holds a
|
|
13
|
+
* mix no choice describes, so the control shows nothing picked rather than a
|
|
14
|
+
* choice that is not what the table shows.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useTableFilterChoice<C extends string>(filters: Ref<TableFilters>, column: string, choices: Record<C, readonly unknown[] | null>, values?: MaybeRefOrGetter<readonly unknown[]>): WritableComputedRef<C | undefined>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { computed, toValue } from "vue";
|
|
2
|
+
import { showOnly, shownValues } from "../components/tableFilter.js";
|
|
3
|
+
function useTableFilterChoice(filters, column, choices, values) {
|
|
4
|
+
const entries = Object.entries(choices);
|
|
5
|
+
const vocabulary = computed(() => {
|
|
6
|
+
const own = values === void 0 ? void 0 : toValue(values);
|
|
7
|
+
return own ?? [...new Set(entries.flatMap(([, shown]) => shown ?? []))];
|
|
8
|
+
});
|
|
9
|
+
return computed({
|
|
10
|
+
get: () => {
|
|
11
|
+
const all = vocabulary.value;
|
|
12
|
+
const shown = new Set(shownValues(all, filters.value[column]));
|
|
13
|
+
for (const [choice, keep] of entries) {
|
|
14
|
+
if (keep === null) {
|
|
15
|
+
if (shown.size === all.length) return choice;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (keep.length === shown.size && keep.every((v) => shown.has(v))) return choice;
|
|
19
|
+
}
|
|
20
|
+
return void 0;
|
|
21
|
+
},
|
|
22
|
+
set: (choice) => {
|
|
23
|
+
const keep = choice === void 0 ? null : choices[choice];
|
|
24
|
+
const hidden = keep === null ? [] : showOnly(vocabulary.value, keep);
|
|
25
|
+
const next = { ...filters.value };
|
|
26
|
+
delete next[column];
|
|
27
|
+
if (hidden.length) next[column] = hidden;
|
|
28
|
+
filters.value = next;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
useTableFilterChoice
|
|
34
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -155,6 +155,9 @@ export type { SortState, TableColumn, TablePage, TableQuery } from "./components
|
|
|
155
155
|
export type { TableLabels } from "./components/LpTable.vue";
|
|
156
156
|
export type { TableExportOptions } from "./components/LpTable.vue";
|
|
157
157
|
export { compareValues } from "./components/tableSort";
|
|
158
|
+
export { filterRows, showOnly, shownValues } from "./components/tableFilter";
|
|
159
|
+
export type { TableFilters } from "./components/tableFilter";
|
|
160
|
+
export { useTableFilterChoice } from "./composables/useTableFilterChoice";
|
|
158
161
|
export { resolveRowMenu } from "./components/tableRowMenu";
|
|
159
162
|
export type { TableRowMenu, TableRowMenuResult } from "./components/tableRowMenu";
|
|
160
163
|
export { default as LpTableOfContents } from "./components/LpTableOfContents.vue";
|
|
@@ -216,6 +219,9 @@ export type { UseGamepadNavOptions } from "./composables/useGamepadNav";
|
|
|
216
219
|
export { focusableIn, nextInDirection } from "./composables/spatialNav";
|
|
217
220
|
export type { Box, Direction } from "./composables/spatialNav";
|
|
218
221
|
export { provideGlass, useGlass } from "./composables/useGlass";
|
|
222
|
+
export { provideOverlayHistory, useOverlayHistory, useStepHistory } from "./composables/useOverlayHistory";
|
|
223
|
+
export type { OverlayHistoryMode, OverlayHistoryOptions, ProvideOverlayHistoryOptions, StepHistoryOptions } from "./composables/useOverlayHistory";
|
|
224
|
+
export type { HistoryRouter } from "./composables/overlayHistory";
|
|
219
225
|
export { LP_CONFIG, lpConfig, mergeConfig, provideLpConfig, useLabels, useLinkComponent, useLpConfig } from "./composables/useLpConfig";
|
|
220
226
|
export type { KitStorage, LabelScope, LpConfig } from "./composables/useLpConfig";
|
|
221
227
|
export { browserStorage, useKitStorage, usePersisted } from "./composables/usePersisted";
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { _ } from "./chunks/LayoutCanvas-CNOAUk7D.js";
|
|
2
|
-
import { _ as _2 } from "./chunks/LpAddressInput-
|
|
2
|
+
import { _ as _2 } from "./chunks/LpAddressInput-De_LTYM7.js";
|
|
3
3
|
import { _ as _3 } from "./chunks/LpMapPicker-DRGgh89B.js";
|
|
4
4
|
import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
|
|
5
5
|
import { _ as _4 } from "./chunks/LpActionBar-BP1ld7-S.js";
|
|
6
6
|
import { _ as _5 } from "./chunks/LpAlert-n5XPAZfv.js";
|
|
7
|
-
import { _ as _6 } from "./chunks/LpAppShell-
|
|
7
|
+
import { _ as _6 } from "./chunks/LpAppShell-Cpy_t1U_.js";
|
|
8
8
|
import { _ as _7 } from "./chunks/LpAutocomplete-DOu7FN5P.js";
|
|
9
9
|
import { _ as _8 } from "./chunks/LpAvatar-Cg6Az3wz.js";
|
|
10
10
|
import { _ as _9 } from "./chunks/LpBadge-wUYLN4U9.js";
|
|
@@ -30,8 +30,8 @@ import { default as default6 } from "./components/LpPaletteStats.vue.js";
|
|
|
30
30
|
import { default as default7 } from "./components/LpPaletteSwatches.vue.js";
|
|
31
31
|
import { default as default8 } from "./components/LpKeys.vue.js";
|
|
32
32
|
import { comboKeys, comboSteps } from "./components/keys.js";
|
|
33
|
-
import { _ as _18 } from "./chunks/LpConfirmDialog-
|
|
34
|
-
import { _ as _19 } from "./chunks/LpConfirmHost-
|
|
33
|
+
import { _ as _18 } from "./chunks/LpConfirmDialog-CphJR6Q9.js";
|
|
34
|
+
import { _ as _19 } from "./chunks/LpConfirmHost-QSJ0zl63.js";
|
|
35
35
|
import { confirm, useConfirm } from "./components/confirm.js";
|
|
36
36
|
import { _ as _20 } from "./chunks/LpContextMenu-rx2tCjwk.js";
|
|
37
37
|
import { _ as _21 } from "./chunks/LpDatePicker-CA0QTfBC.js";
|
|
@@ -40,7 +40,7 @@ import { _ as _23 } from "./chunks/LpRangeCalendar-CYYUYqJ7.js";
|
|
|
40
40
|
import { clampRange, dayAllowed, defaultDatePresets, defaultRangePresets, formatRange, normalizeRange, sameRange } from "./components/dateRange.js";
|
|
41
41
|
import { _ as _24 } from "./chunks/LpDisclosure-Cpa7Hc5T.js";
|
|
42
42
|
import { _ as _25 } from "./chunks/LpDivider-CUX46KR0.js";
|
|
43
|
-
import { _ as _26 } from "./chunks/LpDrawer-
|
|
43
|
+
import { _ as _26 } from "./chunks/LpDrawer-DQZixOqJ.js";
|
|
44
44
|
import { _ as _27 } from "./chunks/LpDropdownMenu-Dl7Z4ZhS.js";
|
|
45
45
|
import { default as default9 } from "./components/LpDropzone.vue.js";
|
|
46
46
|
import { _ as _28 } from "./chunks/LpEmptyState-EQHiRYhG.js";
|
|
@@ -59,8 +59,8 @@ import { _ as _36 } from "./chunks/LpLink-Krkjhe_y.js";
|
|
|
59
59
|
import { _ as _37 } from "./chunks/LpLogViewer-D25jM2_I.js";
|
|
60
60
|
import { countLevels, matchPositions, matchesTerm, ringWindow, stepMatch } from "./components/logSearch.js";
|
|
61
61
|
import { _ as _38 } from "./chunks/LpMediaGallery-Bkram-MO.js";
|
|
62
|
-
import { _ as _39 } from "./chunks/LpModal-
|
|
63
|
-
import { _ as _40 } from "./chunks/LpModalNav-
|
|
62
|
+
import { _ as _39 } from "./chunks/LpModal-C0tGg_D_.js";
|
|
63
|
+
import { _ as _40 } from "./chunks/LpModalNav-iDDtT9c1.js";
|
|
64
64
|
import { _ as _41 } from "./chunks/LpNotificationBell-3244LUVc.js";
|
|
65
65
|
import { _ as _42 } from "./chunks/LpNumberField-e0IBkWU1.js";
|
|
66
66
|
import { _ as _43 } from "./chunks/LpTimeField-BawSVsmt.js";
|
|
@@ -86,7 +86,7 @@ import { _ as _60 } from "./chunks/LpShift-CCq7dofl.js";
|
|
|
86
86
|
import { _ as _61 } from "./chunks/LpSwap-C96c0LWh.js";
|
|
87
87
|
import { _ as _62 } from "./chunks/LpHintExample-9Py0wc-L.js";
|
|
88
88
|
import { default as default11 } from "./components/LpShortcutsDialog.vue.js";
|
|
89
|
-
import { _ as _63 } from "./chunks/LpSidebar-
|
|
89
|
+
import { _ as _63 } from "./chunks/LpSidebar-D6zO8vLm.js";
|
|
90
90
|
import { _ as _64 } from "./chunks/LpContextSwitcher-Dwh6dyID.js";
|
|
91
91
|
import { _ as _65 } from "./chunks/LpSkeleton-BB6XIV-w.js";
|
|
92
92
|
import { _ as _66 } from "./chunks/LpSlider-DxYoCJJO.js";
|
|
@@ -99,6 +99,8 @@ import { _ as _71 } from "./chunks/LpStepper-DpsocBNJ.js";
|
|
|
99
99
|
import { _ as _72 } from "./chunks/LpSwitch-BnXd1llc.js";
|
|
100
100
|
import { default as default12 } from "./components/LpTable.vue.js";
|
|
101
101
|
import { compareValues } from "./components/tableSort.js";
|
|
102
|
+
import { filterRows, showOnly, shownValues } from "./components/tableFilter.js";
|
|
103
|
+
import { useTableFilterChoice } from "./composables/useTableFilterChoice.js";
|
|
102
104
|
import { resolveRowMenu } from "./components/tableRowMenu.js";
|
|
103
105
|
import { _ as _73 } from "./chunks/LpTableOfContents-BN5zvpnx.js";
|
|
104
106
|
import { default as default13 } from "./components/LpTabs.vue.js";
|
|
@@ -132,6 +134,7 @@ import { anyModalOpen, topModalElement, useModalLayer } from "./components/modal
|
|
|
132
134
|
import { useGamepadNav } from "./composables/useGamepadNav.js";
|
|
133
135
|
import { focusableIn, nextInDirection } from "./composables/spatialNav.js";
|
|
134
136
|
import { provideGlass, useGlass } from "./composables/useGlass.js";
|
|
137
|
+
import { provideOverlayHistory, useOverlayHistory, useStepHistory } from "./composables/useOverlayHistory.js";
|
|
135
138
|
import { LP_CONFIG, lpConfig, mergeConfig, provideLpConfig, useLabels, useLinkComponent, useLpConfig } from "./composables/useLpConfig.js";
|
|
136
139
|
import { browserStorage, useKitStorage, usePersisted } from "./composables/usePersisted.js";
|
|
137
140
|
import { useCloseGuard } from "./composables/useCloseGuard.js";
|
|
@@ -469,6 +472,7 @@ export {
|
|
|
469
472
|
fileNameOf,
|
|
470
473
|
fileStem,
|
|
471
474
|
filterCountries,
|
|
475
|
+
filterRows,
|
|
472
476
|
findHotkeyConflicts,
|
|
473
477
|
fitChips,
|
|
474
478
|
flagEmoji,
|
|
@@ -568,6 +572,7 @@ export {
|
|
|
568
572
|
provideGlass,
|
|
569
573
|
provideHintSystem,
|
|
570
574
|
provideLpConfig,
|
|
575
|
+
provideOverlayHistory,
|
|
571
576
|
pseudoLocalize,
|
|
572
577
|
pushRecent,
|
|
573
578
|
quotaSegments,
|
|
@@ -609,6 +614,8 @@ export {
|
|
|
609
614
|
setActiveTab,
|
|
610
615
|
s as setGlassBackdrop,
|
|
611
616
|
settledCells,
|
|
617
|
+
showOnly,
|
|
618
|
+
shownValues,
|
|
612
619
|
sortNodes,
|
|
613
620
|
splitTags,
|
|
614
621
|
splitTime,
|
|
@@ -665,6 +672,7 @@ export {
|
|
|
665
672
|
useModalLayer,
|
|
666
673
|
useNow,
|
|
667
674
|
useOptimisticToggle,
|
|
675
|
+
useOverlayHistory,
|
|
668
676
|
usePaintSelect,
|
|
669
677
|
usePaletteCommands,
|
|
670
678
|
u3 as usePaletteTheme,
|
|
@@ -680,6 +688,8 @@ export {
|
|
|
680
688
|
useSettled,
|
|
681
689
|
useShift,
|
|
682
690
|
useSpotlight,
|
|
691
|
+
useStepHistory,
|
|
692
|
+
useTableFilterChoice,
|
|
683
693
|
u as useTheme,
|
|
684
694
|
u2 as useThemeStudio,
|
|
685
695
|
useTilt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leavepulse/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "LeavePulse token-driven Vue component kit.",
|
|
6
6
|
"repository": {
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"vite-plugin-lib-inject-css": "^2.2.2",
|
|
102
102
|
"vue": "3.5.31",
|
|
103
103
|
"vue-component-meta": "3.3.5",
|
|
104
|
+
"vue-router": "4.6.4",
|
|
104
105
|
"vue-tsc": "^3.0.0"
|
|
105
106
|
},
|
|
106
107
|
"dependencies": {
|
|
@@ -5,6 +5,7 @@ import { useLabels } from "../composables/useLpConfig"
|
|
|
5
5
|
import LpButton from "./LpButton.vue"
|
|
6
6
|
import LpIcon from "./LpIcon.vue"
|
|
7
7
|
import LpModal from "./LpModal.vue"
|
|
8
|
+
import type { OverlayHistoryMode } from "../composables/useOverlayHistory"
|
|
8
9
|
|
|
9
10
|
export type ConfirmLabels = {
|
|
10
11
|
confirm: string
|
|
@@ -54,9 +55,11 @@ const props = withDefaults(
|
|
|
54
55
|
action?: () => unknown
|
|
55
56
|
/** Frosted glass panel, passed to LpModal. Follows `provideGlass` when unset. */
|
|
56
57
|
glass?: boolean
|
|
58
|
+
/** Browser history for the dialog, passed to LpModal. Follows `provideOverlayHistory` when unset. */
|
|
59
|
+
history?: OverlayHistoryMode
|
|
57
60
|
labels?: Partial<ConfirmLabels>
|
|
58
61
|
}>(),
|
|
59
|
-
{ glass: undefined },
|
|
62
|
+
{ glass: undefined, history: undefined },
|
|
60
63
|
)
|
|
61
64
|
|
|
62
65
|
const emit = defineEmits<{
|
|
@@ -128,6 +131,7 @@ function dismiss(open: boolean) {
|
|
|
128
131
|
<template>
|
|
129
132
|
<LpModal
|
|
130
133
|
:glass="glass"
|
|
134
|
+
:history="history"
|
|
131
135
|
:open="open"
|
|
132
136
|
:title="title"
|
|
133
137
|
:description="description"
|
|
@@ -37,6 +37,7 @@ import { CLOSE_ICON } from "./dropdown"
|
|
|
37
37
|
import { prefersReducedMotion } from "../composables/easing"
|
|
38
38
|
import { useCloseGuard, type CloseGuard } from "../composables/useCloseGuard"
|
|
39
39
|
import { useGlass } from "../composables/useGlass"
|
|
40
|
+
import { useOverlayHistory, type OverlayHistoryMode } from "../composables/useOverlayHistory"
|
|
40
41
|
import { useLabels } from "../composables/useLpConfig"
|
|
41
42
|
import { usePersisted } from "../composables/usePersisted"
|
|
42
43
|
import LpIcon from "./LpIcon.vue"
|
|
@@ -132,9 +133,17 @@ const props = withDefaults(
|
|
|
132
133
|
glass?: boolean
|
|
133
134
|
/** Accessible name for a drawer without a visible `title`. */
|
|
134
135
|
ariaLabel?: string
|
|
136
|
+
/**
|
|
137
|
+
* Browser Back closes it, and opening it adds a history entry. Unset
|
|
138
|
+
* follows `provideOverlayHistory` (off without one); `"reopen"` also
|
|
139
|
+
* opens it again on Forward; `false` keeps it out of history, for an
|
|
140
|
+
* overlay the app already drives from its route.
|
|
141
|
+
*/
|
|
142
|
+
history?: OverlayHistoryMode
|
|
135
143
|
labels?: Partial<{ close: string }>
|
|
136
144
|
}>(),
|
|
137
145
|
{
|
|
146
|
+
history: undefined,
|
|
138
147
|
side: "right",
|
|
139
148
|
size: "sm",
|
|
140
149
|
dismissible: true,
|
|
@@ -214,6 +223,18 @@ async function requestClose() {
|
|
|
214
223
|
// ways out do. Handed to the body and footer slots, and exposed.
|
|
215
224
|
defineExpose({ close: requestClose })
|
|
216
225
|
|
|
226
|
+
useOverlayHistory({
|
|
227
|
+
mode: () => props.history,
|
|
228
|
+
open: () => !!props.open,
|
|
229
|
+
close: async () => {
|
|
230
|
+
if (!(await allowClose())) return false
|
|
231
|
+
emit("update:open", false)
|
|
232
|
+
return true
|
|
233
|
+
},
|
|
234
|
+
reopen: () => emit("update:open", true),
|
|
235
|
+
navigated: () => emit("update:open", false),
|
|
236
|
+
})
|
|
237
|
+
|
|
217
238
|
// The header's close button is vaul's own and would slide the panel away at
|
|
218
239
|
// once; with a guard it asks first and closes through the same path as above.
|
|
219
240
|
function guardClose(event: MouseEvent) {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue"
|
|
12
12
|
import { useCloseGuard, type CloseGuard } from "../composables/useCloseGuard"
|
|
13
13
|
import { useGlass } from "../composables/useGlass"
|
|
14
|
+
import { useOverlayHistory, type OverlayHistoryMode } from "../composables/useOverlayHistory"
|
|
14
15
|
import { useLabels } from "../composables/useLpConfig"
|
|
15
16
|
import { useShift } from "../composables/useShift"
|
|
16
17
|
import { useModalLayer } from "./modalDepth"
|
|
@@ -93,9 +94,16 @@ const props = withDefaults(
|
|
|
93
94
|
* form's state, `:guard="form.dirty"`. See `useCloseGuard`.
|
|
94
95
|
*/
|
|
95
96
|
guard?: CloseGuard
|
|
97
|
+
/**
|
|
98
|
+
* Browser Back closes it, and opening it adds a history entry. Unset
|
|
99
|
+
* follows `provideOverlayHistory` (off without one); `"reopen"` also
|
|
100
|
+
* opens it again on Forward; `false` keeps it out of history, for an
|
|
101
|
+
* overlay the app already drives from its route.
|
|
102
|
+
*/
|
|
103
|
+
history?: OverlayHistoryMode
|
|
96
104
|
labels?: Partial<{ close: string }>
|
|
97
105
|
}>(),
|
|
98
|
-
{ size: "md", autoFocus: "auto", glass: undefined, dismissible: true, guard: undefined },
|
|
106
|
+
{ size: "md", autoFocus: "auto", glass: undefined, dismissible: true, guard: undefined, history: undefined },
|
|
99
107
|
)
|
|
100
108
|
|
|
101
109
|
const text = useLabels("modal", { close: "Close" }, () => props.labels)
|
|
@@ -120,6 +128,18 @@ async function onOpenChange(next: boolean) {
|
|
|
120
128
|
function close() {
|
|
121
129
|
void onOpenChange(false)
|
|
122
130
|
}
|
|
131
|
+
|
|
132
|
+
useOverlayHistory({
|
|
133
|
+
mode: () => props.history,
|
|
134
|
+
open: () => !!props.open,
|
|
135
|
+
close: async () => {
|
|
136
|
+
if (!(await allowClose())) return false
|
|
137
|
+
emit("update:open", false)
|
|
138
|
+
return true
|
|
139
|
+
},
|
|
140
|
+
reopen: () => emit("update:open", true),
|
|
141
|
+
navigated: () => emit("update:open", false),
|
|
142
|
+
})
|
|
123
143
|
defineExpose({ close })
|
|
124
144
|
|
|
125
145
|
function onEscape(event: KeyboardEvent) {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { computed } from "vue"
|
|
19
19
|
import LpKeys from "./LpKeys.vue"
|
|
20
20
|
import LpModal from "./LpModal.vue"
|
|
21
|
+
import type { OverlayHistoryMode } from "../composables/useOverlayHistory"
|
|
21
22
|
import {
|
|
22
23
|
activeShortcuts,
|
|
23
24
|
mergeShortcutGroups,
|
|
@@ -36,6 +37,8 @@ const props = withDefaults(
|
|
|
36
37
|
description?: string
|
|
37
38
|
/** Frosted glass panel, passed to LpModal. Follows `provideGlass` when unset. */
|
|
38
39
|
glass?: boolean
|
|
40
|
+
/** Browser history for the dialog, passed to LpModal. Follows `provideOverlayHistory` when unset. */
|
|
41
|
+
history?: OverlayHistoryMode
|
|
39
42
|
/**
|
|
40
43
|
* A key that toggles the reference, e.g. "F1". Bound only while the dialog
|
|
41
44
|
* is mounted, also inside inputs and over other overlays, and the browser's
|
|
@@ -43,7 +46,7 @@ const props = withDefaults(
|
|
|
43
46
|
*/
|
|
44
47
|
hotkey?: string
|
|
45
48
|
}>(),
|
|
46
|
-
{ open: false, groups: () => [], live: false, title: undefined, glass: undefined },
|
|
49
|
+
{ open: false, groups: () => [], live: false, title: undefined, glass: undefined, history: undefined },
|
|
47
50
|
)
|
|
48
51
|
|
|
49
52
|
const text = useLabels("shortcuts", {
|
|
@@ -75,6 +78,7 @@ useHotkeys(() =>
|
|
|
75
78
|
<template>
|
|
76
79
|
<LpModal
|
|
77
80
|
:glass="props.glass"
|
|
81
|
+
:history="props.history"
|
|
78
82
|
:open="props.open"
|
|
79
83
|
size="lg"
|
|
80
84
|
:title="props.title ?? text.title"
|
|
@@ -32,7 +32,16 @@ import { useResizeObserver } from "../composables/useResizeObserver"
|
|
|
32
32
|
import { compareValues } from "./tableSort"
|
|
33
33
|
import LpButton from "./LpButton.vue"
|
|
34
34
|
import { downloadFile, fileStem, toCsv, type CsvOptions } from "./exportData"
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
fromHiddenSets,
|
|
37
|
+
mergeRestored,
|
|
38
|
+
parseTableState,
|
|
39
|
+
sameHidden,
|
|
40
|
+
serializeTableState,
|
|
41
|
+
summarizeShown,
|
|
42
|
+
toHiddenSets,
|
|
43
|
+
type TableFilters,
|
|
44
|
+
} from "./tableFilter"
|
|
36
45
|
import { resolveRowMenu, type TableRowMenuResult } from "./tableRowMenu"
|
|
37
46
|
|
|
38
47
|
export interface TableColumn<Row> {
|
|
@@ -64,6 +73,21 @@ export interface TableColumn<Row> {
|
|
|
64
73
|
* vocabulary; the menu should say what the column itself says.
|
|
65
74
|
*/
|
|
66
75
|
valueLabel?: (value: unknown) => string
|
|
76
|
+
/**
|
|
77
|
+
* The column's whole vocabulary, when it is known apart from the rows. The
|
|
78
|
+
* funnel lists these first and then anything else the rows hold, so a value
|
|
79
|
+
* is offered even while nothing on the page has it: the rows may already be
|
|
80
|
+
* narrowed by the same column upstream (a control above the table, a server
|
|
81
|
+
* query), and a funnel built only from what arrived could not widen it again.
|
|
82
|
+
*/
|
|
83
|
+
filterValues?: readonly unknown[]
|
|
84
|
+
/**
|
|
85
|
+
* Show how many rows hold each value in the funnel (the default). Turn off
|
|
86
|
+
* when the rows are already narrowed by this column before they reach the
|
|
87
|
+
* table: the counts would then claim that the values filtered out upstream
|
|
88
|
+
* hold no rows at all.
|
|
89
|
+
*/
|
|
90
|
+
filterCounts?: boolean
|
|
67
91
|
/** The value a CSV export writes for this column; the raw cell by default. */
|
|
68
92
|
exportValue?: (row: Row) => unknown
|
|
69
93
|
/** Keep this column out of the show/hide list — an actions column has no
|
|
@@ -209,6 +233,23 @@ const props = withDefaults(
|
|
|
209
233
|
/** Active sort (v-model:sort). When set, the parent owns ordering (server
|
|
210
234
|
* side); otherwise the table sorts its own rows client-side. */
|
|
211
235
|
sort?: SortState | null
|
|
236
|
+
/**
|
|
237
|
+
* The header filters (v-model:filters): per column key, the values hidden.
|
|
238
|
+
* When set, the parent owns them, so a control above the table that
|
|
239
|
+
* filters the same column (see `useTableFilterChoice`) and the column's
|
|
240
|
+
* funnel are one state: each shows what the other picked, and the table's
|
|
241
|
+
* resets clear both. Left unset, the table keeps them itself. With
|
|
242
|
+
* `persist`, remembered filters fill only the columns the parent leaves
|
|
243
|
+
* unfiltered, and arrive through `update:filters`.
|
|
244
|
+
*/
|
|
245
|
+
filters?: TableFilters
|
|
246
|
+
/**
|
|
247
|
+
* The rows are narrowed by filters outside the table (a search, a period).
|
|
248
|
+
* An empty table then says nothing matches rather than that there is
|
|
249
|
+
* nothing at all, and its reset button also emits `resetFilters`, for the
|
|
250
|
+
* parent to clear its own.
|
|
251
|
+
*/
|
|
252
|
+
filtered?: boolean
|
|
212
253
|
/** Pin the header while the body scrolls. Pair with a height on the wrapper. */
|
|
213
254
|
stickyHeader?: boolean
|
|
214
255
|
/** Per-row right-click menu. Return [] (or omit the prop) to disable it for
|
|
@@ -264,6 +305,8 @@ const props = withDefaults(
|
|
|
264
305
|
// undefined, not null: null is a parent saying "unsorted", undefined is no
|
|
265
306
|
// parent at all, and only the latter lets the table keep its own sort.
|
|
266
307
|
sort: undefined,
|
|
308
|
+
filters: undefined,
|
|
309
|
+
filtered: false,
|
|
267
310
|
},
|
|
268
311
|
)
|
|
269
312
|
|
|
@@ -337,6 +380,9 @@ const scrollAttrs = computed(() => {
|
|
|
337
380
|
const emit = defineEmits<{
|
|
338
381
|
(e: "update:selected", value: RowKey[]): void
|
|
339
382
|
(e: "update:sort", value: SortState | null): void
|
|
383
|
+
(e: "update:filters", value: TableFilters): void
|
|
384
|
+
/** The empty table's reset was pressed: clear the filters kept outside it. */
|
|
385
|
+
(e: "resetFilters"): void
|
|
340
386
|
(e: "update:allMatching", value: boolean): void
|
|
341
387
|
(e: "rowClick", row: T): void
|
|
342
388
|
}>()
|
|
@@ -474,7 +520,25 @@ async function holdHeight() {
|
|
|
474
520
|
* being invisible until someone notices the list has an unticked entry they
|
|
475
521
|
* never saw.
|
|
476
522
|
*/
|
|
477
|
-
const
|
|
523
|
+
const ownHidden = ref<Record<string, Set<unknown>>>({})
|
|
524
|
+
let lastHidden: Record<string, Set<unknown>> = {}
|
|
525
|
+
/*
|
|
526
|
+
* The parent's lists become sets once per real change: a parent handing back
|
|
527
|
+
* an equal state as a new object (its v-model echo, a computed that rebuilt)
|
|
528
|
+
* must not refetch a server page or replay the rows' motion.
|
|
529
|
+
*/
|
|
530
|
+
const hiddenValues = computed<Record<string, Set<unknown>>>(() => {
|
|
531
|
+
const next = props.filters !== undefined ? toHiddenSets(props.filters) : ownHidden.value
|
|
532
|
+
if (sameHidden(next, lastHidden)) return lastHidden
|
|
533
|
+
lastHidden = next
|
|
534
|
+
return next
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
function writeHidden(next: Record<string, Set<unknown>>) {
|
|
538
|
+
if (sameHidden(next, hiddenValues.value)) return
|
|
539
|
+
ownHidden.value = next
|
|
540
|
+
emit("update:filters", fromHiddenSets(next))
|
|
541
|
+
}
|
|
478
542
|
|
|
479
543
|
/*
|
|
480
544
|
* Rows animate only when the table itself reshuffles them — a header filter or
|
|
@@ -520,33 +584,28 @@ const localFacets = useTableFacets({
|
|
|
520
584
|
})
|
|
521
585
|
|
|
522
586
|
function valuesOf(col: TableColumn<T>): unknown[] {
|
|
523
|
-
|
|
524
|
-
|
|
587
|
+
const found = props.load
|
|
588
|
+
? facets.value[col.key] ?? [...new Set(sourceRows.value.map((row) => row[col.key]))]
|
|
589
|
+
: localFacets.distinct(col.key)
|
|
590
|
+
return col.filterValues ? [...new Set([...col.filterValues, ...found])] : found
|
|
525
591
|
}
|
|
526
592
|
|
|
527
593
|
function isValueHidden(key: string, value: unknown): boolean {
|
|
528
594
|
return hiddenValues.value[key]?.has(value) ?? false
|
|
529
595
|
}
|
|
530
596
|
|
|
531
|
-
function toggleValue(key: string, value: unknown) {
|
|
532
|
-
withMotion(() => applyToggle(key, value))
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function applyToggle(key: string, value: unknown) {
|
|
536
|
-
const next = new Set(hiddenValues.value[key] ?? [])
|
|
537
|
-
if (next.has(value)) next.delete(value)
|
|
538
|
-
else next.add(value)
|
|
539
|
-
hiddenValues.value = { ...hiddenValues.value, [key]: next }
|
|
540
|
-
}
|
|
541
|
-
|
|
542
597
|
function showAllValues(key: string) {
|
|
543
|
-
|
|
598
|
+
writeHidden({ ...hiddenValues.value, [key]: new Set() })
|
|
544
599
|
}
|
|
545
600
|
|
|
546
601
|
function setHidden(key: string, next: Set<unknown>) {
|
|
547
|
-
|
|
602
|
+
writeHidden({ ...hiddenValues.value, [key]: next })
|
|
548
603
|
}
|
|
549
604
|
|
|
605
|
+
// Whoever changed the filters, the funnel or a control of the parent's, the
|
|
606
|
+
// rows they hide and reveal move the same way.
|
|
607
|
+
watch(hiddenValues, () => withMotion(() => {}))
|
|
608
|
+
|
|
550
609
|
function labelOf(col: TableColumn<T>, value: unknown): string {
|
|
551
610
|
return col.valueLabel?.(value) ?? String(value ?? "—")
|
|
552
611
|
}
|
|
@@ -558,7 +617,7 @@ function labelOf(col: TableColumn<T>, value: unknown): string {
|
|
|
558
617
|
*/
|
|
559
618
|
const facetOptions = computed(() => new Map(props.columns.filter((col) => col.filterable).map((col) => {
|
|
560
619
|
return [col.key, computed(() => {
|
|
561
|
-
const counts = props.load ? undefined : localFacets.counts(col.key)
|
|
620
|
+
const counts = props.load || col.filterCounts === false ? undefined : localFacets.counts(col.key)
|
|
562
621
|
return valuesOf(col).map((value) => ({
|
|
563
622
|
value,
|
|
564
623
|
label: labelOf(col, value),
|
|
@@ -579,7 +638,12 @@ const activeFilters = computed(() =>
|
|
|
579
638
|
)
|
|
580
639
|
|
|
581
640
|
function clearAllFilters() {
|
|
582
|
-
|
|
641
|
+
writeHidden({})
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function resetFilters() {
|
|
645
|
+
clearAllFilters()
|
|
646
|
+
emit("resetFilters")
|
|
583
647
|
}
|
|
584
648
|
|
|
585
649
|
function filterLabels() {
|
|
@@ -816,9 +880,7 @@ onMounted(() => {
|
|
|
816
880
|
ownSort.value = state.sort
|
|
817
881
|
emit("update:sort", state.sort)
|
|
818
882
|
}
|
|
819
|
-
if (state.hidden)
|
|
820
|
-
hiddenValues.value = Object.fromEntries(Object.entries(state.hidden).map(([k, v]) => [k, new Set(v)]))
|
|
821
|
-
}
|
|
883
|
+
if (state.hidden) writeHidden(mergeRestored(hiddenValues.value, state.hidden))
|
|
822
884
|
if (state.columns) hiddenColumns.value = new Set(state.columns)
|
|
823
885
|
}
|
|
824
886
|
}
|
|
@@ -991,6 +1053,7 @@ const showBar = computed(
|
|
|
991
1053
|
)
|
|
992
1054
|
const filteredOut = computed(() => {
|
|
993
1055
|
if (busy.value || failed.value || displayRows.value.length) return false
|
|
1056
|
+
if (props.filtered) return true
|
|
994
1057
|
if (props.load) return Object.values(hiddenValues.value).some((set) => set.size)
|
|
995
1058
|
return props.rows.length > 0
|
|
996
1059
|
})
|
|
@@ -1364,7 +1427,7 @@ usePaletteCommands(() => {
|
|
|
1364
1427
|
:icon="failed ? 'lucide:circle-alert' : filteredOut ? 'lucide:filter-x' : emptyIcon"
|
|
1365
1428
|
:title="failed ? text.error : filteredOut ? text.noMatches : text.empty"
|
|
1366
1429
|
>
|
|
1367
|
-
<LpButton v-if="filteredOut" variant="outline" size="sm" @click="
|
|
1430
|
+
<LpButton v-if="filteredOut" variant="outline" size="sm" @click="resetFilters">
|
|
1368
1431
|
{{ text.resetFilters }}
|
|
1369
1432
|
</LpButton>
|
|
1370
1433
|
</LpEmptyState>
|