@leavepulse/ui 0.29.0 → 0.29.2
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-ChxURw0m.css +45 -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/index.js +22 -22
- package/package.json +1 -1
- package/src/components/LpInfraNode.vue +75 -9
- package/src/components/LpTable.vue +95 -13
- package/src/components/LpThemeSwitcher.vue +25 -3
- package/src/components/LpTopologyCanvas.vue +4 -0
- package/dist/chunks/LpTable-DTj52qlg.js +0 -266
|
@@ -0,0 +1,45 @@
|
|
|
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-359c1c0d],
|
|
15
|
+
.lp-row-leave-active[data-v-359c1c0d] {
|
|
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-359c1c0d] {
|
|
21
|
+
transition: transform var(--duration-medium) var(--ease-emphasized);
|
|
22
|
+
}
|
|
23
|
+
.lp-row-enter-from[data-v-359c1c0d],
|
|
24
|
+
.lp-row-leave-to[data-v-359c1c0d] {
|
|
25
|
+
opacity: 0;
|
|
26
|
+
transform: translateX(-0.5rem);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* The leaving row stays in the flow. Taking it out with `position: absolute` is
|
|
31
|
+
* the usual trick for a list, but a `<tr>` positioned that way loses the table's
|
|
32
|
+
* column grid and collapses to its content width — the row visibly breaks apart
|
|
33
|
+
* on its way out. The rows below close the gap through `lp-row-move` instead,
|
|
34
|
+
* which is a moment slower and stays a table the whole time.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/* A filter that redraws the list is information, not decoration: with motion
|
|
38
|
+
turned down the rows change without animating rather than not changing. */
|
|
39
|
+
@media (prefers-reduced-motion: reduce) {
|
|
40
|
+
.lp-row-enter-active[data-v-359c1c0d],
|
|
41
|
+
.lp-row-leave-active[data-v-359c1c0d],
|
|
42
|
+
.lp-row-move[data-v-359c1c0d] {
|
|
43
|
+
transition: none;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -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-ChxURw0m.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-359c1c0d"]]);
|
|
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
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ import { ancestorIds, checkStateOf, checkedCount, computeStats, fileIcon, format
|
|
|
30
30
|
import { _ as _25 } from "./chunks/LpFormField-fV_2ZdCk.js";
|
|
31
31
|
import { _ as _26 } from "./chunks/LpIcon-CCnX5_2j.js";
|
|
32
32
|
import { default as default4 } from "./components/LpImageEditor.vue.js";
|
|
33
|
-
import { _ as _27 } from "./chunks/LpInfraNode-
|
|
33
|
+
import { _ as _27 } from "./chunks/LpInfraNode-Ds_8SZgN.js";
|
|
34
34
|
import { _ as _28 } from "./chunks/LpInput-CYcTL9gj.js";
|
|
35
35
|
import { _ as _29 } from "./chunks/LpLightbox-Ci35Y_xP.js";
|
|
36
36
|
import { fileNameOf } from "./components/lightbox.js";
|
|
@@ -65,17 +65,17 @@ import { _ as _55 } from "./chunks/LpSparkline-C6xQ2dwv.js";
|
|
|
65
65
|
import { _ as _56 } from "./chunks/LpStat-CDanpzU0.js";
|
|
66
66
|
import { _ as _57 } from "./chunks/LpStepper-Wtl-6nUh.js";
|
|
67
67
|
import { _ as _58 } from "./chunks/LpSwitch-v6fnccSM.js";
|
|
68
|
-
import {
|
|
69
|
-
import { _ as
|
|
70
|
-
import { _ as
|
|
71
|
-
import { _ as
|
|
72
|
-
import { p as presets, a as parseTheme, s as serializeTheme } from "./chunks/LpThemeSwitcher-
|
|
73
|
-
import { D, _ as
|
|
74
|
-
import { _ as
|
|
75
|
-
import { _ as
|
|
76
|
-
import { _ as
|
|
77
|
-
import { _ as
|
|
78
|
-
import { _ as
|
|
68
|
+
import { default as default6 } from "./components/LpTable.vue.js";
|
|
69
|
+
import { _ as _59 } from "./chunks/LpTableOfContents-CjvxPMpU.js";
|
|
70
|
+
import { _ as _60 } from "./chunks/LpTabs-Cefe_lys.js";
|
|
71
|
+
import { _ as _61 } from "./chunks/LpTextarea-BMIOfw5l.js";
|
|
72
|
+
import { p as presets, a as parseTheme, s as serializeTheme } from "./chunks/LpThemeSwitcher-Dj4VcTu7.js";
|
|
73
|
+
import { D, _ as _62, N, T, b, c, d, e, f, g, i, l, h, j, n, r, t, k, m, u, v } from "./chunks/LpThemeSwitcher-Dj4VcTu7.js";
|
|
74
|
+
import { _ as _63 } from "./chunks/LpTilt-DuyKbpkA.js";
|
|
75
|
+
import { _ as _64 } from "./chunks/LpToaster-gDB1EaU3.js";
|
|
76
|
+
import { _ as _65 } from "./chunks/LpTooltip-hCLPAqMG.js";
|
|
77
|
+
import { _ as _66 } from "./chunks/LpTopologyCanvas-Cm1L39uL.js";
|
|
78
|
+
import { _ as _67 } from "./chunks/LpUptimeBar-DcQ8HikG.js";
|
|
79
79
|
import { deserializeLayout, serializeLayout } from "./layout/tree.js";
|
|
80
80
|
import { addLeaf, addTab, countBlocks, countLeaves, makeLayout, moveLeaf, removeBlock, removeLeaf, reorderTab, resizeAt, setActiveTab } from "./layout/tree.js";
|
|
81
81
|
import { blockTitle, defineBlocks } from "./layout/registry.js";
|
|
@@ -186,16 +186,16 @@ export {
|
|
|
186
186
|
_56 as LpStat,
|
|
187
187
|
_57 as LpStepper,
|
|
188
188
|
_58 as LpSwitch,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
189
|
+
default6 as LpTable,
|
|
190
|
+
_59 as LpTableOfContents,
|
|
191
|
+
_60 as LpTabs,
|
|
192
|
+
_61 as LpTextarea,
|
|
193
|
+
_62 as LpThemeSwitcher,
|
|
194
|
+
_63 as LpTilt,
|
|
195
|
+
_64 as LpToaster,
|
|
196
|
+
_65 as LpTooltip,
|
|
197
|
+
_66 as LpTopologyCanvas,
|
|
198
|
+
_67 as LpUptimeBar,
|
|
199
199
|
N as NO_BACKDROP_ATTR,
|
|
200
200
|
OSM_TILES,
|
|
201
201
|
T as THEME_COOKIE,
|
package/package.json
CHANGED
|
@@ -13,7 +13,13 @@ import { computed } from "vue"
|
|
|
13
13
|
|
|
14
14
|
export interface InfraNodeData {
|
|
15
15
|
name: string
|
|
16
|
+
/** Role key: selects the colour/glyph, and shown as-is unless `roleLabel`. */
|
|
16
17
|
role: string
|
|
18
|
+
/**
|
|
19
|
+
* What to print for the role. The key itself ("host", "proxy") is an API
|
|
20
|
+
* value, not a word for an operator to read in their own language.
|
|
21
|
+
*/
|
|
22
|
+
roleLabel?: string
|
|
17
23
|
overlay?: string
|
|
18
24
|
online?: boolean
|
|
19
25
|
kind?: string
|
|
@@ -31,6 +37,31 @@ export interface InfraNodeData {
|
|
|
31
37
|
staleFor?: string
|
|
32
38
|
/** Fade the node back (search miss / not part of the focused node's graph). */
|
|
33
39
|
dimmed?: boolean
|
|
40
|
+
/**
|
|
41
|
+
* Wording for this node's badges. Carried in `data` because that is what
|
|
42
|
+
* LpTopologyCanvas forwards to the node renderer; a component prop would
|
|
43
|
+
* never reach it.
|
|
44
|
+
*/
|
|
45
|
+
labels?: InfraNodeLabels
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Wording for the badges, so the node speaks the app's language.
|
|
50
|
+
*
|
|
51
|
+
* The kit ships English defaults; a localised app passes its own. `{n}` is the
|
|
52
|
+
* count, `{age}` the value of `staleFor`.
|
|
53
|
+
*/
|
|
54
|
+
export interface InfraNodeLabels {
|
|
55
|
+
/** Tooltip on the firewall badge. Default: "{n} firewall rule(s)". */
|
|
56
|
+
firewallRules?: string
|
|
57
|
+
/** Tooltip on the service badge. Default: "{n} service(s)". */
|
|
58
|
+
services?: string
|
|
59
|
+
/** Tooltip on the service badge when readings are stale. */
|
|
60
|
+
servicesStale?: string
|
|
61
|
+
/** Tooltip on the staleness badge. */
|
|
62
|
+
noContact?: string
|
|
63
|
+
/** Suffix after the age, e.g. "old" in "4m old". */
|
|
64
|
+
old?: string
|
|
34
65
|
}
|
|
35
66
|
|
|
36
67
|
const props = defineProps<{
|
|
@@ -40,6 +71,21 @@ const props = defineProps<{
|
|
|
40
71
|
roles?: Record<string, InfraRoleStyle>
|
|
41
72
|
}>()
|
|
42
73
|
|
|
74
|
+
/** Fill `{n}` / `{age}` in a label template. */
|
|
75
|
+
function fill(tpl: string, vars: Record<string, string | number>): string {
|
|
76
|
+
return tpl.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? ""))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const text = computed(() => ({
|
|
80
|
+
firewallRules: props.data.labels?.firewallRules ?? "{n} firewall rule(s)",
|
|
81
|
+
services: props.data.labels?.services ?? "{n} service(s)",
|
|
82
|
+
servicesStale:
|
|
83
|
+
props.data.labels?.servicesStale ?? "{n} service(s) as of {age} ago - not current",
|
|
84
|
+
noContact:
|
|
85
|
+
props.data.labels?.noContact ?? "No contact for {age} - everything shown here is that old",
|
|
86
|
+
old: props.data.labels?.old ?? "old",
|
|
87
|
+
}))
|
|
88
|
+
|
|
43
89
|
/*
|
|
44
90
|
* Role → look. Colours are token references, never literals, so a theme (or a
|
|
45
91
|
* consumer) restyles the graph without forking this component; the glyph keeps
|
|
@@ -80,13 +126,28 @@ const accentLine = computed(
|
|
|
80
126
|
</script>
|
|
81
127
|
|
|
82
128
|
<template>
|
|
129
|
+
<!--
|
|
130
|
+
An offline host is drawn offline, not merely faded.
|
|
131
|
+
|
|
132
|
+
Opacity alone was the only signal the frame carried, and at 0.5 on a dark
|
|
133
|
+
canvas it is nearly invisible: a machine that had not reported for minutes
|
|
134
|
+
sat in a role-coloured frame that read as healthy, contradicted only by a
|
|
135
|
+
7px dot. Offline now takes the frame and the role bar as well, so the three
|
|
136
|
+
signals on a node agree instead of arguing.
|
|
137
|
+
-->
|
|
83
138
|
<div
|
|
84
139
|
class="w-[168px] overflow-hidden rounded-card border bg-surface-raised shadow-panel transition-[border-color,box-shadow]"
|
|
85
|
-
:class="
|
|
140
|
+
:class="[
|
|
141
|
+
selected
|
|
142
|
+
? 'border-[var(--accent)]'
|
|
143
|
+
: data.online === false
|
|
144
|
+
? 'border-dashed border-danger/50'
|
|
145
|
+
: 'border-[var(--accent-line)]',
|
|
146
|
+
]"
|
|
86
147
|
:style="{
|
|
87
148
|
'--accent': accent,
|
|
88
149
|
'--accent-line': accentLine,
|
|
89
|
-
opacity: data.dimmed ? 0.3 : data.online === false ? 0.
|
|
150
|
+
opacity: data.dimmed ? 0.3 : data.online === false ? 0.75 : 1,
|
|
90
151
|
...(selected
|
|
91
152
|
? { boxShadow: '0 0 0 2px color-mix(in srgb, var(--accent) 40%, transparent), var(--shadow-panel)' }
|
|
92
153
|
: {}),
|
|
@@ -103,7 +164,12 @@ const accentLine = computed(
|
|
|
103
164
|
class="!size-2 !border-2 !border-[var(--accent)] !bg-surface"
|
|
104
165
|
/>
|
|
105
166
|
|
|
106
|
-
|
|
167
|
+
<!-- The role bar reads as "this node is alive and is a proxy"; on an
|
|
168
|
+
offline host it must not keep asserting the first half. -->
|
|
169
|
+
<div
|
|
170
|
+
class="h-[3px]"
|
|
171
|
+
:class="data.online === false ? 'bg-danger/50' : 'bg-[var(--accent)]'"
|
|
172
|
+
/>
|
|
107
173
|
<div class="px-[11px] pb-2.5 pt-[9px]">
|
|
108
174
|
<div class="flex items-center gap-[7px]">
|
|
109
175
|
<span class="text-[13px] text-[var(--accent)]">{{ icon }}</span>
|
|
@@ -116,7 +182,7 @@ const accentLine = computed(
|
|
|
116
182
|
</div>
|
|
117
183
|
<div class="mt-1.5 flex items-center justify-between">
|
|
118
184
|
<span class="text-[10px] font-semibold uppercase tracking-wide text-[var(--accent)]">
|
|
119
|
-
{{ data.role }}
|
|
185
|
+
{{ data.roleLabel ?? data.role }}
|
|
120
186
|
</span>
|
|
121
187
|
<span v-if="data.overlay" class="font-mono text-[10px] text-muted">{{ data.overlay }}</span>
|
|
122
188
|
</div>
|
|
@@ -127,7 +193,7 @@ const accentLine = computed(
|
|
|
127
193
|
<span
|
|
128
194
|
v-if="data.firewallCount"
|
|
129
195
|
class="inline-flex items-center gap-1 rounded-pill bg-danger-soft px-1.5 py-0.5 text-[9px] font-medium text-danger"
|
|
130
|
-
:title="
|
|
196
|
+
:title="fill(text.firewallRules, { n: data.firewallCount! })"
|
|
131
197
|
>
|
|
132
198
|
<span>⛨</span>{{ data.firewallCount }}
|
|
133
199
|
</span>
|
|
@@ -137,8 +203,8 @@ const accentLine = computed(
|
|
|
137
203
|
:class="data.staleFor ? 'line-through decoration-danger/70' : ''"
|
|
138
204
|
:title="
|
|
139
205
|
data.staleFor
|
|
140
|
-
?
|
|
141
|
-
:
|
|
206
|
+
? fill(text.servicesStale, { n: data.serviceCount!, age: data.staleFor })
|
|
207
|
+
: fill(text.services, { n: data.serviceCount! })
|
|
142
208
|
"
|
|
143
209
|
>
|
|
144
210
|
<span>◇</span>{{ data.serviceCount }}
|
|
@@ -146,9 +212,9 @@ const accentLine = computed(
|
|
|
146
212
|
<span
|
|
147
213
|
v-if="data.staleFor"
|
|
148
214
|
class="inline-flex items-center gap-1 rounded-pill bg-danger-soft px-1.5 py-0.5 text-[9px] font-medium text-danger"
|
|
149
|
-
:title="
|
|
215
|
+
:title="fill(text.noContact, { age: data.staleFor })"
|
|
150
216
|
>
|
|
151
|
-
<span>⚠</span>{{ data.staleFor }} old
|
|
217
|
+
<span>⚠</span>{{ data.staleFor }} {{ text.old }}
|
|
152
218
|
</span>
|
|
153
219
|
</div>
|
|
154
220
|
</div>
|
|
@@ -77,10 +77,18 @@ function menuFor(row: T): ContextMenuItemDef[] {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// ── sorting ──────────────────────────────────────────────────
|
|
80
|
-
//
|
|
81
|
-
//
|
|
80
|
+
//
|
|
81
|
+
// The parent may drive `sort` through v-model, which is how a server-side sort
|
|
82
|
+
// is expressed. When it does not, the table keeps the state itself and sorts
|
|
83
|
+
// its own rows — otherwise `sortable` rendered a header that could be clicked,
|
|
84
|
+
// emitted an event nobody was listening for, and left the rows exactly as they
|
|
85
|
+
// were: a control that answers a click by doing nothing.
|
|
86
|
+
const ownSort = ref<SortState | null>(null)
|
|
87
|
+
const activeSort = computed(() => props.sort ?? ownSort.value)
|
|
88
|
+
|
|
89
|
+
// We compare by raw cell value with a stable-ish coerce.
|
|
82
90
|
const displayRows = computed<T[]>(() => {
|
|
83
|
-
const s =
|
|
91
|
+
const s = activeSort.value
|
|
84
92
|
if (!s) return props.rows
|
|
85
93
|
const col = props.columns.find((c) => c.key === s.key)
|
|
86
94
|
if (!col?.sortable) return props.rows
|
|
@@ -98,18 +106,22 @@ function compare(a: unknown, b: unknown): number {
|
|
|
98
106
|
|
|
99
107
|
function toggleSort(col: TableColumn<T>) {
|
|
100
108
|
if (!col.sortable) return
|
|
101
|
-
const s =
|
|
109
|
+
const s = activeSort.value
|
|
102
110
|
// Cycle: none → asc → desc → none, scoped to the clicked column.
|
|
103
111
|
let next: SortState | null
|
|
104
112
|
if (!s || s.key !== col.key) next = { key: col.key, dir: "asc" }
|
|
105
113
|
else if (s.dir === "asc") next = { key: col.key, dir: "desc" }
|
|
106
114
|
else next = null
|
|
115
|
+
// Kept locally as well as emitted: a parent that binds v-model overwrites
|
|
116
|
+
// this on the way back, and one that does not still gets a table that sorts.
|
|
117
|
+
ownSort.value = next
|
|
107
118
|
emit("update:sort", next)
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
function sortIcon(col: TableColumn<T>): string {
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
const s = activeSort.value
|
|
123
|
+
if (s?.key !== col.key) return "lucide:chevrons-up-down"
|
|
124
|
+
return s.dir === "asc" ? "lucide:arrow-up" : "lucide:arrow-down"
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
// ── selection ────────────────────────────────────────────────
|
|
@@ -237,7 +249,11 @@ const barInsetTop = computed(() =>
|
|
|
237
249
|
class="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted"
|
|
238
250
|
:class="alignClass(col.align)"
|
|
239
251
|
:aria-sort="
|
|
240
|
-
|
|
252
|
+
activeSort?.key === col.key
|
|
253
|
+
? activeSort.dir === 'asc'
|
|
254
|
+
? 'ascending'
|
|
255
|
+
: 'descending'
|
|
256
|
+
: undefined
|
|
241
257
|
"
|
|
242
258
|
>
|
|
243
259
|
<button
|
|
@@ -246,7 +262,7 @@ const barInsetTop = computed(() =>
|
|
|
246
262
|
class="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"
|
|
247
263
|
:class="[
|
|
248
264
|
col.align === 'right' ? 'flex-row-reverse' : '',
|
|
249
|
-
|
|
265
|
+
activeSort?.key === col.key ? 'text-ink' : '',
|
|
250
266
|
]"
|
|
251
267
|
@click="toggleSort(col)"
|
|
252
268
|
>
|
|
@@ -255,7 +271,11 @@ const barInsetTop = computed(() =>
|
|
|
255
271
|
:name="sortIcon(col)"
|
|
256
272
|
:size="13"
|
|
257
273
|
class="shrink-0 transition-[transform,opacity] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)]"
|
|
258
|
-
:class="
|
|
274
|
+
:class="
|
|
275
|
+
activeSort?.key === col.key
|
|
276
|
+
? 'opacity-100'
|
|
277
|
+
: 'opacity-40 group-hover/sort:opacity-70'
|
|
278
|
+
"
|
|
259
279
|
/>
|
|
260
280
|
</button>
|
|
261
281
|
<template v-else>{{ col.label }}</template>
|
|
@@ -266,8 +286,17 @@ const barInsetTop = computed(() =>
|
|
|
266
286
|
steer, and a stray key from a control inside a cell must not move the
|
|
267
287
|
cursor. The container is bound only so the composable can find rows
|
|
268
288
|
to focus. -->
|
|
269
|
-
|
|
270
|
-
|
|
289
|
+
<!-- Rows fade and settle rather than snapping in and out. Filtering a
|
|
290
|
+
list is a change the eye has to follow: rows vanishing between two
|
|
291
|
+
frames read as the table being redrawn, and the one that stayed is
|
|
292
|
+
impossible to keep track of. `move` is what carries the survivors to
|
|
293
|
+
their new position instead of teleporting them. -->
|
|
294
|
+
<TransitionGroup
|
|
295
|
+
tag="tbody"
|
|
296
|
+
name="lp-row"
|
|
297
|
+
:ref="(el: unknown) => roving.setContainer((el as { $el?: HTMLElement })?.$el ?? null)"
|
|
298
|
+
>
|
|
299
|
+
<tr v-if="displayRows.length === 0" key="lp-table-empty">
|
|
271
300
|
<td :colspan="colSpan" class="px-4 py-10 text-center text-muted">
|
|
272
301
|
<LpIcon :name="emptyIcon" :size="22" class="mx-auto mb-2 opacity-60" />
|
|
273
302
|
<div>{{ emptyLabel }}</div>
|
|
@@ -282,8 +311,12 @@ const barInsetTop = computed(() =>
|
|
|
282
311
|
:key="keyFor(row, index)"
|
|
283
312
|
:items="menuFor(row)"
|
|
284
313
|
>
|
|
314
|
+
<!-- `group` so a cell's own content can answer the row's hover: a
|
|
315
|
+
link inside a row wants its underline to sweep in when the row
|
|
316
|
+
lights up, not when the pointer crosses the text exactly. The
|
|
317
|
+
class paints nothing by itself. -->
|
|
285
318
|
<tr
|
|
286
|
-
class="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"
|
|
319
|
+
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"
|
|
287
320
|
:class="selectedSet.has(keyFor(row, index)) ? 'bg-brand-soft/40' : ''"
|
|
288
321
|
v-bind="roving.itemProps(String(keyFor(row, index)))"
|
|
289
322
|
@click="emit('rowClick', row)"
|
|
@@ -307,7 +340,56 @@ const barInsetTop = computed(() =>
|
|
|
307
340
|
</td>
|
|
308
341
|
</tr>
|
|
309
342
|
</LpContextMenu>
|
|
310
|
-
</
|
|
343
|
+
</TransitionGroup>
|
|
311
344
|
</table>
|
|
312
345
|
</LpScrollArea>
|
|
313
346
|
</template>
|
|
347
|
+
|
|
348
|
+
<style scoped>
|
|
349
|
+
/*
|
|
350
|
+
* Rows arriving, leaving and moving as the filter changes.
|
|
351
|
+
*
|
|
352
|
+
* A row cannot be translated on its Y axis without the table reflowing around
|
|
353
|
+
* it, so the entrance is opacity plus a small horizontal offset — enough to
|
|
354
|
+
* read as movement, not enough to fight the column grid. `--ease-emphasized`
|
|
355
|
+
* because this is interface responding to a click, not data being drawn.
|
|
356
|
+
*
|
|
357
|
+
* The leaving row is taken out of the flow, or the rows below it wait for the
|
|
358
|
+
* fade before closing the gap and the whole list lurches at the end instead of
|
|
359
|
+
* gliding.
|
|
360
|
+
*/
|
|
361
|
+
.lp-row-enter-active,
|
|
362
|
+
.lp-row-leave-active {
|
|
363
|
+
transition:
|
|
364
|
+
opacity var(--duration-fast) var(--ease-emphasized),
|
|
365
|
+
transform var(--duration-fast) var(--ease-emphasized);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.lp-row-move {
|
|
369
|
+
transition: transform var(--duration-medium) var(--ease-emphasized);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.lp-row-enter-from,
|
|
373
|
+
.lp-row-leave-to {
|
|
374
|
+
opacity: 0;
|
|
375
|
+
transform: translateX(-0.5rem);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/*
|
|
379
|
+
* The leaving row stays in the flow. Taking it out with `position: absolute` is
|
|
380
|
+
* the usual trick for a list, but a `<tr>` positioned that way loses the table's
|
|
381
|
+
* column grid and collapses to its content width — the row visibly breaks apart
|
|
382
|
+
* on its way out. The rows below close the gap through `lp-row-move` instead,
|
|
383
|
+
* which is a moment slower and stays a table the whole time.
|
|
384
|
+
*/
|
|
385
|
+
|
|
386
|
+
/* A filter that redraws the list is information, not decoration: with motion
|
|
387
|
+
turned down the rows change without animating rather than not changing. */
|
|
388
|
+
@media (prefers-reduced-motion: reduce) {
|
|
389
|
+
.lp-row-enter-active,
|
|
390
|
+
.lp-row-leave-active,
|
|
391
|
+
.lp-row-move {
|
|
392
|
+
transition: none;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
</style>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* and the choice survives a reload (anti-flash bootstrap). The component never
|
|
12
12
|
* fetches; it just drives the theme engine.
|
|
13
13
|
*/
|
|
14
|
-
import { computed, onMounted } from "vue"
|
|
14
|
+
import { computed, onMounted, ref } from "vue"
|
|
15
15
|
import { presets } from "../theme/presets"
|
|
16
16
|
import type { TokenSet } from "../theme/tokens"
|
|
17
17
|
import { useTheme } from "../theme/useTheme"
|
|
@@ -153,8 +153,30 @@ const menu = computed<ContextMenuItemDef[]>(() =>
|
|
|
153
153
|
})),
|
|
154
154
|
)
|
|
155
155
|
|
|
156
|
+
/*
|
|
157
|
+
* The theme the SERVER rendered is not necessarily the one the client restores:
|
|
158
|
+
* SSR paints from the `lp-theme` cookie (or the default), while on the client
|
|
159
|
+
* `bootstrapTheme()` deliberately wins from localStorage, which holds the full
|
|
160
|
+
* token set including custom themes the server cannot name. The colours cope —
|
|
161
|
+
* they are CSS variables, reapplied on boot — but a label that spells the theme
|
|
162
|
+
* OUT is text, and Vue compares text: every SSR consumer logged a hydration
|
|
163
|
+
* mismatch on this attribute alone.
|
|
164
|
+
*
|
|
165
|
+
* So the label says what the server said until the component is mounted, then
|
|
166
|
+
* names the theme actually in effect. Nobody reads a tooltip in the frame
|
|
167
|
+
* before hydration, and the a11y name settles on the truth.
|
|
168
|
+
*/
|
|
169
|
+
const hydrated = ref(false)
|
|
170
|
+
onMounted(() => {
|
|
171
|
+
hydrated.value = true
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const labelName = computed(() =>
|
|
175
|
+
hydrated.value ? activeName.value : (list.value[0]?.name ?? ""),
|
|
176
|
+
)
|
|
177
|
+
|
|
156
178
|
const triggerLabel = computed(
|
|
157
|
-
() => props["aria-label"] ?? `Theme: ${
|
|
179
|
+
() => props["aria-label"] ?? `Theme: ${labelName.value}. Click to switch, right-click to choose.`,
|
|
158
180
|
)
|
|
159
181
|
</script>
|
|
160
182
|
|
|
@@ -195,7 +217,7 @@ const triggerLabel = computed(
|
|
|
195
217
|
/>
|
|
196
218
|
</span>
|
|
197
219
|
|
|
198
|
-
<span v-if="wantsLabel" class="text-sm font-medium">{{
|
|
220
|
+
<span v-if="wantsLabel" class="text-sm font-medium">{{ labelName }}</span>
|
|
199
221
|
</button>
|
|
200
222
|
</LpContextMenu>
|
|
201
223
|
</template>
|
|
@@ -439,6 +439,10 @@ defineExpose({ fitView, setViewport: applyViewport })
|
|
|
439
439
|
</svg>
|
|
440
440
|
{{ l.label }}
|
|
441
441
|
</div>
|
|
442
|
+
<!-- Edges are only half of what a graph encodes; a node also carries
|
|
443
|
+
state (offline, stale readings) that the legend cannot know the
|
|
444
|
+
meaning of. The host explains its own node encoding here. -->
|
|
445
|
+
<slot name="legend-extra" />
|
|
442
446
|
</div>
|
|
443
447
|
</Panel>
|
|
444
448
|
</VueFlow>
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { defineComponent, computed, ref, watch, onBeforeUnmount, openBlock, createBlock, withCtx, createElementVNode, normalizeClass, createElementBlock, createVNode, createCommentVNode, Fragment, renderList, normalizeStyle, createTextVNode, toDisplayString, unref, mergeProps, withModifiers, renderSlot } from "vue";
|
|
2
|
-
import { _ as _sfc_main$2 } from "./LpCheckbox-BO0zuuPh.js";
|
|
3
|
-
import { _ as _sfc_main$4 } from "./LpContextMenu-D0mzXqA7.js";
|
|
4
|
-
import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
|
|
5
|
-
import { _ as _sfc_main$1 } from "./LpScrollArea-D8xyKXcF.js";
|
|
6
|
-
import { useRovingFocus } from "../composables/useRovingFocus.js";
|
|
7
|
-
const _hoisted_1 = { class: "w-full border-collapse text-sm" };
|
|
8
|
-
const _hoisted_2 = { class: "border-b border-line bg-surface-soft" };
|
|
9
|
-
const _hoisted_3 = {
|
|
10
|
-
key: 0,
|
|
11
|
-
class: "w-px px-4 py-3"
|
|
12
|
-
};
|
|
13
|
-
const _hoisted_4 = ["aria-sort"];
|
|
14
|
-
const _hoisted_5 = ["onClick"];
|
|
15
|
-
const _hoisted_6 = { key: 0 };
|
|
16
|
-
const _hoisted_7 = ["colspan"];
|
|
17
|
-
const _hoisted_8 = ["onClick"];
|
|
18
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
19
|
-
__name: "LpTable",
|
|
20
|
-
props: {
|
|
21
|
-
columns: {},
|
|
22
|
-
rows: {},
|
|
23
|
-
rowKey: {},
|
|
24
|
-
emptyLabel: { default: "Nothing here yet" },
|
|
25
|
-
emptyIcon: { default: "lucide:inbox" },
|
|
26
|
-
selectable: { type: Boolean, default: false },
|
|
27
|
-
selected: { default: () => [] },
|
|
28
|
-
sort: { default: null },
|
|
29
|
-
stickyHeader: { type: Boolean },
|
|
30
|
-
rowMenu: {}
|
|
31
|
-
},
|
|
32
|
-
emits: ["update:selected", "update:sort", "rowClick"],
|
|
33
|
-
setup(__props, { emit: __emit }) {
|
|
34
|
-
const props = __props;
|
|
35
|
-
const emit = __emit;
|
|
36
|
-
function keyFor(row, index) {
|
|
37
|
-
if (typeof props.rowKey === "function") return props.rowKey(row);
|
|
38
|
-
if (props.rowKey) return row[props.rowKey];
|
|
39
|
-
return index;
|
|
40
|
-
}
|
|
41
|
-
function alignClass(align) {
|
|
42
|
-
if (align === "right") return "text-right";
|
|
43
|
-
if (align === "center") return "text-center";
|
|
44
|
-
return "text-left";
|
|
45
|
-
}
|
|
46
|
-
function menuFor(row) {
|
|
47
|
-
return props.rowMenu?.(row) ?? [];
|
|
48
|
-
}
|
|
49
|
-
const displayRows = computed(() => {
|
|
50
|
-
const s = props.sort;
|
|
51
|
-
if (!s) return props.rows;
|
|
52
|
-
const col = props.columns.find((c) => c.key === s.key);
|
|
53
|
-
if (!col?.sortable) return props.rows;
|
|
54
|
-
const factor = s.dir === "asc" ? 1 : -1;
|
|
55
|
-
return [...props.rows].sort((a, b) => compare(a[s.key], b[s.key]) * factor);
|
|
56
|
-
});
|
|
57
|
-
function compare(a, b) {
|
|
58
|
-
if (a == null && b == null) return 0;
|
|
59
|
-
if (a == null) return -1;
|
|
60
|
-
if (b == null) return 1;
|
|
61
|
-
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
62
|
-
return String(a).localeCompare(String(b), void 0, { numeric: true });
|
|
63
|
-
}
|
|
64
|
-
function toggleSort(col) {
|
|
65
|
-
if (!col.sortable) return;
|
|
66
|
-
const s = props.sort;
|
|
67
|
-
let next;
|
|
68
|
-
if (!s || s.key !== col.key) next = { key: col.key, dir: "asc" };
|
|
69
|
-
else if (s.dir === "asc") next = { key: col.key, dir: "desc" };
|
|
70
|
-
else next = null;
|
|
71
|
-
emit("update:sort", next);
|
|
72
|
-
}
|
|
73
|
-
function sortIcon(col) {
|
|
74
|
-
if (props.sort?.key !== col.key) return "lucide:chevrons-up-down";
|
|
75
|
-
return props.sort.dir === "asc" ? "lucide:arrow-up" : "lucide:arrow-down";
|
|
76
|
-
}
|
|
77
|
-
const selectedSet = computed(() => new Set(props.selected));
|
|
78
|
-
const allKeys = computed(() => props.rows.map((r, i) => keyFor(r, i)));
|
|
79
|
-
const allChecked = computed(
|
|
80
|
-
() => allKeys.value.length > 0 && allKeys.value.every((k) => selectedSet.value.has(k))
|
|
81
|
-
);
|
|
82
|
-
const someChecked = computed(
|
|
83
|
-
() => !allChecked.value && allKeys.value.some((k) => selectedSet.value.has(k))
|
|
84
|
-
);
|
|
85
|
-
function toggleAll(checked) {
|
|
86
|
-
emit("update:selected", checked ? [...allKeys.value] : []);
|
|
87
|
-
}
|
|
88
|
-
function toggleRow(key, checked) {
|
|
89
|
-
const next = new Set(selectedSet.value);
|
|
90
|
-
if (checked) next.add(key);
|
|
91
|
-
else next.delete(key);
|
|
92
|
-
emit("update:selected", [...next]);
|
|
93
|
-
}
|
|
94
|
-
const colSpan = computed(() => props.columns.length + (props.selectable ? 1 : 0));
|
|
95
|
-
const rovingItems = computed(
|
|
96
|
-
() => displayRows.value.map((row, index) => ({
|
|
97
|
-
id: String(keyFor(row, index)),
|
|
98
|
-
label: String(row[props.columns[0]?.key ?? ""] ?? "")
|
|
99
|
-
}))
|
|
100
|
-
);
|
|
101
|
-
const rowByKey = computed(() => {
|
|
102
|
-
const map = /* @__PURE__ */ new Map();
|
|
103
|
-
displayRows.value.forEach((row, index) => map.set(String(keyFor(row, index)), row));
|
|
104
|
-
return map;
|
|
105
|
-
});
|
|
106
|
-
const roving = useRovingFocus(rovingItems, {
|
|
107
|
-
onActivate: (id) => {
|
|
108
|
-
const row = rowByKey.value.get(id);
|
|
109
|
-
if (!row) return;
|
|
110
|
-
emit("rowClick", row);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
function onRowKeydown(event) {
|
|
114
|
-
if (event.key === " " && props.selectable && roving.activeId.value) {
|
|
115
|
-
event.preventDefault();
|
|
116
|
-
const key = roving.activeId.value;
|
|
117
|
-
const row = rowByKey.value.get(key);
|
|
118
|
-
if (row) {
|
|
119
|
-
const rowKey = keyFor(row, displayRows.value.indexOf(row));
|
|
120
|
-
toggleRow(rowKey, !selectedSet.value.has(rowKey));
|
|
121
|
-
}
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
roving.onKeydown(event);
|
|
125
|
-
}
|
|
126
|
-
const headEl = ref(null);
|
|
127
|
-
const headHeight = ref(0);
|
|
128
|
-
let ro;
|
|
129
|
-
watch(
|
|
130
|
-
[headEl, () => props.stickyHeader],
|
|
131
|
-
([el, sticky]) => {
|
|
132
|
-
ro?.disconnect();
|
|
133
|
-
if (!el || !sticky || typeof ResizeObserver === "undefined") {
|
|
134
|
-
headHeight.value = 0;
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
ro = new ResizeObserver(() => {
|
|
138
|
-
headHeight.value = el.offsetHeight;
|
|
139
|
-
});
|
|
140
|
-
ro.observe(el);
|
|
141
|
-
headHeight.value = el.offsetHeight;
|
|
142
|
-
},
|
|
143
|
-
{ immediate: true, flush: "post" }
|
|
144
|
-
);
|
|
145
|
-
onBeforeUnmount(() => ro?.disconnect());
|
|
146
|
-
const barInsetTop = computed(
|
|
147
|
-
() => props.stickyHeader && headHeight.value ? `${headHeight.value}px` : void 0
|
|
148
|
-
);
|
|
149
|
-
return (_ctx, _cache) => {
|
|
150
|
-
return openBlock(), createBlock(_sfc_main$1, {
|
|
151
|
-
fade: false,
|
|
152
|
-
class: "rounded-card border border-line",
|
|
153
|
-
"bar-inset-top": barInsetTop.value
|
|
154
|
-
}, {
|
|
155
|
-
default: withCtx(() => [
|
|
156
|
-
createElementVNode("table", _hoisted_1, [
|
|
157
|
-
createElementVNode("thead", {
|
|
158
|
-
ref_key: "headEl",
|
|
159
|
-
ref: headEl,
|
|
160
|
-
class: normalizeClass(__props.stickyHeader ? "sticky top-0 z-10" : "")
|
|
161
|
-
}, [
|
|
162
|
-
createElementVNode("tr", _hoisted_2, [
|
|
163
|
-
__props.selectable ? (openBlock(), createElementBlock("th", _hoisted_3, [
|
|
164
|
-
createVNode(_sfc_main$2, {
|
|
165
|
-
"model-value": allChecked.value,
|
|
166
|
-
indeterminate: someChecked.value,
|
|
167
|
-
"onUpdate:modelValue": toggleAll
|
|
168
|
-
}, null, 8, ["model-value", "indeterminate"])
|
|
169
|
-
])) : createCommentVNode("", true),
|
|
170
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.columns, (col) => {
|
|
171
|
-
return openBlock(), createElementBlock("th", {
|
|
172
|
-
key: col.key,
|
|
173
|
-
style: normalizeStyle(col.width ? { width: col.width } : void 0),
|
|
174
|
-
class: normalizeClass(["px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted", alignClass(col.align)]),
|
|
175
|
-
"aria-sort": __props.sort?.key === col.key ? __props.sort.dir === "asc" ? "ascending" : "descending" : void 0
|
|
176
|
-
}, [
|
|
177
|
-
col.sortable ? (openBlock(), createElementBlock("button", {
|
|
178
|
-
key: 0,
|
|
179
|
-
type: "button",
|
|
180
|
-
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", [
|
|
181
|
-
col.align === "right" ? "flex-row-reverse" : "",
|
|
182
|
-
__props.sort?.key === col.key ? "text-ink" : ""
|
|
183
|
-
]]),
|
|
184
|
-
onClick: ($event) => toggleSort(col)
|
|
185
|
-
}, [
|
|
186
|
-
createTextVNode(toDisplayString(col.label) + " ", 1),
|
|
187
|
-
createVNode(_sfc_main$3, {
|
|
188
|
-
name: sortIcon(col),
|
|
189
|
-
size: 13,
|
|
190
|
-
class: normalizeClass(["shrink-0 transition-[transform,opacity] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)]", __props.sort?.key === col.key ? "opacity-100" : "opacity-40 group-hover/sort:opacity-70"])
|
|
191
|
-
}, null, 8, ["name", "class"])
|
|
192
|
-
], 10, _hoisted_5)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
193
|
-
createTextVNode(toDisplayString(col.label), 1)
|
|
194
|
-
], 64))
|
|
195
|
-
], 14, _hoisted_4);
|
|
196
|
-
}), 128))
|
|
197
|
-
])
|
|
198
|
-
], 2),
|
|
199
|
-
createElementVNode("tbody", {
|
|
200
|
-
ref: (el) => unref(roving).setContainer(el)
|
|
201
|
-
}, [
|
|
202
|
-
displayRows.value.length === 0 ? (openBlock(), createElementBlock("tr", _hoisted_6, [
|
|
203
|
-
createElementVNode("td", {
|
|
204
|
-
colspan: colSpan.value,
|
|
205
|
-
class: "px-4 py-10 text-center text-muted"
|
|
206
|
-
}, [
|
|
207
|
-
createVNode(_sfc_main$3, {
|
|
208
|
-
name: __props.emptyIcon,
|
|
209
|
-
size: 22,
|
|
210
|
-
class: "mx-auto mb-2 opacity-60"
|
|
211
|
-
}, null, 8, ["name"]),
|
|
212
|
-
createElementVNode("div", null, toDisplayString(__props.emptyLabel), 1)
|
|
213
|
-
], 8, _hoisted_7)
|
|
214
|
-
])) : createCommentVNode("", true),
|
|
215
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(displayRows.value, (row, index) => {
|
|
216
|
-
return openBlock(), createBlock(_sfc_main$4, {
|
|
217
|
-
key: keyFor(row, index),
|
|
218
|
-
items: menuFor(row)
|
|
219
|
-
}, {
|
|
220
|
-
default: withCtx(() => [
|
|
221
|
-
createElementVNode("tr", mergeProps({
|
|
222
|
-
class: ["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" : ""]
|
|
223
|
-
}, { ref_for: true }, unref(roving).itemProps(String(keyFor(row, index))), {
|
|
224
|
-
onClick: ($event) => emit("rowClick", row),
|
|
225
|
-
onKeydown: onRowKeydown
|
|
226
|
-
}), [
|
|
227
|
-
__props.selectable ? (openBlock(), createElementBlock("td", {
|
|
228
|
-
key: 0,
|
|
229
|
-
class: "w-px px-4 py-3",
|
|
230
|
-
onClick: _cache[0] || (_cache[0] = withModifiers(() => {
|
|
231
|
-
}, ["stop"]))
|
|
232
|
-
}, [
|
|
233
|
-
createVNode(_sfc_main$2, {
|
|
234
|
-
"model-value": selectedSet.value.has(keyFor(row, index)),
|
|
235
|
-
"onUpdate:modelValue": (v) => toggleRow(keyFor(row, index), v)
|
|
236
|
-
}, null, 8, ["model-value", "onUpdate:modelValue"])
|
|
237
|
-
])) : createCommentVNode("", true),
|
|
238
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.columns, (col) => {
|
|
239
|
-
return openBlock(), createElementBlock("td", {
|
|
240
|
-
key: col.key,
|
|
241
|
-
class: normalizeClass(["px-4 py-3 text-ink", alignClass(col.align)])
|
|
242
|
-
}, [
|
|
243
|
-
renderSlot(_ctx.$slots, `cell-${col.key}`, {
|
|
244
|
-
row,
|
|
245
|
-
value: row[col.key]
|
|
246
|
-
}, () => [
|
|
247
|
-
createTextVNode(toDisplayString(row[col.key]), 1)
|
|
248
|
-
])
|
|
249
|
-
], 2);
|
|
250
|
-
}), 128))
|
|
251
|
-
], 16, _hoisted_8)
|
|
252
|
-
]),
|
|
253
|
-
_: 2
|
|
254
|
-
}, 1032, ["items"]);
|
|
255
|
-
}), 128))
|
|
256
|
-
], 512)
|
|
257
|
-
])
|
|
258
|
-
]),
|
|
259
|
-
_: 3
|
|
260
|
-
}, 8, ["bar-inset-top"]);
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
export {
|
|
265
|
-
_sfc_main as _
|
|
266
|
-
};
|