@leavepulse/ui 0.22.3 → 0.22.5

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.
@@ -16,33 +16,44 @@ const _hoisted_8 = {
16
16
  };
17
17
  const _hoisted_9 = ["title"];
18
18
  const _hoisted_10 = ["title"];
19
+ const _hoisted_11 = ["title"];
19
20
  const _sfc_main = /* @__PURE__ */ defineComponent({
20
21
  __name: "LpInfraNode",
21
22
  props: {
22
23
  data: {},
23
- selected: { type: Boolean }
24
+ selected: { type: Boolean },
25
+ roles: {}
24
26
  },
25
27
  setup(__props) {
26
28
  const props = __props;
27
- const roleVar = {
28
- hypervisor: "var(--color-accent)",
29
- game: "var(--color-action)",
30
- router: "var(--color-muted-strong)",
31
- edge: "var(--color-brand)"
29
+ const DEFAULT_ROLES = {
30
+ compute: { color: "var(--color-node-compute)", icon: "▣" },
31
+ host: { color: "var(--color-node-compute)", icon: "▣" },
32
+ worker: { color: "var(--color-node-compute)", icon: "◱" },
33
+ hypervisor: { color: "var(--color-node-compute)", icon: "▦" },
34
+ game: { color: "var(--color-node-compute)", icon: "◈" },
35
+ storage: { color: "var(--color-node-transit)", icon: "▤" },
36
+ database: { color: "var(--color-node-transit)", icon: "▤" },
37
+ router: { color: "var(--color-node-transit)", icon: "⇄" },
38
+ proxy: { color: "var(--color-node-transit)", icon: "⇅" },
39
+ edge: { color: "var(--color-node-edge)", icon: "☁" },
40
+ public: { color: "var(--color-node-edge)", icon: "◉" }
32
41
  };
33
- const accent = computed(() => roleVar[props.data.role] ?? "var(--color-line-strong)");
34
- const roleIcon = {
35
- hypervisor: "▣",
36
- game: "◈",
37
- router: "⇄",
38
- edge: "☁"
39
- };
40
- const icon = computed(() => roleIcon[props.data.role] ?? "●");
42
+ const UNKNOWN_ROLE = { color: "var(--color-node-unknown)", icon: "●" };
43
+ const role = computed(
44
+ () => ({ ...DEFAULT_ROLES, ...props.roles ?? {} })[props.data.role] ?? UNKNOWN_ROLE
45
+ );
46
+ const accent = computed(() => role.value.color);
47
+ const icon = computed(() => role.value.icon);
48
+ const accentLine = computed(
49
+ () => `color-mix(in srgb, ${accent.value} 35%, var(--color-line))`
50
+ );
41
51
  return (_ctx, _cache) => {
42
52
  return openBlock(), createElementBlock("div", {
43
- class: normalizeClass(["w-[168px] overflow-hidden rounded-card border bg-surface-raised shadow-panel transition-[border-color,box-shadow]", __props.selected ? "border-[var(--accent)]" : "border-line"]),
53
+ class: normalizeClass(["w-[168px] overflow-hidden rounded-card border bg-surface-raised shadow-panel transition-[border-color,box-shadow]", __props.selected ? "border-[var(--accent)]" : "border-[var(--accent-line)]"]),
44
54
  style: normalizeStyle({
45
55
  "--accent": accent.value,
56
+ "--accent-line": accentLine.value,
46
57
  opacity: __props.data.dimmed ? 0.3 : __props.data.online === false ? 0.5 : 1,
47
58
  ...__props.selected ? { boxShadow: "0 0 0 2px color-mix(in srgb, var(--accent) 40%, transparent), var(--shadow-panel)" } : {}
48
59
  })
@@ -57,7 +68,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
57
68
  position: unref(Position).Right,
58
69
  class: "!size-2 !border-2 !border-[var(--accent)] !bg-surface"
59
70
  }, null, 8, ["position"]),
60
- _cache[2] || (_cache[2] = createElementVNode("div", { class: "h-[3px] bg-[var(--accent)]" }, null, -1)),
71
+ _cache[3] || (_cache[3] = createElementVNode("div", { class: "h-[3px] bg-[var(--accent)]" }, null, -1)),
61
72
  createElementVNode("div", _hoisted_1, [
62
73
  createElementVNode("div", _hoisted_2, [
63
74
  createElementVNode("span", _hoisted_3, toDisplayString(icon.value), 1),
@@ -71,7 +82,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
71
82
  createElementVNode("span", _hoisted_6, toDisplayString(__props.data.role), 1),
72
83
  __props.data.overlay ? (openBlock(), createElementBlock("span", _hoisted_7, toDisplayString(__props.data.overlay), 1)) : createCommentVNode("", true)
73
84
  ]),
74
- __props.data.firewallCount || __props.data.serviceCount !== void 0 ? (openBlock(), createElementBlock("div", _hoisted_8, [
85
+ __props.data.firewallCount || __props.data.serviceCount !== void 0 || __props.data.staleFor ? (openBlock(), createElementBlock("div", _hoisted_8, [
75
86
  __props.data.firewallCount ? (openBlock(), createElementBlock("span", {
76
87
  key: 0,
77
88
  class: "inline-flex items-center gap-1 rounded-pill bg-danger-soft px-1.5 py-0.5 text-[9px] font-medium text-danger",
@@ -82,12 +93,20 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
82
93
  ], 8, _hoisted_9)) : createCommentVNode("", true),
83
94
  __props.data.serviceCount !== void 0 ? (openBlock(), createElementBlock("span", {
84
95
  key: 1,
85
- class: "inline-flex items-center gap-1 rounded-pill bg-surface-soft px-1.5 py-0.5 text-[9px] font-medium text-muted",
86
- title: `${__props.data.serviceCount} service(s)`
96
+ 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 ? `${__props.data.serviceCount} service(s) as of ${__props.data.staleFor} ago — not current` : `${__props.data.serviceCount} service(s)`
87
98
  }, [
88
99
  _cache[1] || (_cache[1] = createElementVNode("span", null, "◇", -1)),
89
100
  createTextVNode(toDisplayString(__props.data.serviceCount), 1)
90
- ], 8, _hoisted_10)) : createCommentVNode("", true)
101
+ ], 10, _hoisted_10)) : createCommentVNode("", true),
102
+ __props.data.staleFor ? (openBlock(), createElementBlock("span", {
103
+ key: 2,
104
+ 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: `No contact for ${__props.data.staleFor} — everything shown here is that old`
106
+ }, [
107
+ _cache[2] || (_cache[2] = createElementVNode("span", null, "⚠", -1)),
108
+ createTextVNode(toDisplayString(__props.data.staleFor) + " old ", 1)
109
+ ], 8, _hoisted_11)) : createCommentVNode("", true)
91
110
  ])) : createCommentVNode("", true)
92
111
  ])
93
112
  ], 6);
@@ -3,7 +3,7 @@ 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-C_PHcthC.js";
6
+ import { _ as _sfc_main$3 } from "./LpInfraNode-qIL3l7IO.js";
7
7
  import { _ as _sfc_main$1 } from "./LpLaneNode-HtnQf2FF.js";
8
8
  import { _ as _sfc_main$2 } from "./LpServiceNode-Dc-Ut5RS.js";
9
9
  const _hoisted_1 = { class: "flex flex-col gap-1.5 rounded-card border border-line bg-surface-overlay/90 px-3 py-2 text-[11px] backdrop-blur" };
@@ -9,13 +9,27 @@ export interface InfraNodeData {
9
9
  firewallCount?: number;
10
10
  /** Service count on this host (badge when defined — services toggle on). */
11
11
  serviceCount?: number;
12
+ /**
13
+ * How old this node's readings are, human-readable ("28d"). An unreachable
14
+ * machine keeps serving its last snapshot, which reads as current unless the
15
+ * card says otherwise — so when set, the counts below are struck through and
16
+ * the age is shown in their place.
17
+ */
18
+ staleFor?: string;
12
19
  /** Fade the node back (search miss / not part of the focused node's graph). */
13
20
  dimmed?: boolean;
14
21
  }
15
22
  type __VLS_Props = {
16
23
  data: InfraNodeData;
17
24
  selected?: boolean;
25
+ /** Extra/overriding role styles, merged over the defaults below. */
26
+ roles?: Record<string, InfraRoleStyle>;
18
27
  };
28
+ export interface InfraRoleStyle {
29
+ /** CSS colour — normally a var() reference, e.g. var(--color-node-edge). */
30
+ color: string;
31
+ icon: string;
32
+ }
19
33
  declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
20
34
  declare const _default: typeof __VLS_export;
21
35
  export default _default;
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpInfraNode-C_PHcthC.js";
1
+ import { _ as _sfc_main } from "../chunks/LpInfraNode-qIL3l7IO.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpTopologyCanvas-DCQINTZE.js";
1
+ import { _ as _sfc_main } from "../chunks/LpTopologyCanvas-BsJWyacK.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ import { _ as _23 } from "./chunks/LpFileTree-DWx77B1F.js";
26
26
  import { ancestorIds, checkStateOf, checkedCount, computeStats, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize } from "./components/fileTree.js";
27
27
  import { _ as _24 } from "./chunks/LpFormField-Bn-ZwC6z.js";
28
28
  import { _ as _25 } from "./chunks/LpIcon-CCnX5_2j.js";
29
- import { _ as _26 } from "./chunks/LpInfraNode-C_PHcthC.js";
29
+ import { _ as _26 } from "./chunks/LpInfraNode-qIL3l7IO.js";
30
30
  import { _ as _27 } from "./chunks/LpInput-BauZRyzm.js";
31
31
  import { _ as _28 } from "./chunks/LpLightbox-Ci35Y_xP.js";
32
32
  import { fileNameOf } from "./components/lightbox.js";
@@ -64,7 +64,7 @@ import { D, _ as _56, N, T, b, c, d, e, f, g, i, l, h, j, n, r, t, k, m, u, v }
64
64
  import { _ as _57 } from "./chunks/LpTilt-DuyKbpkA.js";
65
65
  import { _ as _58 } from "./chunks/LpToaster-gDB1EaU3.js";
66
66
  import { _ as _59 } from "./chunks/LpTooltip-D1hbjQWG.js";
67
- import { _ as _60 } from "./chunks/LpTopologyCanvas-DCQINTZE.js";
67
+ import { _ as _60 } from "./chunks/LpTopologyCanvas-BsJWyacK.js";
68
68
  import { _ as _61 } from "./chunks/LpUptimeBar-CB366k0O.js";
69
69
  import { deserializeLayout, serializeLayout } from "./layout/tree.js";
70
70
  import { addLeaf, addTab, countBlocks, countLeaves, makeLayout, moveLeaf, removeBlock, removeLeaf, reorderTab, resizeAt, setActiveTab } from "./layout/tree.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.22.3",
3
+ "version": "0.22.5",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -22,34 +22,70 @@ export interface InfraNodeData {
22
22
  firewallCount?: number
23
23
  /** Service count on this host (badge when defined — services toggle on). */
24
24
  serviceCount?: number
25
+ /**
26
+ * How old this node's readings are, human-readable ("28d"). An unreachable
27
+ * machine keeps serving its last snapshot, which reads as current unless the
28
+ * card says otherwise — so when set, the counts below are struck through and
29
+ * the age is shown in their place.
30
+ */
31
+ staleFor?: string
25
32
  /** Fade the node back (search miss / not part of the focused node's graph). */
26
33
  dimmed?: boolean
27
34
  }
28
35
 
29
- const props = defineProps<{ data: InfraNodeData; selected?: boolean }>()
36
+ const props = defineProps<{
37
+ data: InfraNodeData
38
+ selected?: boolean
39
+ /** Extra/overriding role styles, merged over the defaults below. */
40
+ roles?: Record<string, InfraRoleStyle>
41
+ }>()
30
42
 
31
- const roleVar: Record<string, string> = {
32
- hypervisor: "var(--color-accent)",
33
- game: "var(--color-action)",
34
- router: "var(--color-muted-strong)",
35
- edge: "var(--color-brand)",
43
+ /*
44
+ * Role → look. Colours are token references, never literals, so a theme (or a
45
+ * consumer) restyles the graph without forking this component; the glyph keeps
46
+ * the encoding readable without colour. `roles` overrides or extends the table
47
+ * for a fleet whose vocabulary differs — anything unmapped stays neutral rather
48
+ * than silently borrowing another role's meaning.
49
+ */
50
+ export interface InfraRoleStyle {
51
+ /** CSS colour — normally a var() reference, e.g. var(--color-node-edge). */
52
+ color: string
53
+ icon: string
36
54
  }
37
- const accent = computed(() => roleVar[props.data.role] ?? "var(--color-line-strong)")
38
- const roleIcon: Record<string, string> = {
39
- hypervisor: "▣",
40
- game: "◈",
41
- router: "⇄",
42
- edge: "☁",
55
+ const DEFAULT_ROLES: Record<string, InfraRoleStyle> = {
56
+ compute: { color: "var(--color-node-compute)", icon: "▣" },
57
+ host: { color: "var(--color-node-compute)", icon: "▣" },
58
+ worker: { color: "var(--color-node-compute)", icon: "◱" },
59
+ hypervisor: { color: "var(--color-node-compute)", icon: "▦" },
60
+ game: { color: "var(--color-node-compute)", icon: "◈" },
61
+ storage: { color: "var(--color-node-transit)", icon: "▤" },
62
+ database: { color: "var(--color-node-transit)", icon: "▤" },
63
+ router: { color: "var(--color-node-transit)", icon: "⇄" },
64
+ proxy: { color: "var(--color-node-transit)", icon: "⇅" },
65
+ edge: { color: "var(--color-node-edge)", icon: "☁" },
66
+ public: { color: "var(--color-node-edge)", icon: "◉" },
43
67
  }
44
- const icon = computed(() => roleIcon[props.data.role] ?? "●")
68
+ const UNKNOWN_ROLE: InfraRoleStyle = { color: "var(--color-node-unknown)", icon: "●" }
69
+
70
+ const role = computed<InfraRoleStyle>(
71
+ () => ({ ...DEFAULT_ROLES, ...(props.roles ?? {}) })[props.data.role] ?? UNKNOWN_ROLE,
72
+ )
73
+ const accent = computed(() => role.value.color)
74
+ const icon = computed(() => role.value.icon)
75
+ // A tinted edge rather than a grey one: with only the top stripe carrying the
76
+ // role, a canvas of cards read as one undifferentiated grid.
77
+ const accentLine = computed(
78
+ () => `color-mix(in srgb, ${accent.value} 35%, var(--color-line))`,
79
+ )
45
80
  </script>
46
81
 
47
82
  <template>
48
83
  <div
49
84
  class="w-[168px] overflow-hidden rounded-card border bg-surface-raised shadow-panel transition-[border-color,box-shadow]"
50
- :class="selected ? 'border-[var(--accent)]' : 'border-line'"
85
+ :class="selected ? 'border-[var(--accent)]' : 'border-[var(--accent-line)]'"
51
86
  :style="{
52
87
  '--accent': accent,
88
+ '--accent-line': accentLine,
53
89
  opacity: data.dimmed ? 0.3 : data.online === false ? 0.5 : 1,
54
90
  ...(selected
55
91
  ? { boxShadow: '0 0 0 2px color-mix(in srgb, var(--accent) 40%, transparent), var(--shadow-panel)' }
@@ -85,7 +121,7 @@ const icon = computed(() => roleIcon[props.data.role] ?? "●")
85
121
  <span v-if="data.overlay" class="font-mono text-[10px] text-muted">{{ data.overlay }}</span>
86
122
  </div>
87
123
  <div
88
- v-if="data.firewallCount || data.serviceCount !== undefined"
124
+ v-if="data.firewallCount || data.serviceCount !== undefined || data.staleFor"
89
125
  class="mt-1.5 flex items-center gap-1.5"
90
126
  >
91
127
  <span
@@ -98,10 +134,22 @@ const icon = computed(() => roleIcon[props.data.role] ?? "●")
98
134
  <span
99
135
  v-if="data.serviceCount !== undefined"
100
136
  class="inline-flex items-center gap-1 rounded-pill bg-surface-soft px-1.5 py-0.5 text-[9px] font-medium text-muted"
101
- :title="`${data.serviceCount} service(s)`"
137
+ :class="data.staleFor ? 'line-through decoration-danger/70' : ''"
138
+ :title="
139
+ data.staleFor
140
+ ? `${data.serviceCount} service(s) as of ${data.staleFor} ago — not current`
141
+ : `${data.serviceCount} service(s)`
142
+ "
102
143
  >
103
144
  <span>◇</span>{{ data.serviceCount }}
104
145
  </span>
146
+ <span
147
+ v-if="data.staleFor"
148
+ 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="`No contact for ${data.staleFor} — everything shown here is that old`"
150
+ >
151
+ <span>⚠</span>{{ data.staleFor }} old
152
+ </span>
105
153
  </div>
106
154
  </div>
107
155
  </div>
@@ -72,12 +72,8 @@ const widthClass = computed(() => {
72
72
  // inset horizontally; top only when there's no header, bottom only when there's
73
73
  // no footer (header/footer own those edges).
74
74
  const slots = useSlots()
75
- /*
76
- * The body clips its overflow, and a focus ring is drawn OUTSIDE the control it
77
- * belongs to — so a field sitting flush against the top of the body had its
78
- * ring sliced off. Under a header the gap is only the ring's width plus a
79
- * hair; without one the existing pt-5 already clears it.
80
- */
75
+ // The body clips overflow and focus rings are drawn outside their control, so a
76
+ // field flush against the top edge lost its ring; pt-1 clears the ring's width.
81
77
  const bodyPad = computed(() =>
82
78
  [
83
79
  "px-5",
@@ -46,6 +46,17 @@
46
46
  --color-line-strong: rgba(255, 255, 255, 0.18);
47
47
  --color-ring: rgba(0, 188, 255, 0.45);
48
48
 
49
+ /* ── color: infra roles ──────────────────────────
50
+ What a machine on a topology DOES, in three families: it runs work, it
51
+ carries traffic, or it is the way in from outside. Derived from the palette
52
+ above so a theme gets them for free, and named so a consumer whose fleet
53
+ doesn't split this way can repoint them without forking the component.
54
+ Anything unmapped falls back to --color-node-unknown. */
55
+ --color-node-compute: var(--color-action);
56
+ --color-node-transit: var(--color-accent);
57
+ --color-node-edge: var(--color-brand);
58
+ --color-node-unknown: var(--color-line-strong);
59
+
49
60
  /* ── shape ───────────────────────────────────── */
50
61
  --radius-control: 8px;
51
62
  --radius-card: 14px;