@leavepulse/ui 0.28.5 → 0.29.1
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/assets/LpTable-DMOyAUOh.css +44 -0
- package/dist/chunks/{LpInfraNode-qIL3l7IO.js → LpInfraNode-Ds_8SZgN.js} +22 -8
- package/dist/chunks/{LpThemeSwitcher-7mAK6bI7.js → LpThemeSwitcher-Dj4VcTu7.js} +10 -3
- package/dist/chunks/{LpTopologyCanvas-ebP67CLx.js → LpTopologyCanvas-Cm1L39uL.js} +6 -5
- package/dist/components/LpInfraNode.vue.d.ts +30 -0
- package/dist/components/LpInfraNode.vue.js +1 -1
- package/dist/components/LpTable.vue.js +278 -2
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.d.ts +11 -1
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/composables/useFormDraft.d.ts +67 -0
- package/dist/composables/useFormDraft.js +124 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -22
- package/package.json +1 -1
- package/src/components/LpInfraNode.vue +75 -9
- package/src/components/LpTable.vue +96 -13
- package/src/components/LpThemeSwitcher.vue +25 -3
- package/src/components/LpTopologyCanvas.vue +4 -0
- package/src/composables/useFormDraft.ts +259 -0
- package/src/index.ts +3 -0
- package/dist/chunks/LpTable-DTj52qlg.js +0 -266
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
/*
|
|
3
|
+
* Rows arriving, leaving and moving as the filter changes.
|
|
4
|
+
*
|
|
5
|
+
* A row cannot be translated on its Y axis without the table reflowing around
|
|
6
|
+
* it, so the entrance is opacity plus a small horizontal offset — enough to
|
|
7
|
+
* read as movement, not enough to fight the column grid. `--ease-emphasized`
|
|
8
|
+
* because this is interface responding to a click, not data being drawn.
|
|
9
|
+
*
|
|
10
|
+
* The leaving row is taken out of the flow, or the rows below it wait for the
|
|
11
|
+
* fade before closing the gap and the whole list lurches at the end instead of
|
|
12
|
+
* gliding.
|
|
13
|
+
*/
|
|
14
|
+
.lp-row-enter-active[data-v-7884aed8],
|
|
15
|
+
.lp-row-leave-active[data-v-7884aed8] {
|
|
16
|
+
transition:
|
|
17
|
+
opacity var(--duration-fast) var(--ease-emphasized),
|
|
18
|
+
transform var(--duration-fast) var(--ease-emphasized);
|
|
19
|
+
}
|
|
20
|
+
.lp-row-move[data-v-7884aed8] {
|
|
21
|
+
transition: transform var(--duration-medium) var(--ease-emphasized);
|
|
22
|
+
}
|
|
23
|
+
.lp-row-enter-from[data-v-7884aed8],
|
|
24
|
+
.lp-row-leave-to[data-v-7884aed8] {
|
|
25
|
+
opacity: 0;
|
|
26
|
+
transform: translateX(-0.5rem);
|
|
27
|
+
}
|
|
28
|
+
.lp-row-leave-active[data-v-7884aed8] {
|
|
29
|
+
position: absolute;
|
|
30
|
+
width: 100%;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* A filter that redraws the list is information, not decoration: with motion
|
|
34
|
+
turned down the rows change without animating rather than not changing. */
|
|
35
|
+
@media (prefers-reduced-motion: reduce) {
|
|
36
|
+
.lp-row-enter-active[data-v-7884aed8],
|
|
37
|
+
.lp-row-leave-active[data-v-7884aed8],
|
|
38
|
+
.lp-row-move[data-v-7884aed8] {
|
|
39
|
+
transition: none;
|
|
40
|
+
}
|
|
41
|
+
.lp-row-leave-active[data-v-7884aed8] {
|
|
42
|
+
position: static;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -26,6 +26,16 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
26
26
|
},
|
|
27
27
|
setup(__props) {
|
|
28
28
|
const props = __props;
|
|
29
|
+
function fill(tpl, vars) {
|
|
30
|
+
return tpl.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? ""));
|
|
31
|
+
}
|
|
32
|
+
const text = computed(() => ({
|
|
33
|
+
firewallRules: props.data.labels?.firewallRules ?? "{n} firewall rule(s)",
|
|
34
|
+
services: props.data.labels?.services ?? "{n} service(s)",
|
|
35
|
+
servicesStale: props.data.labels?.servicesStale ?? "{n} service(s) as of {age} ago - not current",
|
|
36
|
+
noContact: props.data.labels?.noContact ?? "No contact for {age} - everything shown here is that old",
|
|
37
|
+
old: props.data.labels?.old ?? "old"
|
|
38
|
+
}));
|
|
29
39
|
const DEFAULT_ROLES = {
|
|
30
40
|
compute: { color: "var(--color-node-compute)", icon: "▣" },
|
|
31
41
|
host: { color: "var(--color-node-compute)", icon: "▣" },
|
|
@@ -50,11 +60,13 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
50
60
|
);
|
|
51
61
|
return (_ctx, _cache) => {
|
|
52
62
|
return openBlock(), createElementBlock("div", {
|
|
53
|
-
class: normalizeClass(["w-[168px] overflow-hidden rounded-card border bg-surface-raised shadow-panel transition-[border-color,box-shadow]",
|
|
63
|
+
class: normalizeClass(["w-[168px] overflow-hidden rounded-card border bg-surface-raised shadow-panel transition-[border-color,box-shadow]", [
|
|
64
|
+
__props.selected ? "border-[var(--accent)]" : __props.data.online === false ? "border-dashed border-danger/50" : "border-[var(--accent-line)]"
|
|
65
|
+
]]),
|
|
54
66
|
style: normalizeStyle({
|
|
55
67
|
"--accent": accent.value,
|
|
56
68
|
"--accent-line": accentLine.value,
|
|
57
|
-
opacity: __props.data.dimmed ? 0.3 : __props.data.online === false ? 0.
|
|
69
|
+
opacity: __props.data.dimmed ? 0.3 : __props.data.online === false ? 0.75 : 1,
|
|
58
70
|
...__props.selected ? { boxShadow: "0 0 0 2px color-mix(in srgb, var(--accent) 40%, transparent), var(--shadow-panel)" } : {}
|
|
59
71
|
})
|
|
60
72
|
}, [
|
|
@@ -68,7 +80,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
68
80
|
position: unref(Position).Right,
|
|
69
81
|
class: "!size-2 !border-2 !border-[var(--accent)] !bg-surface"
|
|
70
82
|
}, null, 8, ["position"]),
|
|
71
|
-
|
|
83
|
+
createElementVNode("div", {
|
|
84
|
+
class: normalizeClass(["h-[3px]", __props.data.online === false ? "bg-danger/50" : "bg-[var(--accent)]"])
|
|
85
|
+
}, null, 2),
|
|
72
86
|
createElementVNode("div", _hoisted_1, [
|
|
73
87
|
createElementVNode("div", _hoisted_2, [
|
|
74
88
|
createElementVNode("span", _hoisted_3, toDisplayString(icon.value), 1),
|
|
@@ -79,14 +93,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
79
93
|
}, null, 2)) : createCommentVNode("", true)
|
|
80
94
|
]),
|
|
81
95
|
createElementVNode("div", _hoisted_5, [
|
|
82
|
-
createElementVNode("span", _hoisted_6, toDisplayString(__props.data.role), 1),
|
|
96
|
+
createElementVNode("span", _hoisted_6, toDisplayString(__props.data.roleLabel ?? __props.data.role), 1),
|
|
83
97
|
__props.data.overlay ? (openBlock(), createElementBlock("span", _hoisted_7, toDisplayString(__props.data.overlay), 1)) : createCommentVNode("", true)
|
|
84
98
|
]),
|
|
85
99
|
__props.data.firewallCount || __props.data.serviceCount !== void 0 || __props.data.staleFor ? (openBlock(), createElementBlock("div", _hoisted_8, [
|
|
86
100
|
__props.data.firewallCount ? (openBlock(), createElementBlock("span", {
|
|
87
101
|
key: 0,
|
|
88
102
|
class: "inline-flex items-center gap-1 rounded-pill bg-danger-soft px-1.5 py-0.5 text-[9px] font-medium text-danger",
|
|
89
|
-
title:
|
|
103
|
+
title: fill(text.value.firewallRules, { n: __props.data.firewallCount })
|
|
90
104
|
}, [
|
|
91
105
|
_cache[0] || (_cache[0] = createElementVNode("span", null, "⛨", -1)),
|
|
92
106
|
createTextVNode(toDisplayString(__props.data.firewallCount), 1)
|
|
@@ -94,7 +108,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
94
108
|
__props.data.serviceCount !== void 0 ? (openBlock(), createElementBlock("span", {
|
|
95
109
|
key: 1,
|
|
96
110
|
class: normalizeClass(["inline-flex items-center gap-1 rounded-pill bg-surface-soft px-1.5 py-0.5 text-[9px] font-medium text-muted", __props.data.staleFor ? "line-through decoration-danger/70" : ""]),
|
|
97
|
-
title: __props.data.staleFor ?
|
|
111
|
+
title: __props.data.staleFor ? fill(text.value.servicesStale, { n: __props.data.serviceCount, age: __props.data.staleFor }) : fill(text.value.services, { n: __props.data.serviceCount })
|
|
98
112
|
}, [
|
|
99
113
|
_cache[1] || (_cache[1] = createElementVNode("span", null, "◇", -1)),
|
|
100
114
|
createTextVNode(toDisplayString(__props.data.serviceCount), 1)
|
|
@@ -102,10 +116,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
102
116
|
__props.data.staleFor ? (openBlock(), createElementBlock("span", {
|
|
103
117
|
key: 2,
|
|
104
118
|
class: "inline-flex items-center gap-1 rounded-pill bg-danger-soft px-1.5 py-0.5 text-[9px] font-medium text-danger",
|
|
105
|
-
title:
|
|
119
|
+
title: fill(text.value.noContact, { age: __props.data.staleFor })
|
|
106
120
|
}, [
|
|
107
121
|
_cache[2] || (_cache[2] = createElementVNode("span", null, "⚠", -1)),
|
|
108
|
-
createTextVNode(toDisplayString(__props.data.staleFor) + " old
|
|
122
|
+
createTextVNode(toDisplayString(__props.data.staleFor) + " " + toDisplayString(text.value.old), 1)
|
|
109
123
|
], 8, _hoisted_11)) : createCommentVNode("", true)
|
|
110
124
|
])) : createCommentVNode("", true)
|
|
111
125
|
])
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, readonly, shallowRef, defineComponent, onMounted, openBlock, createBlock, withCtx, createElementVNode, normalizeClass, createElementBlock, normalizeStyle, createVNode, toDisplayString, createCommentVNode } from "vue";
|
|
1
|
+
import { computed, readonly, shallowRef, defineComponent, onMounted, ref, openBlock, createBlock, withCtx, createElementVNode, normalizeClass, createElementBlock, normalizeStyle, createVNode, toDisplayString, createCommentVNode } from "vue";
|
|
2
2
|
import { prefersReducedMotion, easingToken } from "../composables/easing.js";
|
|
3
3
|
import { _ as _sfc_main$1 } from "./LpContextMenu-D0mzXqA7.js";
|
|
4
4
|
import { _ as _sfc_main$2 } from "./LpIcon-CCnX5_2j.js";
|
|
@@ -592,8 +592,15 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
592
592
|
onSelect: () => applyTheme2(t)
|
|
593
593
|
}))
|
|
594
594
|
);
|
|
595
|
+
const hydrated = ref(false);
|
|
596
|
+
onMounted(() => {
|
|
597
|
+
hydrated.value = true;
|
|
598
|
+
});
|
|
599
|
+
const labelName = computed(
|
|
600
|
+
() => hydrated.value ? activeName.value : list.value[0]?.name ?? ""
|
|
601
|
+
);
|
|
595
602
|
const triggerLabel = computed(
|
|
596
|
-
() => props["aria-label"] ?? `Theme: ${
|
|
603
|
+
() => props["aria-label"] ?? `Theme: ${labelName.value}. Click to switch, right-click to choose.`
|
|
597
604
|
);
|
|
598
605
|
return (_ctx, _cache) => {
|
|
599
606
|
return openBlock(), createBlock(_sfc_main$1, { items: menu.value }, {
|
|
@@ -629,7 +636,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
629
636
|
size: ICON_SIZE[__props.size]
|
|
630
637
|
}, null, 8, ["name", "size"])
|
|
631
638
|
], 2)),
|
|
632
|
-
wantsLabel.value ? (openBlock(), createElementBlock("span", _hoisted_2, toDisplayString(
|
|
639
|
+
wantsLabel.value ? (openBlock(), createElementBlock("span", _hoisted_2, toDisplayString(labelName.value), 1)) : createCommentVNode("", true)
|
|
633
640
|
], 10, _hoisted_1)
|
|
634
641
|
]),
|
|
635
642
|
_: 1
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { defineComponent, markRaw, computed, watch, openBlock, createBlock, unref, withCtx, createVNode, createCommentVNode, normalizeClass, createElementVNode, createElementBlock, Fragment, renderList, createTextVNode, toDisplayString } from "vue";
|
|
1
|
+
import { defineComponent, markRaw, computed, watch, openBlock, createBlock, unref, withCtx, createVNode, createCommentVNode, normalizeClass, createElementVNode, createElementBlock, Fragment, renderList, createTextVNode, toDisplayString, renderSlot } from "vue";
|
|
2
2
|
import { Background } from "@vue-flow/background";
|
|
3
3
|
import { Controls } from "@vue-flow/controls";
|
|
4
4
|
import { MarkerType, useVueFlow, VueFlow, SelectionMode, Panel } from "@vue-flow/core";
|
|
5
5
|
import { MiniMap } from "@vue-flow/minimap";
|
|
6
|
-
import { _ as _sfc_main$3 } from "./LpInfraNode-
|
|
6
|
+
import { _ as _sfc_main$3 } from "./LpInfraNode-Ds_8SZgN.js";
|
|
7
7
|
import { _ as _sfc_main$1 } from "./LpLaneNode-HtnQf2FF.js";
|
|
8
8
|
import { _ as _sfc_main$2 } from "./LpServiceNode-D8uJ8sGk.js";
|
|
9
9
|
import { _ as _sfc_main$4 } from "./LpWaypointEdge-D8yHlQiw.js";
|
|
@@ -245,13 +245,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
245
245
|
])),
|
|
246
246
|
createTextVNode(" " + toDisplayString(l.label), 1)
|
|
247
247
|
]);
|
|
248
|
-
}), 128))
|
|
248
|
+
}), 128)),
|
|
249
|
+
renderSlot(_ctx.$slots, "legend-extra")
|
|
249
250
|
])
|
|
250
251
|
]),
|
|
251
|
-
_:
|
|
252
|
+
_: 3
|
|
252
253
|
}, 8, ["position", "class"])) : createCommentVNode("", true)
|
|
253
254
|
]),
|
|
254
|
-
_:
|
|
255
|
+
_: 3
|
|
255
256
|
}, 8, ["nodes", "edges", "nodes-connectable", "selection-on-drag", "pan-on-drag", "selection-key-code", "selection-mode"]);
|
|
256
257
|
};
|
|
257
258
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export interface InfraNodeData {
|
|
2
2
|
name: string;
|
|
3
|
+
/** Role key: selects the colour/glyph, and shown as-is unless `roleLabel`. */
|
|
3
4
|
role: string;
|
|
5
|
+
/**
|
|
6
|
+
* What to print for the role. The key itself ("host", "proxy") is an API
|
|
7
|
+
* value, not a word for an operator to read in their own language.
|
|
8
|
+
*/
|
|
9
|
+
roleLabel?: string;
|
|
4
10
|
overlay?: string;
|
|
5
11
|
online?: boolean;
|
|
6
12
|
kind?: string;
|
|
@@ -18,6 +24,30 @@ export interface InfraNodeData {
|
|
|
18
24
|
staleFor?: string;
|
|
19
25
|
/** Fade the node back (search miss / not part of the focused node's graph). */
|
|
20
26
|
dimmed?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Wording for this node's badges. Carried in `data` because that is what
|
|
29
|
+
* LpTopologyCanvas forwards to the node renderer; a component prop would
|
|
30
|
+
* never reach it.
|
|
31
|
+
*/
|
|
32
|
+
labels?: InfraNodeLabels;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Wording for the badges, so the node speaks the app's language.
|
|
36
|
+
*
|
|
37
|
+
* The kit ships English defaults; a localised app passes its own. `{n}` is the
|
|
38
|
+
* count, `{age}` the value of `staleFor`.
|
|
39
|
+
*/
|
|
40
|
+
export interface InfraNodeLabels {
|
|
41
|
+
/** Tooltip on the firewall badge. Default: "{n} firewall rule(s)". */
|
|
42
|
+
firewallRules?: string;
|
|
43
|
+
/** Tooltip on the service badge. Default: "{n} service(s)". */
|
|
44
|
+
services?: string;
|
|
45
|
+
/** Tooltip on the service badge when readings are stale. */
|
|
46
|
+
servicesStale?: string;
|
|
47
|
+
/** Tooltip on the staleness badge. */
|
|
48
|
+
noContact?: string;
|
|
49
|
+
/** Suffix after the age, e.g. "old" in "4m old". */
|
|
50
|
+
old?: string;
|
|
21
51
|
}
|
|
22
52
|
type __VLS_Props = {
|
|
23
53
|
data: InfraNodeData;
|
|
@@ -1,4 +1,280 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineComponent, ref, computed, watch, onBeforeUnmount, openBlock, createBlock, withCtx, createElementVNode, normalizeClass, createElementBlock, createVNode, createCommentVNode, Fragment, renderList, normalizeStyle, createTextVNode, toDisplayString, TransitionGroup, unref, mergeProps, withModifiers, renderSlot } from "vue";
|
|
2
|
+
import { _ as _sfc_main$2 } from "../chunks/LpCheckbox-BO0zuuPh.js";
|
|
3
|
+
import { _ as _sfc_main$4 } from "../chunks/LpContextMenu-D0mzXqA7.js";
|
|
4
|
+
import { _ as _sfc_main$3 } from "../chunks/LpIcon-CCnX5_2j.js";
|
|
5
|
+
import { _ as _sfc_main$1 } from "../chunks/LpScrollArea-D8xyKXcF.js";
|
|
6
|
+
import { useRovingFocus } from "../composables/useRovingFocus.js";
|
|
7
|
+
import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
|
|
8
|
+
import '../assets/LpTable-DMOyAUOh.css';const _hoisted_1 = { class: "w-full border-collapse text-sm" };
|
|
9
|
+
const _hoisted_2 = { class: "border-b border-line bg-surface-soft" };
|
|
10
|
+
const _hoisted_3 = {
|
|
11
|
+
key: 0,
|
|
12
|
+
class: "w-px px-4 py-3"
|
|
13
|
+
};
|
|
14
|
+
const _hoisted_4 = ["aria-sort"];
|
|
15
|
+
const _hoisted_5 = ["onClick"];
|
|
16
|
+
const _hoisted_6 = { key: "lp-table-empty" };
|
|
17
|
+
const _hoisted_7 = ["colspan"];
|
|
18
|
+
const _hoisted_8 = ["onClick"];
|
|
19
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
20
|
+
__name: "LpTable",
|
|
21
|
+
props: {
|
|
22
|
+
columns: {},
|
|
23
|
+
rows: {},
|
|
24
|
+
rowKey: {},
|
|
25
|
+
emptyLabel: { default: "Nothing here yet" },
|
|
26
|
+
emptyIcon: { default: "lucide:inbox" },
|
|
27
|
+
selectable: { type: Boolean, default: false },
|
|
28
|
+
selected: { default: () => [] },
|
|
29
|
+
sort: { default: null },
|
|
30
|
+
stickyHeader: { type: Boolean },
|
|
31
|
+
rowMenu: {}
|
|
32
|
+
},
|
|
33
|
+
emits: ["update:selected", "update:sort", "rowClick"],
|
|
34
|
+
setup(__props, { emit: __emit }) {
|
|
35
|
+
const props = __props;
|
|
36
|
+
const emit = __emit;
|
|
37
|
+
function keyFor(row, index) {
|
|
38
|
+
if (typeof props.rowKey === "function") return props.rowKey(row);
|
|
39
|
+
if (props.rowKey) return row[props.rowKey];
|
|
40
|
+
return index;
|
|
41
|
+
}
|
|
42
|
+
function alignClass(align) {
|
|
43
|
+
if (align === "right") return "text-right";
|
|
44
|
+
if (align === "center") return "text-center";
|
|
45
|
+
return "text-left";
|
|
46
|
+
}
|
|
47
|
+
function menuFor(row) {
|
|
48
|
+
return props.rowMenu?.(row) ?? [];
|
|
49
|
+
}
|
|
50
|
+
const ownSort = ref(null);
|
|
51
|
+
const activeSort = computed(() => props.sort ?? ownSort.value);
|
|
52
|
+
const displayRows = computed(() => {
|
|
53
|
+
const s = activeSort.value;
|
|
54
|
+
if (!s) return props.rows;
|
|
55
|
+
const col = props.columns.find((c) => c.key === s.key);
|
|
56
|
+
if (!col?.sortable) return props.rows;
|
|
57
|
+
const factor = s.dir === "asc" ? 1 : -1;
|
|
58
|
+
return [...props.rows].sort((a, b) => compare(a[s.key], b[s.key]) * factor);
|
|
59
|
+
});
|
|
60
|
+
function compare(a, b) {
|
|
61
|
+
if (a == null && b == null) return 0;
|
|
62
|
+
if (a == null) return -1;
|
|
63
|
+
if (b == null) return 1;
|
|
64
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
65
|
+
return String(a).localeCompare(String(b), void 0, { numeric: true });
|
|
66
|
+
}
|
|
67
|
+
function toggleSort(col) {
|
|
68
|
+
if (!col.sortable) return;
|
|
69
|
+
const s = activeSort.value;
|
|
70
|
+
let next;
|
|
71
|
+
if (!s || s.key !== col.key) next = { key: col.key, dir: "asc" };
|
|
72
|
+
else if (s.dir === "asc") next = { key: col.key, dir: "desc" };
|
|
73
|
+
else next = null;
|
|
74
|
+
ownSort.value = next;
|
|
75
|
+
emit("update:sort", next);
|
|
76
|
+
}
|
|
77
|
+
function sortIcon(col) {
|
|
78
|
+
const s = activeSort.value;
|
|
79
|
+
if (s?.key !== col.key) return "lucide:chevrons-up-down";
|
|
80
|
+
return s.dir === "asc" ? "lucide:arrow-up" : "lucide:arrow-down";
|
|
81
|
+
}
|
|
82
|
+
const selectedSet = computed(() => new Set(props.selected));
|
|
83
|
+
const allKeys = computed(() => props.rows.map((r, i) => keyFor(r, i)));
|
|
84
|
+
const allChecked = computed(
|
|
85
|
+
() => allKeys.value.length > 0 && allKeys.value.every((k) => selectedSet.value.has(k))
|
|
86
|
+
);
|
|
87
|
+
const someChecked = computed(
|
|
88
|
+
() => !allChecked.value && allKeys.value.some((k) => selectedSet.value.has(k))
|
|
89
|
+
);
|
|
90
|
+
function toggleAll(checked) {
|
|
91
|
+
emit("update:selected", checked ? [...allKeys.value] : []);
|
|
92
|
+
}
|
|
93
|
+
function toggleRow(key, checked) {
|
|
94
|
+
const next = new Set(selectedSet.value);
|
|
95
|
+
if (checked) next.add(key);
|
|
96
|
+
else next.delete(key);
|
|
97
|
+
emit("update:selected", [...next]);
|
|
98
|
+
}
|
|
99
|
+
const colSpan = computed(() => props.columns.length + (props.selectable ? 1 : 0));
|
|
100
|
+
const rovingItems = computed(
|
|
101
|
+
() => displayRows.value.map((row, index) => ({
|
|
102
|
+
id: String(keyFor(row, index)),
|
|
103
|
+
label: String(row[props.columns[0]?.key ?? ""] ?? "")
|
|
104
|
+
}))
|
|
105
|
+
);
|
|
106
|
+
const rowByKey = computed(() => {
|
|
107
|
+
const map = /* @__PURE__ */ new Map();
|
|
108
|
+
displayRows.value.forEach((row, index) => map.set(String(keyFor(row, index)), row));
|
|
109
|
+
return map;
|
|
110
|
+
});
|
|
111
|
+
const roving = useRovingFocus(rovingItems, {
|
|
112
|
+
onActivate: (id) => {
|
|
113
|
+
const row = rowByKey.value.get(id);
|
|
114
|
+
if (!row) return;
|
|
115
|
+
emit("rowClick", row);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
function onRowKeydown(event) {
|
|
119
|
+
if (event.key === " " && props.selectable && roving.activeId.value) {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
const key = roving.activeId.value;
|
|
122
|
+
const row = rowByKey.value.get(key);
|
|
123
|
+
if (row) {
|
|
124
|
+
const rowKey = keyFor(row, displayRows.value.indexOf(row));
|
|
125
|
+
toggleRow(rowKey, !selectedSet.value.has(rowKey));
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
roving.onKeydown(event);
|
|
130
|
+
}
|
|
131
|
+
const headEl = ref(null);
|
|
132
|
+
const headHeight = ref(0);
|
|
133
|
+
let ro;
|
|
134
|
+
watch(
|
|
135
|
+
[headEl, () => props.stickyHeader],
|
|
136
|
+
([el, sticky]) => {
|
|
137
|
+
ro?.disconnect();
|
|
138
|
+
if (!el || !sticky || typeof ResizeObserver === "undefined") {
|
|
139
|
+
headHeight.value = 0;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
ro = new ResizeObserver(() => {
|
|
143
|
+
headHeight.value = el.offsetHeight;
|
|
144
|
+
});
|
|
145
|
+
ro.observe(el);
|
|
146
|
+
headHeight.value = el.offsetHeight;
|
|
147
|
+
},
|
|
148
|
+
{ immediate: true, flush: "post" }
|
|
149
|
+
);
|
|
150
|
+
onBeforeUnmount(() => ro?.disconnect());
|
|
151
|
+
const barInsetTop = computed(
|
|
152
|
+
() => props.stickyHeader && headHeight.value ? `${headHeight.value}px` : void 0
|
|
153
|
+
);
|
|
154
|
+
return (_ctx, _cache) => {
|
|
155
|
+
return openBlock(), createBlock(_sfc_main$1, {
|
|
156
|
+
fade: false,
|
|
157
|
+
class: "rounded-card border border-line",
|
|
158
|
+
"bar-inset-top": barInsetTop.value
|
|
159
|
+
}, {
|
|
160
|
+
default: withCtx(() => [
|
|
161
|
+
createElementVNode("table", _hoisted_1, [
|
|
162
|
+
createElementVNode("thead", {
|
|
163
|
+
ref_key: "headEl",
|
|
164
|
+
ref: headEl,
|
|
165
|
+
class: normalizeClass(__props.stickyHeader ? "sticky top-0 z-10" : "")
|
|
166
|
+
}, [
|
|
167
|
+
createElementVNode("tr", _hoisted_2, [
|
|
168
|
+
__props.selectable ? (openBlock(), createElementBlock("th", _hoisted_3, [
|
|
169
|
+
createVNode(_sfc_main$2, {
|
|
170
|
+
"model-value": allChecked.value,
|
|
171
|
+
indeterminate: someChecked.value,
|
|
172
|
+
"onUpdate:modelValue": toggleAll
|
|
173
|
+
}, null, 8, ["model-value", "indeterminate"])
|
|
174
|
+
])) : createCommentVNode("", true),
|
|
175
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.columns, (col) => {
|
|
176
|
+
return openBlock(), createElementBlock("th", {
|
|
177
|
+
key: col.key,
|
|
178
|
+
style: normalizeStyle(col.width ? { width: col.width } : void 0),
|
|
179
|
+
class: normalizeClass(["px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted", alignClass(col.align)]),
|
|
180
|
+
"aria-sort": activeSort.value?.key === col.key ? activeSort.value.dir === "asc" ? "ascending" : "descending" : void 0
|
|
181
|
+
}, [
|
|
182
|
+
col.sortable ? (openBlock(), createElementBlock("button", {
|
|
183
|
+
key: 0,
|
|
184
|
+
type: "button",
|
|
185
|
+
class: normalizeClass(["group/sort -mx-1 inline-flex items-center gap-1 rounded px-1 outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring", [
|
|
186
|
+
col.align === "right" ? "flex-row-reverse" : "",
|
|
187
|
+
activeSort.value?.key === col.key ? "text-ink" : ""
|
|
188
|
+
]]),
|
|
189
|
+
onClick: ($event) => toggleSort(col)
|
|
190
|
+
}, [
|
|
191
|
+
createTextVNode(toDisplayString(col.label) + " ", 1),
|
|
192
|
+
createVNode(_sfc_main$3, {
|
|
193
|
+
name: sortIcon(col),
|
|
194
|
+
size: 13,
|
|
195
|
+
class: normalizeClass([
|
|
196
|
+
"shrink-0 transition-[transform,opacity] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)]",
|
|
197
|
+
activeSort.value?.key === col.key ? "opacity-100" : "opacity-40 group-hover/sort:opacity-70"
|
|
198
|
+
])
|
|
199
|
+
}, null, 8, ["name", "class"])
|
|
200
|
+
], 10, _hoisted_5)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
201
|
+
createTextVNode(toDisplayString(col.label), 1)
|
|
202
|
+
], 64))
|
|
203
|
+
], 14, _hoisted_4);
|
|
204
|
+
}), 128))
|
|
205
|
+
])
|
|
206
|
+
], 2),
|
|
207
|
+
createVNode(TransitionGroup, {
|
|
208
|
+
tag: "tbody",
|
|
209
|
+
name: "lp-row",
|
|
210
|
+
ref: (el) => unref(roving).setContainer(el?.$el ?? null)
|
|
211
|
+
}, {
|
|
212
|
+
default: withCtx(() => [
|
|
213
|
+
displayRows.value.length === 0 ? (openBlock(), createElementBlock("tr", _hoisted_6, [
|
|
214
|
+
createElementVNode("td", {
|
|
215
|
+
colspan: colSpan.value,
|
|
216
|
+
class: "px-4 py-10 text-center text-muted"
|
|
217
|
+
}, [
|
|
218
|
+
createVNode(_sfc_main$3, {
|
|
219
|
+
name: __props.emptyIcon,
|
|
220
|
+
size: 22,
|
|
221
|
+
class: "mx-auto mb-2 opacity-60"
|
|
222
|
+
}, null, 8, ["name"]),
|
|
223
|
+
createElementVNode("div", null, toDisplayString(__props.emptyLabel), 1)
|
|
224
|
+
], 8, _hoisted_7)
|
|
225
|
+
])) : createCommentVNode("", true),
|
|
226
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(displayRows.value, (row, index) => {
|
|
227
|
+
return openBlock(), createBlock(_sfc_main$4, {
|
|
228
|
+
key: keyFor(row, index),
|
|
229
|
+
items: menuFor(row)
|
|
230
|
+
}, {
|
|
231
|
+
default: withCtx(() => [
|
|
232
|
+
createElementVNode("tr", mergeProps({
|
|
233
|
+
class: ["group border-b border-line/60 outline-none transition-colors last:border-0 hover:bg-surface-soft/60 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring data-[active]:bg-surface-soft/40", selectedSet.value.has(keyFor(row, index)) ? "bg-brand-soft/40" : ""]
|
|
234
|
+
}, { ref_for: true }, unref(roving).itemProps(String(keyFor(row, index))), {
|
|
235
|
+
onClick: ($event) => emit("rowClick", row),
|
|
236
|
+
onKeydown: onRowKeydown
|
|
237
|
+
}), [
|
|
238
|
+
__props.selectable ? (openBlock(), createElementBlock("td", {
|
|
239
|
+
key: 0,
|
|
240
|
+
class: "w-px px-4 py-3",
|
|
241
|
+
onClick: _cache[0] || (_cache[0] = withModifiers(() => {
|
|
242
|
+
}, ["stop"]))
|
|
243
|
+
}, [
|
|
244
|
+
createVNode(_sfc_main$2, {
|
|
245
|
+
"model-value": selectedSet.value.has(keyFor(row, index)),
|
|
246
|
+
"onUpdate:modelValue": (v) => toggleRow(keyFor(row, index), v)
|
|
247
|
+
}, null, 8, ["model-value", "onUpdate:modelValue"])
|
|
248
|
+
])) : createCommentVNode("", true),
|
|
249
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.columns, (col) => {
|
|
250
|
+
return openBlock(), createElementBlock("td", {
|
|
251
|
+
key: col.key,
|
|
252
|
+
class: normalizeClass(["px-4 py-3 text-ink", alignClass(col.align)])
|
|
253
|
+
}, [
|
|
254
|
+
renderSlot(_ctx.$slots, `cell-${col.key}`, {
|
|
255
|
+
row,
|
|
256
|
+
value: row[col.key]
|
|
257
|
+
}, () => [
|
|
258
|
+
createTextVNode(toDisplayString(row[col.key]), 1)
|
|
259
|
+
], true)
|
|
260
|
+
], 2);
|
|
261
|
+
}), 128))
|
|
262
|
+
], 16, _hoisted_8)
|
|
263
|
+
]),
|
|
264
|
+
_: 2
|
|
265
|
+
}, 1032, ["items"]);
|
|
266
|
+
}), 128))
|
|
267
|
+
]),
|
|
268
|
+
_: 3
|
|
269
|
+
}, 512)
|
|
270
|
+
])
|
|
271
|
+
]),
|
|
272
|
+
_: 3
|
|
273
|
+
}, 8, ["bar-inset-top"]);
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-7884aed8"]]);
|
|
2
278
|
export {
|
|
3
|
-
|
|
279
|
+
LpTable as default
|
|
4
280
|
};
|
|
@@ -121,7 +121,11 @@ type __VLS_Props = {
|
|
|
121
121
|
controls?: boolean;
|
|
122
122
|
};
|
|
123
123
|
declare function applyViewport(v: CanvasViewport): Promise<void>;
|
|
124
|
-
declare
|
|
124
|
+
declare var __VLS_29: {};
|
|
125
|
+
type __VLS_Slots = {} & {
|
|
126
|
+
'legend-extra'?: (props: typeof __VLS_29) => any;
|
|
127
|
+
};
|
|
128
|
+
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {
|
|
125
129
|
fitView: import('@vue-flow/core').FitView;
|
|
126
130
|
setViewport: typeof applyViewport;
|
|
127
131
|
}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
|
|
@@ -191,5 +195,11 @@ declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {
|
|
|
191
195
|
lanes: TopologyLane[];
|
|
192
196
|
minimap: boolean;
|
|
193
197
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
198
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
194
199
|
declare const _default: typeof __VLS_export;
|
|
195
200
|
export default _default;
|
|
201
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
202
|
+
new (): {
|
|
203
|
+
$slots: S;
|
|
204
|
+
};
|
|
205
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { MaybeRefOrGetter, Ref } from 'vue';
|
|
2
|
+
export interface UseFormDraftOptions<T> {
|
|
3
|
+
/**
|
|
4
|
+
* Where this draft lives. Include whatever identifies the subject — a draft
|
|
5
|
+
* for device A must not surface while editing device B.
|
|
6
|
+
*/
|
|
7
|
+
key: MaybeRefOrGetter<string>;
|
|
8
|
+
/** The form's current value. Watched deeply; every change rewrites the draft. */
|
|
9
|
+
value: MaybeRefOrGetter<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Whether there is anything worth keeping. Return false while the form still
|
|
12
|
+
* matches what was loaded, so an untouched form leaves nothing behind and no
|
|
13
|
+
* later visit is offered its own starting state back as a "draft".
|
|
14
|
+
*/
|
|
15
|
+
dirty?: MaybeRefOrGetter<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* How long a draft stays offerable, in milliseconds. Older ones are dropped
|
|
18
|
+
* on read: a fortnight-old draft of a record that has been edited twice since
|
|
19
|
+
* is not a rescue, it is a way to overwrite current facts with stale ones.
|
|
20
|
+
* Defaults to a week. `0` keeps them indefinitely.
|
|
21
|
+
*/
|
|
22
|
+
maxAgeMs?: number;
|
|
23
|
+
/** Skip the write without tearing the composable out of the component. */
|
|
24
|
+
disabled?: MaybeRefOrGetter<boolean>;
|
|
25
|
+
}
|
|
26
|
+
export interface UseFormDraft<T> {
|
|
27
|
+
/** A draft was found for this key, and has not been restored or discarded. */
|
|
28
|
+
hasDraft: Ref<boolean>;
|
|
29
|
+
/** When that draft was written, ISO-8601, or `null` when there is none. */
|
|
30
|
+
savedAt: Ref<string | null>;
|
|
31
|
+
/**
|
|
32
|
+
* The stored value, or `null`. Reading does not apply it — the caller decides
|
|
33
|
+
* what to do with each field, because only it knows the form's shape.
|
|
34
|
+
*/
|
|
35
|
+
read: () => T | null;
|
|
36
|
+
/** Forget the draft. Call after applying it, and after a successful save. */
|
|
37
|
+
discard: () => void;
|
|
38
|
+
/** Write now, ignoring `dirty`. For "save and come back to this later". */
|
|
39
|
+
save: () => void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Keeps an unsaved form in browser storage, so losing the page does not lose
|
|
43
|
+
* what was typed into it.
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* const draft = useFormDraft({
|
|
47
|
+
* key: () => `lp.draft.device-power:${deviceId.value}`,
|
|
48
|
+
* value: () => interfaces.value,
|
|
49
|
+
* dirty: () => hasUnsavedChanges.value,
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
* ```vue
|
|
53
|
+
* <LpAlert v-if="draft.hasDraft.value" variant="info">
|
|
54
|
+
* {{ t("draft.found", { when: draft.savedAt.value }) }}
|
|
55
|
+
* <LpButton @click="applyDraft">Restore</LpButton>
|
|
56
|
+
* <LpButton variant="ghost" @click="draft.discard()">Discard</LpButton>
|
|
57
|
+
* </LpAlert>
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* `hasDraft` answers for the key as it is now, so a form that switches subject
|
|
61
|
+
* re-reads rather than carrying the previous subject's answer.
|
|
62
|
+
*
|
|
63
|
+
* Call `discard()` once the record is saved. A draft that outlives the save it
|
|
64
|
+
* describes is offered back on the next visit as though it were newer than the
|
|
65
|
+
* record, which is how a form helpfully undoes somebody's work.
|
|
66
|
+
*/
|
|
67
|
+
export declare function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T>;
|