@leavepulse/ui 0.91.0 → 0.92.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,36 @@
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-5ac91b9e] {
10
+ --lp-line-sep: 1rem;
11
+ }
12
+ .lp-line-row[data-v-5ac91b9e] {
13
+ margin-left: calc(-1 * var(--lp-line-sep));
14
+ }
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 {
24
+ content: "·" / "";
25
+ position: absolute;
26
+ left: 0;
27
+ width: var(--lp-line-sep);
28
+ text-align: center;
29
+ }
30
+ .lp-line-field[data-v-5ac91b9e]:has(> .lp-line-value:empty) {
31
+ display: none;
32
+ }
33
+
2
34
  /*
3
35
  * Rows arriving, leaving and moving as the filter changes.
4
36
  *
@@ -11,17 +43,17 @@
11
43
  * fade before closing the gap and the whole list lurches at the end instead of
12
44
  * gliding.
13
45
  */
14
- .lp-row-enter-active[data-v-f0750892],
15
- .lp-row-leave-active[data-v-f0750892] {
46
+ .lp-row-enter-active[data-v-5ac91b9e],
47
+ .lp-row-leave-active[data-v-5ac91b9e] {
16
48
  transition:
17
49
  opacity var(--duration-fast) var(--ease-emphasized),
18
50
  transform var(--duration-fast) var(--ease-emphasized);
19
51
  }
20
- .lp-row-move[data-v-f0750892] {
52
+ .lp-row-move[data-v-5ac91b9e] {
21
53
  transition: transform var(--duration-medium) var(--ease-emphasized);
22
54
  }
23
- .lp-row-enter-from[data-v-f0750892],
24
- .lp-row-leave-to[data-v-f0750892] {
55
+ .lp-row-enter-from[data-v-5ac91b9e],
56
+ .lp-row-leave-to[data-v-5ac91b9e] {
25
57
  opacity: 0;
26
58
  transform: translateX(-0.5rem);
27
59
  }
@@ -35,7 +67,7 @@
35
67
  * drops out of the table layout entirely. Nothing sits below this row to be
36
68
  * dragged around, so simply not animating its exit costs nothing.
37
69
  */
38
- .lp-row-empty.lp-row-leave-active[data-v-f0750892] {
70
+ .lp-row-empty.lp-row-leave-active[data-v-5ac91b9e] {
39
71
  display: none;
40
72
  }
41
73
 
@@ -54,7 +86,7 @@
54
86
  * once anything is selected. An earlier version collapsed the column to nothing
55
87
  * and floated the hover box over the first cell, where it sat on the text.
56
88
  */
57
- .lp-tick[data-v-f0750892] {
89
+ .lp-tick[data-v-5ac91b9e] {
58
90
  display: flex;
59
91
  align-items: center;
60
92
  max-width: 0;
@@ -66,17 +98,17 @@
66
98
  padding var(--duration-medium) var(--ease-emphasized),
67
99
  opacity var(--duration-fast) ease;
68
100
  }
69
- .lp-tick-open[data-v-f0750892] {
101
+ .lp-tick-open[data-v-5ac91b9e] {
70
102
  max-width: 3.25rem;
71
103
  padding-inline: 1rem;
72
104
  opacity: 1;
73
105
  }
74
- .lp-tick-hidden[data-v-f0750892],
75
- .lp-tick-cue[data-v-f0750892] {
106
+ .lp-tick-hidden[data-v-5ac91b9e],
107
+ .lp-tick-cue[data-v-5ac91b9e] {
76
108
  opacity: 0;
77
109
  }
78
- tr:hover .lp-tick-cue[data-v-f0750892],
79
- .lp-tick-cue[data-v-f0750892]:focus-within {
110
+ tr:hover .lp-tick-cue[data-v-5ac91b9e],
111
+ .lp-tick-cue[data-v-5ac91b9e]:focus-within {
80
112
  opacity: 1;
81
113
  }
82
114
 
@@ -84,15 +116,15 @@ tr:hover .lp-tick-cue[data-v-f0750892],
84
116
  turned down the rows change without animating rather than not changing. */
85
117
  /* Only where there is a hover to reveal it; a finger long-presses instead. */
86
118
  @media (hover: none) {
87
- tr:hover .lp-tick-cue[data-v-f0750892] {
119
+ tr:hover .lp-tick-cue[data-v-5ac91b9e] {
88
120
  opacity: 0;
89
121
  }
90
122
 
91
123
  /* Nor is there anything for the column's width to hold until then: on a
92
124
  touch screen it stays closed until a long press starts selecting, and
93
125
  eases open like the rest of the column. */
94
- .lp-tick-hidden[data-v-f0750892],
95
- .lp-tick-cue[data-v-f0750892] {
126
+ .lp-tick-hidden[data-v-5ac91b9e],
127
+ .lp-tick-cue[data-v-5ac91b9e] {
96
128
  max-width: 0;
97
129
  padding-inline: 0;
98
130
  }
@@ -102,7 +134,7 @@ tr:hover .lp-tick-cue[data-v-f0750892] {
102
134
  * A list line's tick opens from nothing, as the table's column does, so lines
103
135
  * do not carry an empty gap until selection starts.
104
136
  */
105
- .lp-tick-line[data-v-f0750892] {
137
+ .lp-tick-line[data-v-5ac91b9e] {
106
138
  max-width: 2rem;
107
139
  opacity: 1;
108
140
  }
@@ -112,28 +144,28 @@ tr:hover .lp-tick-cue[data-v-f0750892] {
112
144
  * open; a cue shows it only while
113
145
  * the card is pointed at or holds focus, the same rule as a row's.
114
146
  */
115
- .lp-tick-card[data-v-f0750892] {
147
+ .lp-tick-card[data-v-5ac91b9e] {
116
148
  max-width: none;
117
149
  padding-inline: 0;
118
150
  opacity: 1;
119
151
  }
120
- .lp-tick[data-cue][data-v-f0750892] {
152
+ .lp-tick[data-cue][data-v-5ac91b9e] {
121
153
  opacity: 0;
122
154
  }
123
- .lp-item:hover .lp-tick[data-cue][data-v-f0750892],
124
- .lp-tick[data-cue][data-v-f0750892]:focus-within {
155
+ .lp-item:hover .lp-tick[data-cue][data-v-5ac91b9e],
156
+ .lp-tick[data-cue][data-v-5ac91b9e]:focus-within {
125
157
  opacity: 1;
126
158
  }
127
159
  @media (hover: none) {
128
- .lp-item:hover .lp-tick[data-cue][data-v-f0750892] {
160
+ .lp-item:hover .lp-tick[data-cue][data-v-5ac91b9e] {
129
161
  opacity: 0;
130
162
  }
131
163
  }
132
164
  @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] {
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] {
137
169
  transition: none;
138
170
  }
139
171
  }
@@ -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-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
  };
@@ -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-5ac91b9e"]]);
1891
1902
  export {
1892
1903
  LpTable as default
1893
1904
  };
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.91.0",
3
+ "version": "0.92.1",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -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,41 @@ 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
+ /* 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
+
2455
+ .lp-line-field::before {
2456
+ content: "·" / "";
2457
+ position: absolute;
2458
+ left: 0;
2459
+ width: var(--lp-line-sep);
2460
+ text-align: center;
2461
+ }
2462
+
2463
+ .lp-line-field:has(> .lp-line-value:empty) {
2464
+ display: none;
2465
+ }
2466
+
2406
2467
  /*
2407
2468
  * Rows arriving, leaving and moving as the filter changes.
2408
2469
  *
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"