@leavepulse/ui 0.29.3 → 0.29.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.
- package/dist/assets/LpChart-CzgCGaAA.css +8 -0
- package/dist/assets/{LpTable-B9Iko3XD.css → LpTable-Bz3uJNa3.css} +8 -8
- package/dist/chunks/{LpAppShell-CGX0DlIW.js → LpAppShell-BJk3b-u4.js} +1 -1
- package/dist/chunks/{LpAvatar-CzCE5cXg.js → LpAvatar-D3l4nabg.js} +1 -1
- package/dist/chunks/{LpCard-DSefTGEH.js → LpCard-D0M2ZCnB.js} +1 -1
- package/dist/chunks/{LpContextMenu-D0mzXqA7.js → LpContextMenu-B6WBLleS.js} +55 -8
- package/dist/chunks/{LpFileTree-BaV7f0O7.js → LpFileTree-DfFqY__1.js} +1 -1
- package/dist/chunks/{LpFileTreeNode-BzpBa7R7.js → LpFileTreeNode-COchUMNn.js} +1 -1
- package/dist/chunks/{LpLogViewer-CKmEQYnm.js → LpLogViewer-CVaGfTF_.js} +1 -1
- package/dist/chunks/{LpNotificationBell-Dvu-b6Vn.js → LpNotificationBell-CTsIcPqX.js} +1 -1
- package/dist/chunks/{LpPie-BkjWlfSw.js → LpPie-BL1vTDSM.js} +1 -1
- package/dist/chunks/{LpSidebar-CWehDCxN.js → LpSidebar-C_Mr-JPI.js} +1 -1
- package/dist/chunks/{LpSidebarNav-5RGNr2D3.js → LpSidebarNav-D6v12-36.js} +1 -1
- package/dist/chunks/{LpThemeSwitcher-Dj4VcTu7.js → LpThemeSwitcher-r1te9nB2.js} +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.js +814 -2
- package/dist/components/LpContextMenu.vue.d.ts +7 -0
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpTable.vue.d.ts +19 -0
- package/dist/components/LpTable.vue.js +233 -143
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/index.js +124 -124
- package/package.json +1 -1
- package/src/components/LpChart.vue +337 -63
- package/src/components/LpContextMenu.vue +49 -0
- package/src/components/LpTable.vue +166 -23
- package/dist/chunks/LpChart-UiU4RhXP.js +0 -672
|
@@ -13,6 +13,21 @@ export interface TableColumn<Row> {
|
|
|
13
13
|
width?: string
|
|
14
14
|
/** Allow clicking the header to sort by this column. */
|
|
15
15
|
sortable?: boolean
|
|
16
|
+
/**
|
|
17
|
+
* Offer "show only these values" in the header's menu, built from the values
|
|
18
|
+
* actually present in the rows. For a column that holds a small vocabulary —
|
|
19
|
+
* a type, a state, a site — where the question is "just the mains leads,
|
|
20
|
+
* please". Pointless on a column of unique values, so it is opt-in.
|
|
21
|
+
*/
|
|
22
|
+
filterable?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* How a raw cell value reads in that filter list. `power_ac` is the stored
|
|
25
|
+
* vocabulary; the menu should say what the column itself says.
|
|
26
|
+
*/
|
|
27
|
+
valueLabel?: (value: unknown) => string
|
|
28
|
+
/** Keep this column out of the show/hide list — an actions column has no
|
|
29
|
+
* name to offer and nothing to gain from being hidden. */
|
|
30
|
+
alwaysVisible?: boolean
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
export interface SortState {
|
|
@@ -47,6 +62,10 @@ const props = withDefaults(
|
|
|
47
62
|
sortAscLabel?: string
|
|
48
63
|
sortDescLabel?: string
|
|
49
64
|
sortClearLabel?: string
|
|
65
|
+
/** Wording for the value filter and the column chooser. */
|
|
66
|
+
filterLabel?: string
|
|
67
|
+
showAllLabel?: string
|
|
68
|
+
columnsLabel?: string
|
|
50
69
|
}>(),
|
|
51
70
|
{
|
|
52
71
|
emptyLabel: "Nothing here yet",
|
|
@@ -57,6 +76,9 @@ const props = withDefaults(
|
|
|
57
76
|
sortAscLabel: "Sort ascending",
|
|
58
77
|
sortDescLabel: "Sort descending",
|
|
59
78
|
sortClearLabel: "Clear sorting",
|
|
79
|
+
filterLabel: "Show only",
|
|
80
|
+
showAllLabel: "Show all",
|
|
81
|
+
columnsLabel: "Columns",
|
|
60
82
|
},
|
|
61
83
|
)
|
|
62
84
|
|
|
@@ -94,14 +116,75 @@ function menuFor(row: T): ContextMenuItemDef[] {
|
|
|
94
116
|
const ownSort = ref<SortState | null>(null)
|
|
95
117
|
const activeSort = computed(() => props.sort ?? ownSort.value)
|
|
96
118
|
|
|
119
|
+
/*
|
|
120
|
+
* ── hiding values, and hiding columns ────────────────────────────────────────
|
|
121
|
+
*
|
|
122
|
+
* Both live here rather than in each page for the same reason the sort does: a
|
|
123
|
+
* table that can be sorted but not narrowed sends every caller off to build its
|
|
124
|
+
* own filter bar, and six of those are six different ideas of what filtering
|
|
125
|
+
* looks like.
|
|
126
|
+
*
|
|
127
|
+
* State is the *hidden* set, not the shown one. A column that gains a new value
|
|
128
|
+
* — a cable type nobody had recorded yet — then appears by default instead of
|
|
129
|
+
* being invisible until someone notices the list has an unticked entry they
|
|
130
|
+
* never saw.
|
|
131
|
+
*/
|
|
132
|
+
const hiddenValues = ref<Record<string, Set<unknown>>>({})
|
|
133
|
+
const hiddenColumns = ref<Set<string>>(new Set())
|
|
134
|
+
|
|
135
|
+
const visibleColumns = computed(() =>
|
|
136
|
+
props.columns.filter((c) => !hiddenColumns.value.has(c.key)),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
/** The distinct values a filterable column holds, in first-seen order. */
|
|
140
|
+
function valuesOf(col: TableColumn<T>): unknown[] {
|
|
141
|
+
const seen: unknown[] = []
|
|
142
|
+
for (const row of props.rows) {
|
|
143
|
+
const v = row[col.key]
|
|
144
|
+
if (!seen.includes(v)) seen.push(v)
|
|
145
|
+
}
|
|
146
|
+
return seen
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isValueHidden(key: string, value: unknown): boolean {
|
|
150
|
+
return hiddenValues.value[key]?.has(value) ?? false
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toggleValue(key: string, value: unknown) {
|
|
154
|
+
const next = new Set(hiddenValues.value[key] ?? [])
|
|
155
|
+
if (next.has(value)) next.delete(value)
|
|
156
|
+
else next.add(value)
|
|
157
|
+
hiddenValues.value = { ...hiddenValues.value, [key]: next }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function showAllValues(key: string) {
|
|
161
|
+
hiddenValues.value = { ...hiddenValues.value, [key]: new Set() }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function toggleColumn(key: string) {
|
|
165
|
+
const next = new Set(hiddenColumns.value)
|
|
166
|
+
if (next.has(key)) next.delete(key)
|
|
167
|
+
else next.add(key)
|
|
168
|
+
hiddenColumns.value = next
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Rows left once the per-column value filters have been applied. */
|
|
172
|
+
const filteredRows = computed<T[]>(() => {
|
|
173
|
+
const active = Object.entries(hiddenValues.value).filter(([, set]) => set.size)
|
|
174
|
+
if (!active.length) return props.rows
|
|
175
|
+
return props.rows.filter((row) =>
|
|
176
|
+
active.every(([key, hidden]) => !hidden.has(row[key])),
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
|
|
97
180
|
// We compare by raw cell value with a stable-ish coerce.
|
|
98
181
|
const displayRows = computed<T[]>(() => {
|
|
99
182
|
const s = activeSort.value
|
|
100
|
-
if (!s) return
|
|
183
|
+
if (!s) return filteredRows.value
|
|
101
184
|
const col = props.columns.find((c) => c.key === s.key)
|
|
102
|
-
if (!col?.sortable) return
|
|
185
|
+
if (!col?.sortable) return filteredRows.value
|
|
103
186
|
const factor = s.dir === "asc" ? 1 : -1
|
|
104
|
-
return [...
|
|
187
|
+
return [...filteredRows.value].sort((a, b) => compare(a[s.key], b[s.key]) * factor)
|
|
105
188
|
})
|
|
106
189
|
|
|
107
190
|
function compare(a: unknown, b: unknown): number {
|
|
@@ -143,31 +226,86 @@ function sortIcon(col: TableColumn<T>): string {
|
|
|
143
226
|
* list straight through to the browser's own menu.
|
|
144
227
|
*/
|
|
145
228
|
function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
|
|
146
|
-
if (!col.sortable) return []
|
|
147
229
|
const s = activeSort.value
|
|
148
230
|
const set = (next: SortState | null) => () => {
|
|
149
231
|
ownSort.value = next
|
|
150
232
|
emit("update:sort", next)
|
|
151
233
|
}
|
|
234
|
+
|
|
235
|
+
const items: ContextMenuItemDef[] = []
|
|
236
|
+
|
|
237
|
+
if (col.sortable) {
|
|
238
|
+
items.push(
|
|
239
|
+
{
|
|
240
|
+
label: props.sortAscLabel,
|
|
241
|
+
icon: "lucide:arrow-up",
|
|
242
|
+
disabled: s?.key === col.key && s.dir === "asc",
|
|
243
|
+
onSelect: set({ key: col.key, dir: "asc" }),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
label: props.sortDescLabel,
|
|
247
|
+
icon: "lucide:arrow-down",
|
|
248
|
+
disabled: s?.key === col.key && s.dir === "desc",
|
|
249
|
+
onSelect: set({ key: col.key, dir: "desc" }),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
label: props.sortClearLabel,
|
|
253
|
+
icon: "lucide:x",
|
|
254
|
+
disabled: !s,
|
|
255
|
+
onSelect: set(null),
|
|
256
|
+
},
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// "Show only the mains leads" asked of the column that says which is which,
|
|
261
|
+
// rather than of a filter bar somewhere else on the page.
|
|
262
|
+
if (col.filterable) {
|
|
263
|
+
const hidden = hiddenValues.value[col.key]
|
|
264
|
+
items.push({
|
|
265
|
+
label: props.filterLabel,
|
|
266
|
+
icon: "lucide:filter",
|
|
267
|
+
separatorBefore: items.length > 0,
|
|
268
|
+
children: [
|
|
269
|
+
...valuesOf(col).map((value) => ({
|
|
270
|
+
label: col.valueLabel?.(value) ?? String(value ?? "—"),
|
|
271
|
+
checked: !isValueHidden(col.key, value),
|
|
272
|
+
onSelect: () => toggleValue(col.key, value),
|
|
273
|
+
})),
|
|
274
|
+
{
|
|
275
|
+
label: props.showAllLabel,
|
|
276
|
+
icon: "lucide:list",
|
|
277
|
+
separatorBefore: true,
|
|
278
|
+
disabled: !hidden?.size,
|
|
279
|
+
onSelect: () => showAllValues(col.key),
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return items
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The menu for the header itself, away from any one column: which columns to
|
|
290
|
+
* show. Asked here because it is a question about the table rather than about
|
|
291
|
+
* a column — right-clicking `Type` to hide `Where` would be the wrong place.
|
|
292
|
+
*/
|
|
293
|
+
function tableMenu(): ContextMenuItemDef[] {
|
|
294
|
+
const offered = props.columns.filter((c) => !c.alwaysVisible && c.label)
|
|
295
|
+
if (!offered.length) return []
|
|
152
296
|
return [
|
|
153
297
|
{
|
|
154
|
-
label: props.
|
|
155
|
-
icon: "lucide:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
{
|
|
166
|
-
label: props.sortClearLabel,
|
|
167
|
-
icon: "lucide:x",
|
|
168
|
-
separatorBefore: true,
|
|
169
|
-
disabled: !s,
|
|
170
|
-
onSelect: set(null),
|
|
298
|
+
label: props.columnsLabel,
|
|
299
|
+
icon: "lucide:columns-3",
|
|
300
|
+
children: offered.map((col) => ({
|
|
301
|
+
label: col.label,
|
|
302
|
+
checked: !hiddenColumns.value.has(col.key),
|
|
303
|
+
// The last visible column cannot be hidden: a table with no columns is
|
|
304
|
+
// a blank card the operator has no way back from.
|
|
305
|
+
disabled:
|
|
306
|
+
!hiddenColumns.value.has(col.key) && visibleColumns.value.length <= 1,
|
|
307
|
+
onSelect: () => toggleColumn(col.key),
|
|
308
|
+
})),
|
|
171
309
|
},
|
|
172
310
|
]
|
|
173
311
|
}
|
|
@@ -280,6 +418,10 @@ const barInsetTop = computed(() =>
|
|
|
280
418
|
class="rounded-card border border-line"
|
|
281
419
|
:bar-inset-top="barInsetTop"
|
|
282
420
|
>
|
|
421
|
+
<!-- The table-wide menu, which is what a right-click on empty header space
|
|
422
|
+
reaches: rows and headers carry their own and take the event first, so
|
|
423
|
+
this is the fallback rather than a competitor. -->
|
|
424
|
+
<LpContextMenu :items="tableMenu()">
|
|
283
425
|
<table class="w-full border-collapse text-sm">
|
|
284
426
|
<thead ref="headEl" :class="stickyHeader ? 'sticky top-0 z-10' : ''">
|
|
285
427
|
<tr class="border-b border-line bg-surface-soft">
|
|
@@ -291,7 +433,7 @@ const barInsetTop = computed(() =>
|
|
|
291
433
|
/>
|
|
292
434
|
</th>
|
|
293
435
|
<th
|
|
294
|
-
v-for="col in
|
|
436
|
+
v-for="col in visibleColumns"
|
|
295
437
|
:key="col.key"
|
|
296
438
|
:style="col.width ? { width: col.width } : undefined"
|
|
297
439
|
class="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted"
|
|
@@ -385,7 +527,7 @@ const barInsetTop = computed(() =>
|
|
|
385
527
|
/>
|
|
386
528
|
</td>
|
|
387
529
|
<td
|
|
388
|
-
v-for="col in
|
|
530
|
+
v-for="col in visibleColumns"
|
|
389
531
|
:key="col.key"
|
|
390
532
|
class="px-4 py-3 text-ink"
|
|
391
533
|
:class="alignClass(col.align)"
|
|
@@ -398,6 +540,7 @@ const barInsetTop = computed(() =>
|
|
|
398
540
|
</LpContextMenu>
|
|
399
541
|
</TransitionGroup>
|
|
400
542
|
</table>
|
|
543
|
+
</LpContextMenu>
|
|
401
544
|
</LpScrollArea>
|
|
402
545
|
</template>
|
|
403
546
|
|