@leavepulse/ui 0.92.0 → 0.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,19 +6,28 @@
6
6
  * outside and only the dots between facts are seen. A cell whose slot drew
7
7
  * nothing (a v-if that failed, an empty value) is left out, dot and all.
8
8
  */
9
- .lp-line[data-v-62285817] {
9
+ .lp-line[data-v-5ac91b9e] {
10
10
  --lp-line-sep: 1rem;
11
11
  }
12
- .lp-line-row[data-v-62285817] {
12
+ .lp-line-row[data-v-5ac91b9e] {
13
13
  margin-left: calc(-1 * var(--lp-line-sep));
14
14
  }
15
- .lp-line-field[data-v-62285817]::before {
15
+
16
+ /* The dot lives in the field's own left padding, so whatever the cell draws,
17
+ a word or a block of two lines, starts after it and never in the clipped
18
+ strip. */
19
+ .lp-line-field[data-v-5ac91b9e] {
20
+ position: relative;
21
+ padding-left: var(--lp-line-sep);
22
+ }
23
+ .lp-line-field[data-v-5ac91b9e]::before {
16
24
  content: "·" / "";
17
- display: inline-block;
25
+ position: absolute;
26
+ left: 0;
18
27
  width: var(--lp-line-sep);
19
28
  text-align: center;
20
29
  }
21
- .lp-line-field[data-v-62285817]:has(> .lp-line-value:empty) {
30
+ .lp-line-field[data-v-5ac91b9e]:has(> .lp-line-value:empty) {
22
31
  display: none;
23
32
  }
24
33
 
@@ -34,17 +43,17 @@
34
43
  * fade before closing the gap and the whole list lurches at the end instead of
35
44
  * gliding.
36
45
  */
37
- .lp-row-enter-active[data-v-62285817],
38
- .lp-row-leave-active[data-v-62285817] {
46
+ .lp-row-enter-active[data-v-5ac91b9e],
47
+ .lp-row-leave-active[data-v-5ac91b9e] {
39
48
  transition:
40
49
  opacity var(--duration-fast) var(--ease-emphasized),
41
50
  transform var(--duration-fast) var(--ease-emphasized);
42
51
  }
43
- .lp-row-move[data-v-62285817] {
52
+ .lp-row-move[data-v-5ac91b9e] {
44
53
  transition: transform var(--duration-medium) var(--ease-emphasized);
45
54
  }
46
- .lp-row-enter-from[data-v-62285817],
47
- .lp-row-leave-to[data-v-62285817] {
55
+ .lp-row-enter-from[data-v-5ac91b9e],
56
+ .lp-row-leave-to[data-v-5ac91b9e] {
48
57
  opacity: 0;
49
58
  transform: translateX(-0.5rem);
50
59
  }
@@ -58,7 +67,7 @@
58
67
  * drops out of the table layout entirely. Nothing sits below this row to be
59
68
  * dragged around, so simply not animating its exit costs nothing.
60
69
  */
61
- .lp-row-empty.lp-row-leave-active[data-v-62285817] {
70
+ .lp-row-empty.lp-row-leave-active[data-v-5ac91b9e] {
62
71
  display: none;
63
72
  }
64
73
 
@@ -77,7 +86,7 @@
77
86
  * once anything is selected. An earlier version collapsed the column to nothing
78
87
  * and floated the hover box over the first cell, where it sat on the text.
79
88
  */
80
- .lp-tick[data-v-62285817] {
89
+ .lp-tick[data-v-5ac91b9e] {
81
90
  display: flex;
82
91
  align-items: center;
83
92
  max-width: 0;
@@ -89,17 +98,17 @@
89
98
  padding var(--duration-medium) var(--ease-emphasized),
90
99
  opacity var(--duration-fast) ease;
91
100
  }
92
- .lp-tick-open[data-v-62285817] {
101
+ .lp-tick-open[data-v-5ac91b9e] {
93
102
  max-width: 3.25rem;
94
103
  padding-inline: 1rem;
95
104
  opacity: 1;
96
105
  }
97
- .lp-tick-hidden[data-v-62285817],
98
- .lp-tick-cue[data-v-62285817] {
106
+ .lp-tick-hidden[data-v-5ac91b9e],
107
+ .lp-tick-cue[data-v-5ac91b9e] {
99
108
  opacity: 0;
100
109
  }
101
- tr:hover .lp-tick-cue[data-v-62285817],
102
- .lp-tick-cue[data-v-62285817]:focus-within {
110
+ tr:hover .lp-tick-cue[data-v-5ac91b9e],
111
+ .lp-tick-cue[data-v-5ac91b9e]:focus-within {
103
112
  opacity: 1;
104
113
  }
105
114
 
@@ -107,15 +116,15 @@ tr:hover .lp-tick-cue[data-v-62285817],
107
116
  turned down the rows change without animating rather than not changing. */
108
117
  /* Only where there is a hover to reveal it; a finger long-presses instead. */
109
118
  @media (hover: none) {
110
- tr:hover .lp-tick-cue[data-v-62285817] {
119
+ tr:hover .lp-tick-cue[data-v-5ac91b9e] {
111
120
  opacity: 0;
112
121
  }
113
122
 
114
123
  /* Nor is there anything for the column's width to hold until then: on a
115
124
  touch screen it stays closed until a long press starts selecting, and
116
125
  eases open like the rest of the column. */
117
- .lp-tick-hidden[data-v-62285817],
118
- .lp-tick-cue[data-v-62285817] {
126
+ .lp-tick-hidden[data-v-5ac91b9e],
127
+ .lp-tick-cue[data-v-5ac91b9e] {
119
128
  max-width: 0;
120
129
  padding-inline: 0;
121
130
  }
@@ -125,7 +134,7 @@ tr:hover .lp-tick-cue[data-v-62285817] {
125
134
  * A list line's tick opens from nothing, as the table's column does, so lines
126
135
  * do not carry an empty gap until selection starts.
127
136
  */
128
- .lp-tick-line[data-v-62285817] {
137
+ .lp-tick-line[data-v-5ac91b9e] {
129
138
  max-width: 2rem;
130
139
  opacity: 1;
131
140
  }
@@ -135,28 +144,28 @@ tr:hover .lp-tick-cue[data-v-62285817] {
135
144
  * open; a cue shows it only while
136
145
  * the card is pointed at or holds focus, the same rule as a row's.
137
146
  */
138
- .lp-tick-card[data-v-62285817] {
147
+ .lp-tick-card[data-v-5ac91b9e] {
139
148
  max-width: none;
140
149
  padding-inline: 0;
141
150
  opacity: 1;
142
151
  }
143
- .lp-tick[data-cue][data-v-62285817] {
152
+ .lp-tick[data-cue][data-v-5ac91b9e] {
144
153
  opacity: 0;
145
154
  }
146
- .lp-item:hover .lp-tick[data-cue][data-v-62285817],
147
- .lp-tick[data-cue][data-v-62285817]:focus-within {
155
+ .lp-item:hover .lp-tick[data-cue][data-v-5ac91b9e],
156
+ .lp-tick[data-cue][data-v-5ac91b9e]:focus-within {
148
157
  opacity: 1;
149
158
  }
150
159
  @media (hover: none) {
151
- .lp-item:hover .lp-tick[data-cue][data-v-62285817] {
160
+ .lp-item:hover .lp-tick[data-cue][data-v-5ac91b9e] {
152
161
  opacity: 0;
153
162
  }
154
163
  }
155
164
  @media (prefers-reduced-motion: reduce) {
156
- .lp-row-enter-active[data-v-62285817],
157
- .lp-row-leave-active[data-v-62285817],
158
- .lp-row-move[data-v-62285817],
159
- .lp-tick[data-v-62285817] {
165
+ .lp-row-enter-active[data-v-5ac91b9e],
166
+ .lp-row-leave-active[data-v-5ac91b9e],
167
+ .lp-row-move[data-v-5ac91b9e],
168
+ .lp-tick[data-v-5ac91b9e] {
160
169
  transition: none;
161
170
  }
162
171
  }
@@ -2,6 +2,8 @@ import { defineComponent, computed, openBlock, createElementBlock, normalizeStyl
2
2
  import { Handle, Position } from "@vue-flow/core";
3
3
  import { _ as _sfc_main$2 } from "./LpBadge-Bh5roBr_.js";
4
4
  import { _ as _sfc_main$1 } from "./LpStatus-C6GqSHJj.js";
5
+ import { TONE_COLOR } from "../components/meter.js";
6
+ import { STATUS_LOOK } from "../components/status.js";
5
7
  const _hoisted_1 = { class: "px-[11px] pb-2.5 pt-[9px]" };
6
8
  const _hoisted_2 = { class: "flex items-center gap-[7px]" };
7
9
  const _hoisted_3 = { class: "text-[13px] text-[var(--accent)]" };
@@ -74,15 +76,25 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
74
76
  const accentLine = computed(
75
77
  () => `color-mix(in srgb, ${accent.value} 35%, var(--color-line))`
76
78
  );
79
+ const state = computed(
80
+ () => props.data.status ?? (props.data.online === void 0 ? void 0 : props.data.online ? "online" : "down")
81
+ );
82
+ const troubled = computed(() => state.value !== void 0 && state.value !== "online");
83
+ const lost = computed(() => state.value === "down" || state.value === "offline");
84
+ const stateColor = computed(
85
+ () => state.value ? `color-mix(in srgb, ${TONE_COLOR[STATUS_LOOK[state.value].tone]} 50%, transparent)` : void 0
86
+ );
77
87
  return (_ctx, _cache) => {
78
88
  return openBlock(), createElementBlock("div", {
79
- class: normalizeClass(["w-[168px] overflow-hidden rounded-card border bg-surface-raised shadow-panel transition-[border-color,box-shadow]", [
80
- __props.selected ? "border-[var(--accent)]" : __props.data.online === false ? "border-dashed border-danger/50" : "border-[var(--accent-line)]"
89
+ class: normalizeClass(["w-[168px] overflow-hidden rounded-card border bg-surface-raised shadow-panel transition-[border-color,box-shadow,opacity] duration-medium ease-[var(--ease-emphasized)]", [
90
+ __props.selected ? "border-[var(--accent)]" : troubled.value ? "border-[var(--state-line)]" : "border-[var(--accent-line)]",
91
+ !__props.selected && lost.value ? "border-dashed" : ""
81
92
  ]]),
82
93
  style: normalizeStyle({
83
94
  "--accent": accent.value,
84
95
  "--accent-line": accentLine.value,
85
- opacity: __props.data.dimmed ? 0.3 : __props.data.online === false ? 0.75 : 1,
96
+ "--state-line": stateColor.value,
97
+ opacity: __props.data.dimmed ? 0.3 : lost.value ? 0.75 : 1,
86
98
  ...__props.selected ? { boxShadow: "0 0 0 2px color-mix(in srgb, var(--accent) 40%, transparent), var(--shadow-panel)" } : {}
87
99
  })
88
100
  }, [
@@ -97,18 +109,17 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
97
109
  class: "!size-2 !border-2 !border-[var(--accent)] !bg-surface"
98
110
  }, null, 8, ["position"]),
99
111
  createElementVNode("div", {
100
- class: normalizeClass(["h-[3px]", __props.data.online === false ? "bg-danger/50" : "bg-[var(--accent)]"])
112
+ class: normalizeClass(["h-[3px] transition-[background-color] duration-medium ease-[var(--ease-emphasized)]", troubled.value ? "bg-[var(--state-line)]" : "bg-[var(--accent)]"])
101
113
  }, null, 2),
102
114
  createElementVNode("div", _hoisted_1, [
103
115
  createElementVNode("div", _hoisted_2, [
104
116
  createElementVNode("span", _hoisted_3, toDisplayString(icon.value), 1),
105
117
  createElementVNode("span", _hoisted_4, toDisplayString(__props.data.name), 1),
106
- __props.data.online !== void 0 ? (openBlock(), createBlock(_sfc_main$1, {
118
+ state.value ? (openBlock(), createBlock(_sfc_main$1, {
107
119
  key: 0,
108
- state: __props.data.online ? "online" : "down",
109
- pulse: __props.data.online,
120
+ state: state.value,
110
121
  "hide-label": ""
111
- }, null, 8, ["state", "pulse"])) : createCommentVNode("", true)
122
+ }, null, 8, ["state"])) : createCommentVNode("", true)
112
123
  ]),
113
124
  createElementVNode("div", _hoisted_5, [
114
125
  createElementVNode("span", _hoisted_6, toDisplayString(__props.data.roleLabel ?? __props.data.role), 1),
@@ -10,7 +10,7 @@ import { nodeWindow, isHTMLElement } from "../composables/realm.js";
10
10
  import { backgroundOf, resolveCssColor, svgToPng } from "../components/exportData.js";
11
11
  import { _ as _sfc_main$6 } from "./LpButton-BvVoF-PO.js";
12
12
  import { _ as _sfc_main$7 } from "./LpIcon-CxHFPwKt.js";
13
- import { _ as _sfc_main$3 } from "./LpInfraNode-BYiMFDnA.js";
13
+ import { _ as _sfc_main$3 } from "./LpInfraNode-tD5x5YOy.js";
14
14
  import { _ as _sfc_main$5 } from "./LpInput-DGMpxrcg.js";
15
15
  import { _ as _sfc_main$1 } from "./LpLaneNode-Bqcdm8Rf.js";
16
16
  import { _ as _sfc_main$2 } from "./LpServiceNode-Cln2ntdw.js";
@@ -8,6 +8,7 @@
8
8
  * component doesn't reach apps that only import the kit's token css.
9
9
  */
10
10
  import { type NodeProps } from "@vue-flow/core";
11
+ import { type StatusKind } from "./status";
11
12
  export interface InfraNodeData {
12
13
  name: string;
13
14
  /** Role key: selects the colour/glyph, and shown as-is unless `roleLabel`. */
@@ -18,7 +19,15 @@ export interface InfraNodeData {
18
19
  */
19
20
  roleLabel?: string;
20
21
  overlay?: string;
22
+ /** Up or down. Superseded by `status` when that is set. */
21
23
  online?: boolean;
24
+ /**
25
+ * The node's state in the kit's status words, for a fleet that knows more
26
+ * than up and down: a box that serves but takes no instruction, one merely
27
+ * unreachable, one starting. Drawn with LpStatus's light and tone; only
28
+ * `down` and `offline` take the dashed, faded frame of a lost machine.
29
+ */
30
+ status?: StatusKind;
22
31
  kind?: string;
23
32
  project?: string;
24
33
  /** Control-plane firewall rule count on this host (badge when > 0). */
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpInfraNode-BYiMFDnA.js";
1
+ import { _ as _sfc_main } from "../chunks/LpInfraNode-tD5x5YOy.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -14,7 +14,7 @@ declare const __VLS_export: import("vue").DefineComponent<{
14
14
  /** Always show the CSS still, never start WebGL: for a low-power mode or a screenshot. */
15
15
  still?: boolean;
16
16
  }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
17
- fallback: (reason: "compile" | "unavailable" | "slow" | "lost") => any;
17
+ fallback: (reason: "compile" | "unavailable" | "lost" | "slow") => any;
18
18
  }, string, import("vue").PublicProps, Readonly<{
19
19
  preset?: ShaderPreset;
20
20
  /** How strongly the theme colours show over the surface, 0–1. Lower for a backdrop under body text. */
@@ -28,7 +28,7 @@ declare const __VLS_export: import("vue").DefineComponent<{
28
28
  /** Always show the CSS still, never start WebGL: for a low-power mode or a screenshot. */
29
29
  still?: boolean;
30
30
  }> & Readonly<{
31
- onFallback?: ((reason: "compile" | "unavailable" | "slow" | "lost") => any) | undefined;
31
+ onFallback?: ((reason: "compile" | "unavailable" | "lost" | "slow") => any) | undefined;
32
32
  }>, {
33
33
  speed: number;
34
34
  paused: boolean;
@@ -34,7 +34,7 @@ import { toCsv, downloadFile, fileStem } from "./exportData.js";
34
34
  import { toHiddenSets, sameHidden, summarizeShown, parseTableState, mergeRestored, serializeTableState, fromHiddenSets } from "./tableFilter.js";
35
35
  import { resolveRowMenu } from "./tableRowMenu.js";
36
36
  import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
37
- import '../assets/LpTable-B3hNJQVl.css';const _hoisted_1 = {
37
+ import '../assets/LpTable-gpdZSVMX.css';const _hoisted_1 = {
38
38
  key: 0,
39
39
  class: "flex min-h-8 flex-wrap items-center gap-x-3 gap-y-2 pb-3 text-xs"
40
40
  };
@@ -1898,7 +1898,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
1898
1898
  };
1899
1899
  }
1900
1900
  });
1901
- const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-62285817"]]);
1901
+ const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-5ac91b9e"]]);
1902
1902
  export {
1903
1903
  LpTable as default
1904
1904
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpTopologyCanvas-CQ0l6A5h.js";
1
+ import { _ as _sfc_main } from "../chunks/LpTopologyCanvas-B1YFfMaq.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
package/dist/index.js CHANGED
@@ -54,7 +54,7 @@ import { _ as _31 } from "./chunks/LpFormField-jbM7K1Hg.js";
54
54
  import { _ as _32 } from "./chunks/LpIcon-CxHFPwKt.js";
55
55
  import { i, b, a, c } from "./chunks/icons-C8ccRCKw.js";
56
56
  import { default as default10 } from "./components/LpImageEditor.vue.js";
57
- import { _ as _33 } from "./chunks/LpInfraNode-BYiMFDnA.js";
57
+ import { _ as _33 } from "./chunks/LpInfraNode-tD5x5YOy.js";
58
58
  import { _ as _34 } from "./chunks/LpInput-DGMpxrcg.js";
59
59
  import { _ as _35 } from "./chunks/LpLightbox-a24V_0Wk.js";
60
60
  import { fileNameOf, isVideoItem } from "./components/lightbox.js";
@@ -131,7 +131,7 @@ import { _ as _87 } from "./chunks/LpHintLayer-nuMqfzvy.js";
131
131
  import { _ as _88 } from "./chunks/LpHints-ByAd3a76.js";
132
132
  import { HINT_SIGNALS, contextMenuTip, createHintSystem, dropzoneTip, hintKey, hintSummary, hotkeyTip, localHintStorage, noteDropzone, noteHotkeyTarget, paletteTip, provideHintSystem, runningHintSystem, useHintSystem } from "./composables/useHintSystem.js";
133
133
  import { _ as _89 } from "./chunks/LpTooltipProvider-CftjV7Xc.js";
134
- import { _ as _90 } from "./chunks/LpTopologyCanvas-CQ0l6A5h.js";
134
+ import { _ as _90 } from "./chunks/LpTopologyCanvas-B1YFfMaq.js";
135
135
  import { clusterGraph, diffTopology, findPath, layeredLayout, neighborhood, topologyToSvg } from "./components/topologyGraph.js";
136
136
  import { _ as _91 } from "./chunks/LpUptimeBar-Dt6SY_-m.js";
137
137
  import { addLeaf, addTab, countBlocks, countLeaves, deserializeLayout, makeLayout, moveLeaf, removeBlock, removeLeaf, reorderTab, resizeAt, serializeLayout, setActiveTab } from "./layout/tree.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.92.0",
3
+ "version": "0.93.0",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -12,6 +12,8 @@ import { Handle, Position, type NodeProps } from "@vue-flow/core"
12
12
  import { computed } from "vue"
13
13
  import LpBadge from "./LpBadge.vue"
14
14
  import LpStatus from "./LpStatus.vue"
15
+ import { TONE_COLOR } from "./meter"
16
+ import { STATUS_LOOK, type StatusKind } from "./status"
15
17
 
16
18
  export interface InfraNodeData {
17
19
  name: string
@@ -23,7 +25,15 @@ export interface InfraNodeData {
23
25
  */
24
26
  roleLabel?: string
25
27
  overlay?: string
28
+ /** Up or down. Superseded by `status` when that is set. */
26
29
  online?: boolean
30
+ /**
31
+ * The node's state in the kit's status words, for a fleet that knows more
32
+ * than up and down: a box that serves but takes no instruction, one merely
33
+ * unreachable, one starting. Drawn with LpStatus's light and tone; only
34
+ * `down` and `offline` take the dashed, faded frame of a lost machine.
35
+ */
36
+ status?: StatusKind
27
37
  kind?: string
28
38
  project?: string
29
39
  /** Control-plane firewall rule count on this host (badge when > 0). */
@@ -126,6 +136,18 @@ const icon = computed(() => role.value.icon)
126
136
  const accentLine = computed(
127
137
  () => `color-mix(in srgb, ${accent.value} 35%, var(--color-line))`,
128
138
  )
139
+
140
+ /** `status` when given, else the old up/down flag read as a status. */
141
+ const state = computed<StatusKind | undefined>(
142
+ () => props.data.status ?? (props.data.online === undefined ? undefined : props.data.online ? "online" : "down"),
143
+ )
144
+ /** A state other than healthy takes the frame and the role bar in its own tone. */
145
+ const troubled = computed(() => state.value !== undefined && state.value !== "online")
146
+ /** No contact at all: dashed and faded, the way a lost machine has always looked. */
147
+ const lost = computed(() => state.value === "down" || state.value === "offline")
148
+ const stateColor = computed(() =>
149
+ state.value ? `color-mix(in srgb, ${TONE_COLOR[STATUS_LOOK[state.value].tone]} 50%, transparent)` : undefined,
150
+ )
129
151
  </script>
130
152
 
131
153
  <template>
@@ -139,18 +161,16 @@ const accentLine = computed(
139
161
  signals on a node agree instead of arguing.
140
162
  -->
141
163
  <div
142
- class="w-[168px] overflow-hidden rounded-card border bg-surface-raised shadow-panel transition-[border-color,box-shadow]"
164
+ class="w-[168px] overflow-hidden rounded-card border bg-surface-raised shadow-panel transition-[border-color,box-shadow,opacity] duration-medium ease-[var(--ease-emphasized)]"
143
165
  :class="[
144
- selected
145
- ? 'border-[var(--accent)]'
146
- : data.online === false
147
- ? 'border-dashed border-danger/50'
148
- : 'border-[var(--accent-line)]',
166
+ selected ? 'border-[var(--accent)]' : troubled ? 'border-[var(--state-line)]' : 'border-[var(--accent-line)]',
167
+ !selected && lost ? 'border-dashed' : '',
149
168
  ]"
150
169
  :style="{
151
170
  '--accent': accent,
152
171
  '--accent-line': accentLine,
153
- opacity: data.dimmed ? 0.3 : data.online === false ? 0.75 : 1,
172
+ '--state-line': stateColor,
173
+ opacity: data.dimmed ? 0.3 : lost ? 0.75 : 1,
154
174
  ...(selected
155
175
  ? { boxShadow: '0 0 0 2px color-mix(in srgb, var(--accent) 40%, transparent), var(--shadow-panel)' }
156
176
  : {}),
@@ -167,23 +187,18 @@ const accentLine = computed(
167
187
  class="!size-2 !border-2 !border-[var(--accent)] !bg-surface"
168
188
  />
169
189
 
170
- <!-- The role bar reads as "this node is alive and is a proxy"; on an
171
- offline host it must not keep asserting the first half. -->
190
+ <!-- The role bar reads as "this node is alive and is a proxy"; on a host
191
+ in trouble it must not keep asserting the first half. -->
172
192
  <div
173
- class="h-[3px]"
174
- :class="data.online === false ? 'bg-danger/50' : 'bg-[var(--accent)]'"
193
+ class="h-[3px] transition-[background-color] duration-medium ease-[var(--ease-emphasized)]"
194
+ :class="troubled ? 'bg-[var(--state-line)]' : 'bg-[var(--accent)]'"
175
195
  />
176
196
  <div class="px-[11px] pb-2.5 pt-[9px]">
177
197
  <div class="flex items-center gap-[7px]">
178
198
  <span class="text-[13px] text-[var(--accent)]">{{ icon }}</span>
179
199
  <span class="flex-1 truncate text-[13px] font-semibold text-ink">{{ data.name }}</span>
180
- <!-- The kit's status light: a live host breathes, a lost one is a steady red. -->
181
- <LpStatus
182
- v-if="data.online !== undefined"
183
- :state="data.online ? 'online' : 'down'"
184
- :pulse="data.online"
185
- hide-label
186
- />
200
+ <!-- The kit's status light, with LpStatus's own tone and pulse for each state. -->
201
+ <LpStatus v-if="state" :state="state" hide-label />
187
202
  </div>
188
203
  <div class="mt-1.5 flex items-center justify-between">
189
204
  <span class="text-[10px] font-semibold uppercase tracking-wide text-[var(--accent)]">
@@ -2444,9 +2444,18 @@ usePaletteCommands(() => {
2444
2444
  margin-left: calc(-1 * var(--lp-line-sep));
2445
2445
  }
2446
2446
 
2447
+ /* The dot lives in the field's own left padding, so whatever the cell draws,
2448
+ a word or a block of two lines, starts after it and never in the clipped
2449
+ strip. */
2450
+ .lp-line-field {
2451
+ position: relative;
2452
+ padding-left: var(--lp-line-sep);
2453
+ }
2454
+
2447
2455
  .lp-line-field::before {
2448
2456
  content: "·" / "";
2449
- display: inline-block;
2457
+ position: absolute;
2458
+ left: 0;
2450
2459
  width: var(--lp-line-sep);
2451
2460
  text-align: center;
2452
2461
  }