@leavepulse/ui 0.90.0 → 0.92.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.
@@ -5,28 +5,28 @@
5
5
  * pane from its right edge as it grows. Animating the columns themselves made
6
6
  * every line of the list and the pane re-wrap on every frame.
7
7
  */
8
- .lp-cmd-dialog[data-v-1801065c] {
8
+ .lp-cmd-dialog[data-v-d51dc98f] {
9
9
  --lp-cmd-list: min(92vw, 40rem);
10
10
  --lp-cmd-pane: max(0px, min(26rem, 96vw - var(--lp-cmd-list)));
11
11
  width: var(--lp-cmd-list);
12
12
  }
13
- .lp-cmd-split[data-v-1801065c] {
13
+ .lp-cmd-split[data-v-d51dc98f] {
14
14
  transition:
15
15
  width 300ms var(--ease-emphasized, ease),
16
16
  min-height 300ms var(--ease-emphasized, ease);
17
17
  }
18
18
  @media (min-width: 768px) {
19
- .lp-cmd-split .lp-cmd-list[data-v-1801065c] {
19
+ .lp-cmd-split .lp-cmd-list[data-v-d51dc98f] {
20
20
  flex: 0 0 auto;
21
21
  /* the dialog's 1px border on each side */
22
22
  width: calc(var(--lp-cmd-list) - 2px);
23
23
  }
24
- .lp-cmd-dialog[data-pane="open"][data-v-1801065c] {
24
+ .lp-cmd-dialog[data-pane="open"][data-v-d51dc98f] {
25
25
  width: calc(var(--lp-cmd-list) + var(--lp-cmd-pane));
26
26
  min-height: min(76vh, 36rem);
27
27
  }
28
28
  }
29
- .lp-cmd-pane[data-v-1801065c] {
29
+ .lp-cmd-pane[data-v-d51dc98f] {
30
30
  width: var(--lp-cmd-pane);
31
31
  }
32
32
  /*
@@ -36,7 +36,7 @@
36
36
  * desktop window keeps the card.
37
37
  */
38
38
  @media (hover: none) and (pointer: coarse) and (max-width: 639px) {
39
- .lp-cmd-dialog[data-v-1801065c] {
39
+ .lp-cmd-dialog[data-v-d51dc98f] {
40
40
  inset: var(--lp-cmd-vy, 0) auto auto var(--lp-cmd-vx, 0);
41
41
  translate: none;
42
42
  width: var(--lp-cmd-vw, 100%);
@@ -48,10 +48,10 @@
48
48
  padding-bottom: env(safe-area-inset-bottom);
49
49
  }
50
50
  }
51
- .lp-cmd-dialog[data-editing] .lp-cmd-pane[data-v-1801065c] {
51
+ .lp-cmd-dialog[data-editing] .lp-cmd-pane[data-v-d51dc98f] {
52
52
  width: 100%;
53
53
  }
54
- .lp-cmd-dialog[data-editing][data-v-1801065c] {
54
+ .lp-cmd-dialog[data-editing][data-v-d51dc98f] {
55
55
  width: var(--lp-cmd-list);
56
56
  min-height: 0;
57
57
  }
@@ -59,43 +59,43 @@
59
59
  * The pane rides in from the right edge while the dialog opens up to it, and
60
60
  * leaves the same way, for as long as the dialog takes to close over it.
61
61
  */
62
- .lp-cmd-pane-enter-active[data-v-1801065c],
63
- .lp-cmd-pane-leave-active[data-v-1801065c] {
62
+ .lp-cmd-pane-enter-active[data-v-d51dc98f],
63
+ .lp-cmd-pane-leave-active[data-v-d51dc98f] {
64
64
  transition:
65
65
  transform 300ms var(--ease-emphasized, ease),
66
66
  opacity 300ms ease;
67
67
  }
68
- .lp-cmd-pane-enter-from[data-v-1801065c],
69
- .lp-cmd-pane-leave-to[data-v-1801065c] {
68
+ .lp-cmd-pane-enter-from[data-v-d51dc98f],
69
+ .lp-cmd-pane-leave-to[data-v-d51dc98f] {
70
70
  opacity: 0;
71
71
  transform: translateX(40%);
72
72
  }
73
- .lp-cmd-para[data-v-1801065c] {
73
+ .lp-cmd-para[data-v-d51dc98f] {
74
74
  --rise: 4px;
75
75
  animation: rise-in 260ms ease-out both;
76
76
  }
77
77
  @media (prefers-reduced-motion: reduce) {
78
- .lp-cmd-split[data-v-1801065c],
79
- .lp-cmd-pane-enter-active[data-v-1801065c],
80
- .lp-cmd-pane-leave-active[data-v-1801065c] {
78
+ .lp-cmd-split[data-v-d51dc98f],
79
+ .lp-cmd-pane-enter-active[data-v-d51dc98f],
80
+ .lp-cmd-pane-leave-active[data-v-d51dc98f] {
81
81
  transition: none;
82
82
  }
83
- .lp-cmd-para[data-v-1801065c] {
83
+ .lp-cmd-para[data-v-d51dc98f] {
84
84
  animation: none;
85
85
  }
86
86
  }
87
- .lp-cmd-excerpt[data-v-1801065c] {
87
+ .lp-cmd-excerpt[data-v-d51dc98f] {
88
88
  display: -webkit-box;
89
89
  -webkit-box-orient: vertical;
90
90
  overflow: hidden;
91
91
  }
92
92
  /* Each result eases in; honours reduced motion. */
93
- .lp-cmd-item[data-v-1801065c] {
93
+ .lp-cmd-item[data-v-d51dc98f] {
94
94
  --rise: 3px;
95
95
  animation: rise-in 140ms ease-out both;
96
96
  }
97
97
  @media (prefers-reduced-motion: reduce) {
98
- .lp-cmd-item[data-v-1801065c] {
98
+ .lp-cmd-item[data-v-d51dc98f] {
99
99
  animation: none;
100
100
  }
101
101
  }
@@ -1,4 +1,27 @@
1
1
 
2
+ /*
3
+ * A list line's facts, one dot between each two that are there. Every field
4
+ * carries its own dot before it; the row sits one dot to the left inside a
5
+ * box that clips, so the dot opening the line, or any wrapped line, falls
6
+ * outside and only the dots between facts are seen. A cell whose slot drew
7
+ * nothing (a v-if that failed, an empty value) is left out, dot and all.
8
+ */
9
+ .lp-line[data-v-62285817] {
10
+ --lp-line-sep: 1rem;
11
+ }
12
+ .lp-line-row[data-v-62285817] {
13
+ margin-left: calc(-1 * var(--lp-line-sep));
14
+ }
15
+ .lp-line-field[data-v-62285817]::before {
16
+ content: "·" / "";
17
+ display: inline-block;
18
+ width: var(--lp-line-sep);
19
+ text-align: center;
20
+ }
21
+ .lp-line-field[data-v-62285817]:has(> .lp-line-value:empty) {
22
+ display: none;
23
+ }
24
+
2
25
  /*
3
26
  * Rows arriving, leaving and moving as the filter changes.
4
27
  *
@@ -11,17 +34,17 @@
11
34
  * fade before closing the gap and the whole list lurches at the end instead of
12
35
  * gliding.
13
36
  */
14
- .lp-row-enter-active[data-v-f0750892],
15
- .lp-row-leave-active[data-v-f0750892] {
37
+ .lp-row-enter-active[data-v-62285817],
38
+ .lp-row-leave-active[data-v-62285817] {
16
39
  transition:
17
40
  opacity var(--duration-fast) var(--ease-emphasized),
18
41
  transform var(--duration-fast) var(--ease-emphasized);
19
42
  }
20
- .lp-row-move[data-v-f0750892] {
43
+ .lp-row-move[data-v-62285817] {
21
44
  transition: transform var(--duration-medium) var(--ease-emphasized);
22
45
  }
23
- .lp-row-enter-from[data-v-f0750892],
24
- .lp-row-leave-to[data-v-f0750892] {
46
+ .lp-row-enter-from[data-v-62285817],
47
+ .lp-row-leave-to[data-v-62285817] {
25
48
  opacity: 0;
26
49
  transform: translateX(-0.5rem);
27
50
  }
@@ -35,7 +58,7 @@
35
58
  * drops out of the table layout entirely. Nothing sits below this row to be
36
59
  * dragged around, so simply not animating its exit costs nothing.
37
60
  */
38
- .lp-row-empty.lp-row-leave-active[data-v-f0750892] {
61
+ .lp-row-empty.lp-row-leave-active[data-v-62285817] {
39
62
  display: none;
40
63
  }
41
64
 
@@ -54,7 +77,7 @@
54
77
  * once anything is selected. An earlier version collapsed the column to nothing
55
78
  * and floated the hover box over the first cell, where it sat on the text.
56
79
  */
57
- .lp-tick[data-v-f0750892] {
80
+ .lp-tick[data-v-62285817] {
58
81
  display: flex;
59
82
  align-items: center;
60
83
  max-width: 0;
@@ -66,17 +89,17 @@
66
89
  padding var(--duration-medium) var(--ease-emphasized),
67
90
  opacity var(--duration-fast) ease;
68
91
  }
69
- .lp-tick-open[data-v-f0750892] {
92
+ .lp-tick-open[data-v-62285817] {
70
93
  max-width: 3.25rem;
71
94
  padding-inline: 1rem;
72
95
  opacity: 1;
73
96
  }
74
- .lp-tick-hidden[data-v-f0750892],
75
- .lp-tick-cue[data-v-f0750892] {
97
+ .lp-tick-hidden[data-v-62285817],
98
+ .lp-tick-cue[data-v-62285817] {
76
99
  opacity: 0;
77
100
  }
78
- tr:hover .lp-tick-cue[data-v-f0750892],
79
- .lp-tick-cue[data-v-f0750892]:focus-within {
101
+ tr:hover .lp-tick-cue[data-v-62285817],
102
+ .lp-tick-cue[data-v-62285817]:focus-within {
80
103
  opacity: 1;
81
104
  }
82
105
 
@@ -84,15 +107,15 @@ tr:hover .lp-tick-cue[data-v-f0750892],
84
107
  turned down the rows change without animating rather than not changing. */
85
108
  /* Only where there is a hover to reveal it; a finger long-presses instead. */
86
109
  @media (hover: none) {
87
- tr:hover .lp-tick-cue[data-v-f0750892] {
110
+ tr:hover .lp-tick-cue[data-v-62285817] {
88
111
  opacity: 0;
89
112
  }
90
113
 
91
114
  /* Nor is there anything for the column's width to hold until then: on a
92
115
  touch screen it stays closed until a long press starts selecting, and
93
116
  eases open like the rest of the column. */
94
- .lp-tick-hidden[data-v-f0750892],
95
- .lp-tick-cue[data-v-f0750892] {
117
+ .lp-tick-hidden[data-v-62285817],
118
+ .lp-tick-cue[data-v-62285817] {
96
119
  max-width: 0;
97
120
  padding-inline: 0;
98
121
  }
@@ -102,7 +125,7 @@ tr:hover .lp-tick-cue[data-v-f0750892] {
102
125
  * A list line's tick opens from nothing, as the table's column does, so lines
103
126
  * do not carry an empty gap until selection starts.
104
127
  */
105
- .lp-tick-line[data-v-f0750892] {
128
+ .lp-tick-line[data-v-62285817] {
106
129
  max-width: 2rem;
107
130
  opacity: 1;
108
131
  }
@@ -112,28 +135,28 @@ tr:hover .lp-tick-cue[data-v-f0750892] {
112
135
  * open; a cue shows it only while
113
136
  * the card is pointed at or holds focus, the same rule as a row's.
114
137
  */
115
- .lp-tick-card[data-v-f0750892] {
138
+ .lp-tick-card[data-v-62285817] {
116
139
  max-width: none;
117
140
  padding-inline: 0;
118
141
  opacity: 1;
119
142
  }
120
- .lp-tick[data-cue][data-v-f0750892] {
143
+ .lp-tick[data-cue][data-v-62285817] {
121
144
  opacity: 0;
122
145
  }
123
- .lp-item:hover .lp-tick[data-cue][data-v-f0750892],
124
- .lp-tick[data-cue][data-v-f0750892]:focus-within {
146
+ .lp-item:hover .lp-tick[data-cue][data-v-62285817],
147
+ .lp-tick[data-cue][data-v-62285817]:focus-within {
125
148
  opacity: 1;
126
149
  }
127
150
  @media (hover: none) {
128
- .lp-item:hover .lp-tick[data-cue][data-v-f0750892] {
151
+ .lp-item:hover .lp-tick[data-cue][data-v-62285817] {
129
152
  opacity: 0;
130
153
  }
131
154
  }
132
155
  @media (prefers-reduced-motion: reduce) {
133
- .lp-row-enter-active[data-v-f0750892],
134
- .lp-row-leave-active[data-v-f0750892],
135
- .lp-row-move[data-v-f0750892],
136
- .lp-tick[data-v-f0750892] {
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] {
137
160
  transition: none;
138
161
  }
139
162
  }
@@ -53,11 +53,19 @@ export interface Command {
53
53
  group?: string;
54
54
  disabled?: boolean;
55
55
  /**
56
- * Hidden from the default (empty-query) list — only surfaces once the user
57
- * types something it matches. For rarely-used or destructive actions (prune,
58
- * decommission, …) that shouldn't clutter the palette but stay reachable.
56
+ * Hidden from the default (empty-query) list and surfaced only when the query
57
+ * names it: every typed word starts a word of its label or keywords. A word
58
+ * inside another word, the description or a near miss does not reveal it.
59
+ * For rarely-used or destructive actions (prune, decommission, …) that
60
+ * shouldn't clutter the palette but stay reachable.
59
61
  */
60
62
  hidden?: boolean;
63
+ /**
64
+ * For a hidden command: the words that reveal it, instead of its label and
65
+ * keywords. A variant whose label repeats its parent's word ("Vacation: sick
66
+ * leave…") then stays out of a search for the parent and shows up for "sick".
67
+ */
68
+ revealOn?: string[];
61
69
  /**
62
70
  * Picking this opens a mode instead of closing: a nested list, either given
63
71
  * outright or fetched from the query typed inside it. This is what keeps a
@@ -26,7 +26,7 @@ import { parsePaletteQuery, parseCommandQuery, searchCommandItems, applyQueryFil
26
26
  import { PALETTE_MODES, modeChipLabel, settleFetched, fallbackRows, readRecent, resolveRecentItems, recentVisible, commandHasPreview, matchModePrefix, isSafeHref, absoluteHref, commandKey, recentKey, pushRecent, moreRowId, toRecentItem } from "./commandPalette.js";
27
27
  import { useView, usePortalTarget, useOutsideGuard, viewWindow } from "../composables/useView.js";
28
28
  import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
29
- import '../assets/LpCommandPalette-CO_5TFOQ.css';const _hoisted_1 = { class: "relative flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
29
+ import '../assets/LpCommandPalette-D9ZuNQmc.css';const _hoisted_1 = { class: "relative flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
30
30
  const _hoisted_2 = ["title", "onClick"];
31
31
  const _hoisted_3 = ["readonly", "aria-label", "aria-expanded", "aria-controls", "aria-activedescendant", "placeholder", "onKeydown"];
32
32
  const _hoisted_4 = { class: "shrink-0 rounded-small border border-line px-1.5 py-0.5 text-[10px] text-muted [@media(hover:none)_and_(pointer:coarse)]:hidden" };
@@ -1400,7 +1400,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
1400
1400
  };
1401
1401
  }
1402
1402
  });
1403
- const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-1801065c"]]);
1403
+ const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-d51dc98f"]]);
1404
1404
  export {
1405
1405
  LpCommandPalette as default
1406
1406
  };
@@ -2,6 +2,8 @@ import { type ColumnPin, type ColumnPreset, type TableLayout } from "./tableFeat
2
2
  import { type CsvOptions } from "./exportData";
3
3
  import { type TableFilters } from "./tableFilter";
4
4
  import { type TableRowMenuResult } from "./tableRowMenu";
5
+ /** How a column reads in a list line: alone, after its label, or not at all. */
6
+ export type TableListMode = boolean | "label";
5
7
  export interface TableColumn<Row> {
6
8
  key: string;
7
9
  label: string;
@@ -58,6 +60,19 @@ export interface TableColumn<Row> {
58
60
  * user's own show/hide applies as usual.
59
61
  */
60
62
  hideBelow?: number;
63
+ /**
64
+ * How the column reads in a list line (the `list` layout, and a table drawn
65
+ * as a list below `layoutBelow`), where there are no headers to name it.
66
+ * `true` (default) puts the value alone in the line; `"label"` prefixes its
67
+ * label, for a figure that means nothing on its own ("Cores 28"); `false`
68
+ * leaves it out: an actions column (the row menu holds the same actions),
69
+ * or a fact the line already says. A function decides per row, for a fact
70
+ * worth a place only when it holds something ("needs attention", a stack
71
+ * that is set): a cell slot cannot say "nothing" itself, since an empty
72
+ * slot falls back to the raw value. Cards always show labels and are not
73
+ * affected.
74
+ */
75
+ list?: TableListMode | ((row: Row) => TableListMode);
61
76
  /** Stick to this edge while the table scrolls sideways. Pinned columns move to that edge. */
62
77
  pin?: ColumnPin;
63
78
  /** Bounds in px for `resizableColumns`. */
@@ -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-DAVNb-x2.css';const _hoisted_1 = {
37
+ import '../assets/LpTable-B3hNJQVl.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
  };
@@ -132,13 +132,15 @@ const _hoisted_44 = {
132
132
  };
133
133
  const _hoisted_45 = {
134
134
  key: 1,
135
- class: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted"
135
+ class: "lp-line min-w-0 overflow-hidden text-xs text-muted"
136
136
  };
137
- const _hoisted_46 = {
137
+ const _hoisted_46 = { class: "lp-line-row flex flex-wrap items-center gap-y-0.5" };
138
+ const _hoisted_47 = ["title"];
139
+ const _hoisted_48 = {
138
140
  key: 0,
139
- "aria-hidden": "true"
141
+ class: "lp-line-label"
140
142
  };
141
- const _hoisted_47 = ["title"];
143
+ const _hoisted_49 = { class: "lp-line-value" };
142
144
  const __default__ = { inheritAttrs: false };
143
145
  const _sfc_main = /* @__PURE__ */ defineComponent({
144
146
  ...__default__,
@@ -936,6 +938,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
936
938
  );
937
939
  const titleColumn = computed(() => cardColumns.value[0]);
938
940
  const fieldColumns = computed(() => cardColumns.value.slice(1).filter((col) => col.label));
941
+ const lineColumns = computed(() => fieldColumns.value.filter((col) => col.list !== false));
942
+ function lineMode(col, row) {
943
+ return (typeof col.list === "function" ? col.list(row) : col.list) ?? true;
944
+ }
939
945
  watch(chosenLayout, (layout) => {
940
946
  if (storageKey.value && restored.value) store.set(`${storageKey.value}:layout`, layout);
941
947
  });
@@ -1840,26 +1846,31 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
1840
1846
  createTextVNode(toDisplayString(entry.row[titleColumn.value.key]), 1)
1841
1847
  ], true)
1842
1848
  ])) : createCommentVNode("", true),
1843
- fieldColumns.value.length ? (openBlock(), createElementBlock("div", _hoisted_45, [
1844
- (openBlock(true), createElementBlock(Fragment, null, renderList(fieldColumns.value, (col, ci) => {
1845
- return openBlock(), createElementBlock(Fragment, {
1846
- key: col.key
1847
- }, [
1848
- ci > 0 ? (openBlock(), createElementBlock("span", _hoisted_46, "·")) : createCommentVNode("", true),
1849
- createElementVNode("span", {
1850
- class: "min-w-0 truncate",
1851
- title: col.label
1849
+ lineColumns.value.length ? (openBlock(), createElementBlock("div", _hoisted_45, [
1850
+ createElementVNode("div", _hoisted_46, [
1851
+ (openBlock(true), createElementBlock(Fragment, null, renderList(lineColumns.value, (col) => {
1852
+ return openBlock(), createElementBlock(Fragment, {
1853
+ key: col.key
1852
1854
  }, [
1853
- renderSlot(_ctx.$slots, `cell-${col.key}`, {
1854
- row: entry.row,
1855
- value: entry.row[col.key],
1856
- openMenu: openRowMenu
1857
- }, () => [
1858
- createTextVNode(toDisplayString(entry.row[col.key]), 1)
1859
- ], true)
1860
- ], 8, _hoisted_47)
1861
- ], 64);
1862
- }), 128))
1855
+ lineMode(col, entry.row) !== false ? (openBlock(), createElementBlock("span", {
1856
+ key: 0,
1857
+ class: "lp-line-field min-w-0 truncate",
1858
+ title: col.label
1859
+ }, [
1860
+ lineMode(col, entry.row) === "label" ? (openBlock(), createElementBlock("span", _hoisted_48, toDisplayString(`${col.label} `), 1)) : createCommentVNode("", true),
1861
+ createElementVNode("span", _hoisted_49, [
1862
+ renderSlot(_ctx.$slots, `cell-${col.key}`, {
1863
+ row: entry.row,
1864
+ value: entry.row[col.key],
1865
+ openMenu: openRowMenu
1866
+ }, () => [
1867
+ createTextVNode(toDisplayString(entry.row[col.key]), 1)
1868
+ ], true)
1869
+ ])
1870
+ ], 8, _hoisted_47)) : createCommentVNode("", true)
1871
+ ], 64);
1872
+ }), 128))
1873
+ ])
1863
1874
  ])) : createCommentVNode("", true)
1864
1875
  ], true)
1865
1876
  ]))
@@ -1887,7 +1898,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
1887
1898
  };
1888
1899
  }
1889
1900
  });
1890
- const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-f0750892"]]);
1901
+ const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-62285817"]]);
1891
1902
  export {
1892
1903
  LpTable as default
1893
1904
  };
@@ -3,6 +3,8 @@ export interface SearchableCommand {
3
3
  description?: string;
4
4
  keywords?: string[];
5
5
  group?: string;
6
+ hidden?: boolean;
7
+ revealOn?: string[];
6
8
  }
7
9
  export interface CommandSearchTerms {
8
10
  include: string[];
@@ -26,9 +26,17 @@ function nearWord(term, label) {
26
26
  if (term.length < 5) return false;
27
27
  return label.split(/[^\p{L}\p{N}]+/u).some((word) => withinOne(term, word) || word.length > term.length && withinOne(term, word.slice(0, term.length)));
28
28
  }
29
+ function wordStarts(text, term) {
30
+ return text.split(/[^\p{L}\p{N}]+/u).some((word) => word.startsWith(term));
31
+ }
32
+ function named(command, terms) {
33
+ const words = command.revealOn ? command.revealOn.map((word) => word.toLowerCase()) : [command.label.toLowerCase(), ...(command.keywords ?? []).map((keyword) => keyword.toLowerCase())];
34
+ return terms.include.length > 0 && terms.include.every((term) => words.some((text) => wordStarts(text, term)));
35
+ }
29
36
  function score(command, terms) {
37
+ if (command.hidden && !named(command, terms)) return 0;
30
38
  const label = command.label.toLowerCase();
31
- const keywords = (command.keywords ?? []).map((keyword) => keyword.toLowerCase());
39
+ const keywords = [...command.keywords ?? [], ...command.revealOn ?? []].map((keyword) => keyword.toLowerCase());
32
40
  const description = command.description?.toLowerCase() ?? "";
33
41
  const group = command.group?.toLowerCase() ?? "";
34
42
  if (terms.exclude.some((term) => matches(command, term)) || terms.groups.some((term) => !group.includes(term))) return 0;
package/dist/index.d.ts CHANGED
@@ -191,7 +191,7 @@ export { default as LpStepper } from "./components/LpStepper.vue";
191
191
  export type { Step } from "./components/LpStepper.vue";
192
192
  export { default as LpSwitch } from "./components/LpSwitch.vue";
193
193
  export { default as LpTable } from "./components/LpTable.vue";
194
- export type { SortState, TableColumn, TablePage, TableQuery } from "./components/LpTable.vue";
194
+ export type { SortState, TableColumn, TableListMode, TablePage, TableQuery } from "./components/LpTable.vue";
195
195
  export type { TableLabels } from "./components/LpTable.vue";
196
196
  export type { TableCellEdit, TableExportOptions, TableSelectionToken } from "./components/LpTable.vue";
197
197
  export type { ColumnPin, ColumnPreset, TableLayout } from "./components/tableFeatures";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.90.0",
3
+ "version": "0.92.0",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -143,11 +143,19 @@ export interface Command {
143
143
  group?: string
144
144
  disabled?: boolean
145
145
  /**
146
- * Hidden from the default (empty-query) list — only surfaces once the user
147
- * types something it matches. For rarely-used or destructive actions (prune,
148
- * decommission, …) that shouldn't clutter the palette but stay reachable.
146
+ * Hidden from the default (empty-query) list and surfaced only when the query
147
+ * names it: every typed word starts a word of its label or keywords. A word
148
+ * inside another word, the description or a near miss does not reveal it.
149
+ * For rarely-used or destructive actions (prune, decommission, …) that
150
+ * shouldn't clutter the palette but stay reachable.
149
151
  */
150
152
  hidden?: boolean
153
+ /**
154
+ * For a hidden command: the words that reveal it, instead of its label and
155
+ * keywords. A variant whose label repeats its parent's word ("Vacation: sick
156
+ * leave…") then stays out of a search for the parent and shows up for "sick".
157
+ */
158
+ revealOn?: string[]
151
159
  /**
152
160
  * Picking this opens a mode instead of closing: a nested list, either given
153
161
  * outright or fetched from the query typed inside it. This is what keeps a
@@ -902,8 +910,8 @@ const local = computed<{ items: Command[]; terms: string[] }>(() => {
902
910
  const filtered = query.value.trim()
903
911
  ? source.value.filter((c) => !c.fallback)
904
912
  : source.value.filter((c) => !c.hidden && !c.fallback)
905
- // Empty query: show everything EXCEPT hidden commands (they only surface once
906
- // typed). With a query, hidden commands rank like any other.
913
+ // Empty query: show everything EXCEPT hidden commands. With a query, a hidden
914
+ // command ranks only when the query names it (see commandSearch).
907
915
  if (!terms.include.length && !terms.exclude.length && !terms.groups.length) return { items: filtered, terms: [] }
908
916
  return searchCommandItems(filtered, terms)
909
917
  })
@@ -62,6 +62,9 @@ import {
62
62
  } from "./tableFilter"
63
63
  import { resolveRowMenu, type TableRowMenuResult } from "./tableRowMenu"
64
64
 
65
+ /** How a column reads in a list line: alone, after its label, or not at all. */
66
+ export type TableListMode = boolean | "label"
67
+
65
68
  export interface TableColumn<Row> {
66
69
  key: string
67
70
  label: string
@@ -118,6 +121,19 @@ export interface TableColumn<Row> {
118
121
  * user's own show/hide applies as usual.
119
122
  */
120
123
  hideBelow?: number
124
+ /**
125
+ * How the column reads in a list line (the `list` layout, and a table drawn
126
+ * as a list below `layoutBelow`), where there are no headers to name it.
127
+ * `true` (default) puts the value alone in the line; `"label"` prefixes its
128
+ * label, for a figure that means nothing on its own ("Cores 28"); `false`
129
+ * leaves it out: an actions column (the row menu holds the same actions),
130
+ * or a fact the line already says. A function decides per row, for a fact
131
+ * worth a place only when it holds something ("needs attention", a stack
132
+ * that is set): a cell slot cannot say "nothing" itself, since an empty
133
+ * slot falls back to the raw value. Cards always show labels and are not
134
+ * affected.
135
+ */
136
+ list?: TableListMode | ((row: Row) => TableListMode)
121
137
  /** Stick to this edge while the table scrolls sideways. Pinned columns move to that edge. */
122
138
  pin?: ColumnPin
123
139
  /** Bounds in px for `resizableColumns`. */
@@ -1545,6 +1561,10 @@ const cardColumns = computed(() =>
1545
1561
  )
1546
1562
  const titleColumn = computed(() => cardColumns.value[0])
1547
1563
  const fieldColumns = computed(() => cardColumns.value.slice(1).filter((col) => col.label))
1564
+ const lineColumns = computed(() => fieldColumns.value.filter((col) => col.list !== false))
1565
+ function lineMode(col: TableColumn<T>, row: T): TableListMode {
1566
+ return (typeof col.list === "function" ? col.list(row) : col.list) ?? true
1567
+ }
1548
1568
 
1549
1569
  watch(chosenLayout, (layout) => {
1550
1570
  if (storageKey.value && restored.value) store.set(`${storageKey.value}:layout`, layout)
@@ -2377,13 +2397,19 @@ usePaletteCommands(() => {
2377
2397
  <div v-if="titleColumn" class="min-w-0 truncate font-medium text-ink">
2378
2398
  <slot :name="`cell-${titleColumn.key}`" :row="entry.row" :value="entry.row[titleColumn.key]" :open-menu="openRowMenu">{{ entry.row[titleColumn.key] }}</slot>
2379
2399
  </div>
2380
- <div v-if="fieldColumns.length" class="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted">
2381
- <template v-for="(col, ci) in fieldColumns" :key="col.key">
2382
- <span v-if="ci > 0" aria-hidden="true">·</span>
2383
- <span class="min-w-0 truncate" :title="col.label">
2384
- <slot :name="`cell-${col.key}`" :row="entry.row" :value="entry.row[col.key]" :open-menu="openRowMenu">{{ entry.row[col.key] }}</slot>
2400
+ <!-- Separators belong between the facts that are there: a cell
2401
+ that drew nothing takes no dot, and the dot that would open
2402
+ a wrapped line is clipped off (the inner row starts one
2403
+ separator to the left of the clip box). -->
2404
+ <div v-if="lineColumns.length" class="lp-line min-w-0 overflow-hidden text-xs text-muted">
2405
+ <div class="lp-line-row flex flex-wrap items-center gap-y-0.5">
2406
+ <template v-for="col in lineColumns" :key="col.key">
2407
+ <span v-if="lineMode(col, entry.row) !== false" class="lp-line-field min-w-0 truncate" :title="col.label">
2408
+ <span v-if="lineMode(col, entry.row) === 'label'" class="lp-line-label">{{ `${col.label} ` }}</span>
2409
+ <span class="lp-line-value"><slot :name="`cell-${col.key}`" :row="entry.row" :value="entry.row[col.key]" :open-menu="openRowMenu">{{ entry.row[col.key] }}</slot></span>
2385
2410
  </span>
2386
- </template>
2411
+ </template>
2412
+ </div>
2387
2413
  </div>
2388
2414
  </slot>
2389
2415
  </div>
@@ -2403,6 +2429,32 @@ usePaletteCommands(() => {
2403
2429
  </template>
2404
2430
 
2405
2431
  <style scoped>
2432
+ /*
2433
+ * A list line's facts, one dot between each two that are there. Every field
2434
+ * carries its own dot before it; the row sits one dot to the left inside a
2435
+ * box that clips, so the dot opening the line, or any wrapped line, falls
2436
+ * outside and only the dots between facts are seen. A cell whose slot drew
2437
+ * nothing (a v-if that failed, an empty value) is left out, dot and all.
2438
+ */
2439
+ .lp-line {
2440
+ --lp-line-sep: 1rem;
2441
+ }
2442
+
2443
+ .lp-line-row {
2444
+ margin-left: calc(-1 * var(--lp-line-sep));
2445
+ }
2446
+
2447
+ .lp-line-field::before {
2448
+ content: "·" / "";
2449
+ display: inline-block;
2450
+ width: var(--lp-line-sep);
2451
+ text-align: center;
2452
+ }
2453
+
2454
+ .lp-line-field:has(> .lp-line-value:empty) {
2455
+ display: none;
2456
+ }
2457
+
2406
2458
  /*
2407
2459
  * Rows arriving, leaving and moving as the filter changes.
2408
2460
  *
@@ -3,6 +3,8 @@ export interface SearchableCommand {
3
3
  description?: string
4
4
  keywords?: string[]
5
5
  group?: string
6
+ hidden?: boolean
7
+ revealOn?: string[]
6
8
  }
7
9
 
8
10
  export interface CommandSearchTerms {
@@ -50,9 +52,29 @@ function nearWord(term: string, label: string): boolean {
50
52
  .some((word) => withinOne(term, word) || (word.length > term.length && withinOne(term, word.slice(0, term.length))))
51
53
  }
52
54
 
55
+ function wordStarts(text: string, term: string): boolean {
56
+ return text.split(/[^\p{L}\p{N}]+/u).some((word) => word.startsWith(term))
57
+ }
58
+
59
+ /*
60
+ * A hidden command is there for whoever names it, not for whoever types a word
61
+ * it happens to contain. Every term has to start a word of its label or of a
62
+ * keyword (or of `revealOn`, which then replaces both): "пт" no longer digs a
63
+ * settings key out of "Транскрипты", and "vacation" no longer spills every
64
+ * hidden variant of it. Descriptions, mid-word hits and near misses do not
65
+ * reveal; the visible list keeps all of those.
66
+ */
67
+ function named(command: SearchableCommand, terms: CommandSearchTerms): boolean {
68
+ const words = command.revealOn
69
+ ? command.revealOn.map((word) => word.toLowerCase())
70
+ : [command.label.toLowerCase(), ...(command.keywords ?? []).map((keyword) => keyword.toLowerCase())]
71
+ return terms.include.length > 0 && terms.include.every((term) => words.some((text) => wordStarts(text, term)))
72
+ }
73
+
53
74
  function score(command: SearchableCommand, terms: CommandSearchTerms): number {
75
+ if (command.hidden && !named(command, terms)) return 0
54
76
  const label = command.label.toLowerCase()
55
- const keywords = (command.keywords ?? []).map((keyword) => keyword.toLowerCase())
77
+ const keywords = [...(command.keywords ?? []), ...(command.revealOn ?? [])].map((keyword) => keyword.toLowerCase())
56
78
  const description = command.description?.toLowerCase() ?? ""
57
79
  const group = command.group?.toLowerCase() ?? ""
58
80
  if (terms.exclude.some((term) => matches(command, term)) || terms.groups.some((term) => !group.includes(term))) return 0
package/src/index.ts CHANGED
@@ -267,7 +267,7 @@ export { default as LpStepper } from "./components/LpStepper.vue"
267
267
  export type { Step } from "./components/LpStepper.vue"
268
268
  export { default as LpSwitch } from "./components/LpSwitch.vue"
269
269
  export { default as LpTable } from "./components/LpTable.vue"
270
- export type { SortState, TableColumn, TablePage, TableQuery } from "./components/LpTable.vue"
270
+ export type { SortState, TableColumn, TableListMode, TablePage, TableQuery } from "./components/LpTable.vue"
271
271
  export type { TableLabels } from "./components/LpTable.vue"
272
272
  export type { TableCellEdit, TableExportOptions, TableSelectionToken } from "./components/LpTable.vue"
273
273
  export type { ColumnPin, ColumnPreset, TableLayout } from "./components/tableFeatures"