@leavepulse/ui 0.29.1 → 0.29.3
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.
|
@@ -11,34 +11,35 @@
|
|
|
11
11
|
* fade before closing the gap and the whole list lurches at the end instead of
|
|
12
12
|
* gliding.
|
|
13
13
|
*/
|
|
14
|
-
.lp-row-enter-active[data-v-
|
|
15
|
-
.lp-row-leave-active[data-v-
|
|
14
|
+
.lp-row-enter-active[data-v-4fedb617],
|
|
15
|
+
.lp-row-leave-active[data-v-4fedb617] {
|
|
16
16
|
transition:
|
|
17
17
|
opacity var(--duration-fast) var(--ease-emphasized),
|
|
18
18
|
transform var(--duration-fast) var(--ease-emphasized);
|
|
19
19
|
}
|
|
20
|
-
.lp-row-move[data-v-
|
|
20
|
+
.lp-row-move[data-v-4fedb617] {
|
|
21
21
|
transition: transform var(--duration-medium) var(--ease-emphasized);
|
|
22
22
|
}
|
|
23
|
-
.lp-row-enter-from[data-v-
|
|
24
|
-
.lp-row-leave-to[data-v-
|
|
23
|
+
.lp-row-enter-from[data-v-4fedb617],
|
|
24
|
+
.lp-row-leave-to[data-v-4fedb617] {
|
|
25
25
|
opacity: 0;
|
|
26
26
|
transform: translateX(-0.5rem);
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* The leaving row stays in the flow. Taking it out with `position: absolute` is
|
|
31
|
+
* the usual trick for a list, but a `<tr>` positioned that way loses the table's
|
|
32
|
+
* column grid and collapses to its content width — the row visibly breaks apart
|
|
33
|
+
* on its way out. The rows below close the gap through `lp-row-move` instead,
|
|
34
|
+
* which is a moment slower and stays a table the whole time.
|
|
35
|
+
*/
|
|
32
36
|
|
|
33
37
|
/* A filter that redraws the list is information, not decoration: with motion
|
|
34
38
|
turned down the rows change without animating rather than not changing. */
|
|
35
39
|
@media (prefers-reduced-motion: reduce) {
|
|
36
|
-
.lp-row-enter-active[data-v-
|
|
37
|
-
.lp-row-leave-active[data-v-
|
|
38
|
-
.lp-row-move[data-v-
|
|
40
|
+
.lp-row-enter-active[data-v-4fedb617],
|
|
41
|
+
.lp-row-leave-active[data-v-4fedb617],
|
|
42
|
+
.lp-row-move[data-v-4fedb617] {
|
|
39
43
|
transition: none;
|
|
40
44
|
}
|
|
41
|
-
.lp-row-leave-active[data-v-7884aed8] {
|
|
42
|
-
position: static;
|
|
43
|
-
}
|
|
44
45
|
}
|
|
@@ -31,6 +31,11 @@ declare const __VLS_export: <T extends Record<string, unknown>>(__VLS_props: Non
|
|
|
31
31
|
/** Per-row right-click menu. Return [] (or omit the prop) to disable it for
|
|
32
32
|
* a row; the row then keeps the browser's native context menu. */
|
|
33
33
|
rowMenu?: (row: T) => ContextMenuItemDef[];
|
|
34
|
+
/** Wording for the header's right-click menu. Passed in because the kit
|
|
35
|
+
* carries no translations: an app that leaves these alone gets English. */
|
|
36
|
+
sortAscLabel?: string;
|
|
37
|
+
sortDescLabel?: string;
|
|
38
|
+
sortClearLabel?: string;
|
|
34
39
|
} & {
|
|
35
40
|
"onUpdate:selected"?: ((value: (string | number)[]) => any) | undefined;
|
|
36
41
|
"onUpdate:sort"?: ((value: SortState | null) => any) | undefined;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { defineComponent, ref, computed, watch, onBeforeUnmount, openBlock, createBlock, withCtx, createElementVNode, normalizeClass, createElementBlock, createVNode, createCommentVNode, Fragment, renderList, normalizeStyle, createTextVNode, toDisplayString, TransitionGroup, unref, mergeProps, withModifiers, renderSlot } from "vue";
|
|
2
2
|
import { _ as _sfc_main$2 } from "../chunks/LpCheckbox-BO0zuuPh.js";
|
|
3
|
-
import { _ as _sfc_main$
|
|
4
|
-
import { _ as _sfc_main$
|
|
3
|
+
import { _ as _sfc_main$3 } from "../chunks/LpContextMenu-D0mzXqA7.js";
|
|
4
|
+
import { _ as _sfc_main$4 } from "../chunks/LpIcon-CCnX5_2j.js";
|
|
5
5
|
import { _ as _sfc_main$1 } from "../chunks/LpScrollArea-D8xyKXcF.js";
|
|
6
6
|
import { useRovingFocus } from "../composables/useRovingFocus.js";
|
|
7
7
|
import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
|
|
8
|
-
import '../assets/LpTable-
|
|
8
|
+
import '../assets/LpTable-B9Iko3XD.css';const _hoisted_1 = { class: "w-full border-collapse text-sm" };
|
|
9
9
|
const _hoisted_2 = { class: "border-b border-line bg-surface-soft" };
|
|
10
10
|
const _hoisted_3 = {
|
|
11
11
|
key: 0,
|
|
@@ -28,7 +28,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
28
28
|
selected: { default: () => [] },
|
|
29
29
|
sort: { default: null },
|
|
30
30
|
stickyHeader: { type: Boolean },
|
|
31
|
-
rowMenu: {}
|
|
31
|
+
rowMenu: {},
|
|
32
|
+
sortAscLabel: { default: "Sort ascending" },
|
|
33
|
+
sortDescLabel: { default: "Sort descending" },
|
|
34
|
+
sortClearLabel: { default: "Clear sorting" }
|
|
32
35
|
},
|
|
33
36
|
emits: ["update:selected", "update:sort", "rowClick"],
|
|
34
37
|
setup(__props, { emit: __emit }) {
|
|
@@ -79,6 +82,35 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
79
82
|
if (s?.key !== col.key) return "lucide:chevrons-up-down";
|
|
80
83
|
return s.dir === "asc" ? "lucide:arrow-up" : "lucide:arrow-down";
|
|
81
84
|
}
|
|
85
|
+
function headerMenu(col) {
|
|
86
|
+
if (!col.sortable) return [];
|
|
87
|
+
const s = activeSort.value;
|
|
88
|
+
const set = (next) => () => {
|
|
89
|
+
ownSort.value = next;
|
|
90
|
+
emit("update:sort", next);
|
|
91
|
+
};
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
label: props.sortAscLabel,
|
|
95
|
+
icon: "lucide:arrow-up",
|
|
96
|
+
disabled: s?.key === col.key && s.dir === "asc",
|
|
97
|
+
onSelect: set({ key: col.key, dir: "asc" })
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: props.sortDescLabel,
|
|
101
|
+
icon: "lucide:arrow-down",
|
|
102
|
+
disabled: s?.key === col.key && s.dir === "desc",
|
|
103
|
+
onSelect: set({ key: col.key, dir: "desc" })
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
label: props.sortClearLabel,
|
|
107
|
+
icon: "lucide:x",
|
|
108
|
+
separatorBefore: true,
|
|
109
|
+
disabled: !s,
|
|
110
|
+
onSelect: set(null)
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
}
|
|
82
114
|
const selectedSet = computed(() => new Set(props.selected));
|
|
83
115
|
const allKeys = computed(() => props.rows.map((r, i) => keyFor(r, i)));
|
|
84
116
|
const allChecked = computed(
|
|
@@ -179,25 +211,32 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
179
211
|
class: normalizeClass(["px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted", alignClass(col.align)]),
|
|
180
212
|
"aria-sort": activeSort.value?.key === col.key ? activeSort.value.dir === "asc" ? "ascending" : "descending" : void 0
|
|
181
213
|
}, [
|
|
182
|
-
col.sortable ? (openBlock(),
|
|
214
|
+
col.sortable ? (openBlock(), createBlock(_sfc_main$3, {
|
|
183
215
|
key: 0,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
216
|
+
items: headerMenu(col)
|
|
217
|
+
}, {
|
|
218
|
+
default: withCtx(() => [
|
|
219
|
+
createElementVNode("button", {
|
|
220
|
+
type: "button",
|
|
221
|
+
class: normalizeClass(["group/sort -mx-1 inline-flex items-center gap-1 rounded px-1 outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring", [
|
|
222
|
+
col.align === "right" ? "flex-row-reverse" : "",
|
|
223
|
+
activeSort.value?.key === col.key ? "text-ink" : ""
|
|
224
|
+
]]),
|
|
225
|
+
onClick: ($event) => toggleSort(col)
|
|
226
|
+
}, [
|
|
227
|
+
createTextVNode(toDisplayString(col.label) + " ", 1),
|
|
228
|
+
createVNode(_sfc_main$4, {
|
|
229
|
+
name: sortIcon(col),
|
|
230
|
+
size: 13,
|
|
231
|
+
class: normalizeClass([
|
|
232
|
+
"shrink-0 transition-[transform,opacity] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)]",
|
|
233
|
+
activeSort.value?.key === col.key ? "opacity-100" : "opacity-40 group-hover/sort:opacity-70"
|
|
234
|
+
])
|
|
235
|
+
}, null, 8, ["name", "class"])
|
|
236
|
+
], 10, _hoisted_5)
|
|
237
|
+
]),
|
|
238
|
+
_: 2
|
|
239
|
+
}, 1032, ["items"])) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
201
240
|
createTextVNode(toDisplayString(col.label), 1)
|
|
202
241
|
], 64))
|
|
203
242
|
], 14, _hoisted_4);
|
|
@@ -215,7 +254,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
215
254
|
colspan: colSpan.value,
|
|
216
255
|
class: "px-4 py-10 text-center text-muted"
|
|
217
256
|
}, [
|
|
218
|
-
createVNode(_sfc_main$
|
|
257
|
+
createVNode(_sfc_main$4, {
|
|
219
258
|
name: __props.emptyIcon,
|
|
220
259
|
size: 22,
|
|
221
260
|
class: "mx-auto mb-2 opacity-60"
|
|
@@ -224,7 +263,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
224
263
|
], 8, _hoisted_7)
|
|
225
264
|
])) : createCommentVNode("", true),
|
|
226
265
|
(openBlock(true), createElementBlock(Fragment, null, renderList(displayRows.value, (row, index) => {
|
|
227
|
-
return openBlock(), createBlock(_sfc_main$
|
|
266
|
+
return openBlock(), createBlock(_sfc_main$3, {
|
|
228
267
|
key: keyFor(row, index),
|
|
229
268
|
items: menuFor(row)
|
|
230
269
|
}, {
|
|
@@ -274,7 +313,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
274
313
|
};
|
|
275
314
|
}
|
|
276
315
|
});
|
|
277
|
-
const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-
|
|
316
|
+
const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-4fedb617"]]);
|
|
278
317
|
export {
|
|
279
318
|
LpTable as default
|
|
280
319
|
};
|
package/package.json
CHANGED
|
@@ -42,6 +42,11 @@ const props = withDefaults(
|
|
|
42
42
|
/** Per-row right-click menu. Return [] (or omit the prop) to disable it for
|
|
43
43
|
* a row; the row then keeps the browser's native context menu. */
|
|
44
44
|
rowMenu?: (row: T) => ContextMenuItemDef[]
|
|
45
|
+
/** Wording for the header's right-click menu. Passed in because the kit
|
|
46
|
+
* carries no translations: an app that leaves these alone gets English. */
|
|
47
|
+
sortAscLabel?: string
|
|
48
|
+
sortDescLabel?: string
|
|
49
|
+
sortClearLabel?: string
|
|
45
50
|
}>(),
|
|
46
51
|
{
|
|
47
52
|
emptyLabel: "Nothing here yet",
|
|
@@ -49,6 +54,9 @@ const props = withDefaults(
|
|
|
49
54
|
selectable: false,
|
|
50
55
|
selected: () => [],
|
|
51
56
|
sort: null,
|
|
57
|
+
sortAscLabel: "Sort ascending",
|
|
58
|
+
sortDescLabel: "Sort descending",
|
|
59
|
+
sortClearLabel: "Clear sorting",
|
|
52
60
|
},
|
|
53
61
|
)
|
|
54
62
|
|
|
@@ -124,6 +132,46 @@ function sortIcon(col: TableColumn<T>): string {
|
|
|
124
132
|
return s.dir === "asc" ? "lucide:arrow-up" : "lucide:arrow-down"
|
|
125
133
|
}
|
|
126
134
|
|
|
135
|
+
/**
|
|
136
|
+
* What right-clicking a header offers: the directions by name.
|
|
137
|
+
*
|
|
138
|
+
* The click cycles none → asc → desc → none, which is right for the common
|
|
139
|
+
* case and clumsy for the rest — going back to unsorted from ascending is two
|
|
140
|
+
* more clicks. Naming the three states makes each one reachable at once.
|
|
141
|
+
*
|
|
142
|
+
* Empty for a column that does not sort, and LpContextMenu passes an empty
|
|
143
|
+
* list straight through to the browser's own menu.
|
|
144
|
+
*/
|
|
145
|
+
function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
|
|
146
|
+
if (!col.sortable) return []
|
|
147
|
+
const s = activeSort.value
|
|
148
|
+
const set = (next: SortState | null) => () => {
|
|
149
|
+
ownSort.value = next
|
|
150
|
+
emit("update:sort", next)
|
|
151
|
+
}
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
label: props.sortAscLabel,
|
|
155
|
+
icon: "lucide:arrow-up",
|
|
156
|
+
disabled: s?.key === col.key && s.dir === "asc",
|
|
157
|
+
onSelect: set({ key: col.key, dir: "asc" }),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
label: props.sortDescLabel,
|
|
161
|
+
icon: "lucide:arrow-down",
|
|
162
|
+
disabled: s?.key === col.key && s.dir === "desc",
|
|
163
|
+
onSelect: set({ key: col.key, dir: "desc" }),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
label: props.sortClearLabel,
|
|
167
|
+
icon: "lucide:x",
|
|
168
|
+
separatorBefore: true,
|
|
169
|
+
disabled: !s,
|
|
170
|
+
onSelect: set(null),
|
|
171
|
+
},
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
|
|
127
175
|
// ── selection ────────────────────────────────────────────────
|
|
128
176
|
const selectedSet = computed(() => new Set(props.selected))
|
|
129
177
|
const allKeys = computed(() => props.rows.map((r, i) => keyFor(r, i)))
|
|
@@ -256,8 +304,15 @@ const barInsetTop = computed(() =>
|
|
|
256
304
|
: undefined
|
|
257
305
|
"
|
|
258
306
|
>
|
|
307
|
+
<!-- The menu wraps the button rather than the cell: a component
|
|
308
|
+
cannot sit between `<tr>` and `<th>` — the template compiler
|
|
309
|
+
drops it there, silently — so the trigger goes on the one
|
|
310
|
+
element inside the header that is already interactive.
|
|
311
|
+
Right-clicking names the directions instead of making you cycle
|
|
312
|
+
through them: getting back to unsorted from ascending is two
|
|
313
|
+
more clicks otherwise. -->
|
|
314
|
+
<LpContextMenu v-if="col.sortable" :items="headerMenu(col)">
|
|
259
315
|
<button
|
|
260
|
-
v-if="col.sortable"
|
|
261
316
|
type="button"
|
|
262
317
|
class="group/sort -mx-1 inline-flex items-center gap-1 rounded px-1 outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
263
318
|
:class="[
|
|
@@ -278,6 +333,7 @@ const barInsetTop = computed(() =>
|
|
|
278
333
|
"
|
|
279
334
|
/>
|
|
280
335
|
</button>
|
|
336
|
+
</LpContextMenu>
|
|
281
337
|
<template v-else>{{ col.label }}</template>
|
|
282
338
|
</th>
|
|
283
339
|
</tr>
|
|
@@ -375,10 +431,13 @@ const barInsetTop = computed(() =>
|
|
|
375
431
|
transform: translateX(-0.5rem);
|
|
376
432
|
}
|
|
377
433
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
434
|
+
/*
|
|
435
|
+
* The leaving row stays in the flow. Taking it out with `position: absolute` is
|
|
436
|
+
* the usual trick for a list, but a `<tr>` positioned that way loses the table's
|
|
437
|
+
* column grid and collapses to its content width — the row visibly breaks apart
|
|
438
|
+
* on its way out. The rows below close the gap through `lp-row-move` instead,
|
|
439
|
+
* which is a moment slower and stays a table the whole time.
|
|
440
|
+
*/
|
|
382
441
|
|
|
383
442
|
/* A filter that redraws the list is information, not decoration: with motion
|
|
384
443
|
turned down the rows change without animating rather than not changing. */
|
|
@@ -388,9 +447,5 @@ const barInsetTop = computed(() =>
|
|
|
388
447
|
.lp-row-move {
|
|
389
448
|
transition: none;
|
|
390
449
|
}
|
|
391
|
-
|
|
392
|
-
.lp-row-leave-active {
|
|
393
|
-
position: static;
|
|
394
|
-
}
|
|
395
450
|
}
|
|
396
451
|
</style>
|