@leavepulse/ui 0.36.1 → 0.37.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,11 +1,13 @@
1
- import { defineComponent, ref, computed, watch, onBeforeUnmount, openBlock, createBlock, withCtx, createVNode, createElementVNode, normalizeClass, createElementBlock, createCommentVNode, Fragment, renderList, normalizeStyle, createTextVNode, toDisplayString, TransitionGroup, unref, mergeProps, withModifiers, renderSlot } from "vue";
1
+ import { defineComponent, ref, computed, shallowRef, watch, onBeforeUnmount, openBlock, createElementBlock, createVNode, mergeProps, withCtx, createElementVNode, normalizeClass, createCommentVNode, Fragment, renderList, normalizeStyle, createBlock, createTextVNode, toDisplayString, TransitionGroup, unref, withModifiers, renderSlot, nextTick } from "vue";
2
2
  import { _ as _sfc_main$3 } from "../chunks/LpCheckbox-CVccKYaH.js";
3
3
  import { _ as _sfc_main$2 } from "../chunks/LpContextMenu-B6WBLleS.js";
4
4
  import { _ as _sfc_main$4 } from "../chunks/LpIcon-CCnX5_2j.js";
5
+ import { _ as _sfc_main$5 } from "../chunks/LpPagination-5FxjmmB5.js";
5
6
  import { _ as _sfc_main$1 } from "../chunks/LpScrollArea-D8xyKXcF.js";
7
+ import { useAsyncTask } from "../composables/useAsyncTask.js";
6
8
  import { useRovingFocus } from "../composables/useRovingFocus.js";
7
9
  import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
8
- import '../assets/LpTable-DfSwPCJ-.css';const _hoisted_1 = { class: "w-full border-collapse text-sm" };
10
+ import '../assets/LpTable-BbOFmZfc.css';const _hoisted_1 = { class: "flex flex-col gap-3" };
9
11
  const _hoisted_2 = { class: "border-b border-line bg-surface-soft" };
10
12
  const _hoisted_3 = {
11
13
  key: 0,
@@ -19,11 +21,18 @@ const _hoisted_6 = {
19
21
  };
20
22
  const _hoisted_7 = ["colspan"];
21
23
  const _hoisted_8 = ["onContextmenu", "onClick"];
24
+ const __default__ = { inheritAttrs: false };
22
25
  const _sfc_main = /* @__PURE__ */ defineComponent({
26
+ ...__default__,
23
27
  __name: "LpTable",
24
28
  props: {
25
29
  columns: {},
26
- rows: {},
30
+ rows: { default: () => [] },
31
+ load: {},
32
+ pageSize: { default: 25 },
33
+ debounce: { default: 150 },
34
+ loadingLabel: { default: "Loading…" },
35
+ errorLabel: { default: "Could not load" },
27
36
  rowKey: {},
28
37
  emptyLabel: { default: "Nothing here yet" },
29
38
  emptyIcon: { default: "lucide:inbox" },
@@ -40,7 +49,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
40
49
  columnsLabel: { default: "Columns" }
41
50
  },
42
51
  emits: ["update:selected", "update:sort", "rowClick"],
43
- setup(__props, { emit: __emit }) {
52
+ setup(__props, { expose: __expose, emit: __emit }) {
44
53
  const props = __props;
45
54
  const emit = __emit;
46
55
  function keyFor(row, index) {
@@ -66,14 +75,40 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
66
75
  });
67
76
  const ownSort = ref(null);
68
77
  const activeSort = computed(() => props.sort ?? ownSort.value);
78
+ const loaded = shallowRef([]);
79
+ const total = ref(0);
80
+ const facets = ref({});
81
+ const failed = ref(false);
82
+ const page = ref(1);
83
+ const task = useAsyncTask();
84
+ const loading = task.pending;
85
+ const sourceRows = computed(() => props.load ? loaded.value : props.rows);
86
+ const tableEl = ref(null);
87
+ const floor = ref(0);
88
+ async function holdHeight() {
89
+ await nextTick();
90
+ const height = tableEl.value?.offsetHeight ?? 0;
91
+ if (height > floor.value) floor.value = height;
92
+ }
69
93
  const hiddenValues = ref({});
94
+ const animating = ref(false);
95
+ let motionTimer;
96
+ function withMotion(change) {
97
+ if (!props.load) {
98
+ animating.value = true;
99
+ if (motionTimer) clearTimeout(motionTimer);
100
+ motionTimer = setTimeout(() => animating.value = false, 600);
101
+ }
102
+ change();
103
+ }
70
104
  const hiddenColumns = ref(/* @__PURE__ */ new Set());
71
105
  const visibleColumns = computed(
72
106
  () => props.columns.filter((c) => !hiddenColumns.value.has(c.key))
73
107
  );
74
108
  function valuesOf(col) {
109
+ if (props.load && facets.value[col.key]) return facets.value[col.key];
75
110
  const seen = [];
76
- for (const row of props.rows) {
111
+ for (const row of sourceRows.value) {
77
112
  const v = row[col.key];
78
113
  if (!seen.includes(v)) seen.push(v);
79
114
  }
@@ -83,13 +118,16 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
83
118
  return hiddenValues.value[key]?.has(value) ?? false;
84
119
  }
85
120
  function toggleValue(key, value) {
121
+ withMotion(() => applyToggle(key, value));
122
+ }
123
+ function applyToggle(key, value) {
86
124
  const next = new Set(hiddenValues.value[key] ?? []);
87
125
  if (next.has(value)) next.delete(value);
88
126
  else next.add(value);
89
127
  hiddenValues.value = { ...hiddenValues.value, [key]: next };
90
128
  }
91
129
  function showAllValues(key) {
92
- hiddenValues.value = { ...hiddenValues.value, [key]: /* @__PURE__ */ new Set() };
130
+ withMotion(() => hiddenValues.value = { ...hiddenValues.value, [key]: /* @__PURE__ */ new Set() });
93
131
  }
94
132
  function toggleColumn(key) {
95
133
  const next = new Set(hiddenColumns.value);
@@ -98,6 +136,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
98
136
  hiddenColumns.value = next;
99
137
  }
100
138
  const filteredRows = computed(() => {
139
+ if (props.load) return sourceRows.value;
101
140
  const active = Object.entries(hiddenValues.value).filter(([, set]) => set.size);
102
141
  if (!active.length) return props.rows;
103
142
  return props.rows.filter(
@@ -106,7 +145,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
106
145
  });
107
146
  const displayRows = computed(() => {
108
147
  const s = activeSort.value;
109
- if (!s) return filteredRows.value;
148
+ if (!s || props.load) return filteredRows.value;
110
149
  const col = props.columns.find((c) => c.key === s.key);
111
150
  if (!col?.sortable) return filteredRows.value;
112
151
  const factor = s.dir === "asc" ? 1 : -1;
@@ -126,8 +165,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
126
165
  if (!s || s.key !== col.key) next = { key: col.key, dir: "asc" };
127
166
  else if (s.dir === "asc") next = { key: col.key, dir: "desc" };
128
167
  else next = null;
129
- ownSort.value = next;
130
- emit("update:sort", next);
168
+ withMotion(() => {
169
+ ownSort.value = next;
170
+ emit("update:sort", next);
171
+ });
131
172
  }
132
173
  function sortIcon(col) {
133
174
  const s = activeSort.value;
@@ -136,10 +177,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
136
177
  }
137
178
  function headerMenu(col) {
138
179
  const s = activeSort.value;
139
- const set = (next) => () => {
180
+ const set = (next) => () => withMotion(() => {
140
181
  ownSort.value = next;
141
182
  emit("update:sort", next);
142
- };
183
+ });
143
184
  const items = [];
144
185
  if (col.sortable) {
145
186
  items.push(
@@ -205,8 +246,37 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
205
246
  }
206
247
  ];
207
248
  }
249
+ function fetchPage(delay = 0) {
250
+ const load = props.load;
251
+ if (!load) return;
252
+ const hidden = Object.fromEntries(
253
+ Object.entries(hiddenValues.value).filter(([, set]) => set.size).map(([key, set]) => [key, [...set]])
254
+ );
255
+ const query = { page: page.value, pageSize: props.pageSize, sort: activeSort.value, hidden };
256
+ task.run((signal) => load(query, signal), {
257
+ delay,
258
+ apply: (result) => {
259
+ loaded.value = result.rows;
260
+ total.value = result.total;
261
+ facets.value = result.facets ?? {};
262
+ failed.value = false;
263
+ holdHeight();
264
+ },
265
+ fail: () => {
266
+ failed.value = true;
267
+ }
268
+ });
269
+ }
270
+ watch([activeSort, hiddenValues], () => {
271
+ if (!props.load) return;
272
+ if (page.value !== 1) page.value = 1;
273
+ else fetchPage(props.debounce);
274
+ });
275
+ watch(page, () => fetchPage());
276
+ watch(() => props.load, () => fetchPage(), { immediate: true });
277
+ __expose({ reload: () => fetchPage() });
208
278
  const selectedSet = computed(() => new Set(props.selected));
209
- const allKeys = computed(() => props.rows.map((r, i) => keyFor(r, i)));
279
+ const allKeys = computed(() => sourceRows.value.map((r, i) => keyFor(r, i)));
210
280
  const allChecked = computed(
211
281
  () => allKeys.value.length > 0 && allKeys.value.every((k) => selectedSet.value.has(k))
212
282
  );
@@ -273,143 +343,169 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
273
343
  },
274
344
  { immediate: true, flush: "post" }
275
345
  );
276
- onBeforeUnmount(() => ro?.disconnect());
346
+ onBeforeUnmount(() => {
347
+ ro?.disconnect();
348
+ if (motionTimer) clearTimeout(motionTimer);
349
+ });
277
350
  const barInsetTop = computed(
278
351
  () => props.stickyHeader && headHeight.value ? `${headHeight.value}px` : void 0
279
352
  );
280
353
  return (_ctx, _cache) => {
281
- return openBlock(), createBlock(_sfc_main$1, {
282
- fade: false,
283
- class: "rounded-card border border-line",
284
- "bar-inset-top": barInsetTop.value
285
- }, {
286
- default: withCtx(() => [
287
- createVNode(_sfc_main$2, {
288
- items: contextMenu.value,
289
- always: ""
290
- }, {
291
- default: withCtx(() => [
292
- createElementVNode("table", _hoisted_1, [
293
- createElementVNode("thead", {
294
- ref_key: "headEl",
295
- ref: headEl,
296
- class: normalizeClass(__props.stickyHeader ? "sticky top-0 z-10 bg-surface-soft" : "")
354
+ return openBlock(), createElementBlock("div", _hoisted_1, [
355
+ createVNode(_sfc_main$1, mergeProps(_ctx.$attrs, {
356
+ fade: false,
357
+ class: "rounded-card border border-line",
358
+ style: __props.load && floor.value ? { minHeight: `${floor.value + 2}px` } : void 0,
359
+ "bar-inset-top": barInsetTop.value
360
+ }), {
361
+ default: withCtx(() => [
362
+ createVNode(_sfc_main$2, {
363
+ items: contextMenu.value,
364
+ always: ""
365
+ }, {
366
+ default: withCtx(() => [
367
+ createElementVNode("table", {
368
+ ref_key: "tableEl",
369
+ ref: tableEl,
370
+ class: "w-full border-collapse text-sm"
297
371
  }, [
298
- createElementVNode("tr", _hoisted_2, [
299
- __props.selectable ? (openBlock(), createElementBlock("th", _hoisted_3, [
300
- createVNode(_sfc_main$3, {
301
- "model-value": allChecked.value,
302
- indeterminate: someChecked.value,
303
- "onUpdate:modelValue": toggleAll
304
- }, null, 8, ["model-value", "indeterminate"])
305
- ])) : createCommentVNode("", true),
306
- (openBlock(true), createElementBlock(Fragment, null, renderList(visibleColumns.value, (col) => {
307
- return openBlock(), createElementBlock("th", {
308
- key: col.key,
309
- style: normalizeStyle(col.width ? { width: col.width } : void 0),
310
- class: normalizeClass(["px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted", alignClass(col.align)]),
311
- "aria-sort": activeSort.value?.key === col.key ? activeSort.value.dir === "asc" ? "ascending" : "descending" : void 0
312
- }, [
313
- col.sortable ? (openBlock(), createBlock(_sfc_main$2, {
314
- key: 0,
315
- items: headerMenu(col)
316
- }, {
317
- default: withCtx(() => [
318
- createElementVNode("button", {
319
- type: "button",
320
- 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", [
321
- col.align === "right" ? "flex-row-reverse" : "",
322
- activeSort.value?.key === col.key ? "text-ink" : ""
323
- ]]),
324
- onClick: ($event) => toggleSort(col)
325
- }, [
326
- createTextVNode(toDisplayString(col.label) + " ", 1),
327
- createVNode(_sfc_main$4, {
328
- name: sortIcon(col),
329
- size: 13,
330
- class: normalizeClass([
331
- "shrink-0 transition-[transform,opacity] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)]",
332
- activeSort.value?.key === col.key ? "opacity-100" : "opacity-40 group-hover/sort:opacity-70"
333
- ])
334
- }, null, 8, ["name", "class"])
335
- ], 10, _hoisted_5)
336
- ]),
337
- _: 2
338
- }, 1032, ["items"])) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
339
- createTextVNode(toDisplayString(col.label), 1)
340
- ], 64))
341
- ], 14, _hoisted_4);
342
- }), 128))
343
- ])
344
- ], 2),
345
- createVNode(TransitionGroup, {
346
- tag: "tbody",
347
- name: "lp-row",
348
- ref: (el) => unref(roving).setContainer(el?.$el ?? null)
349
- }, {
350
- default: withCtx(() => [
351
- displayRows.value.length === 0 ? (openBlock(), createElementBlock("tr", _hoisted_6, [
352
- createElementVNode("td", {
353
- colspan: colSpan.value,
354
- class: "px-4 py-10 text-center text-muted"
355
- }, [
356
- createVNode(_sfc_main$4, {
357
- name: __props.emptyIcon,
358
- size: 22,
359
- class: "mx-auto mb-2 opacity-60"
360
- }, null, 8, ["name"]),
361
- createElementVNode("div", null, toDisplayString(__props.emptyLabel), 1)
362
- ], 8, _hoisted_7)
363
- ])) : createCommentVNode("", true),
364
- (openBlock(true), createElementBlock(Fragment, null, renderList(displayRows.value, (row, index) => {
365
- return openBlock(), createElementBlock("tr", mergeProps({
366
- key: keyFor(row, index),
367
- onContextmenu: ($event) => onRowContextMenu(row),
368
- class: ["group border-b border-line/60 outline-none transition-colors last:border-0 hover:bg-surface-soft/60 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring data-[active]:bg-surface-soft/40", selectedSet.value.has(keyFor(row, index)) ? "bg-brand-soft/40" : ""]
369
- }, { ref_for: true }, unref(roving).itemProps(String(keyFor(row, index))), {
370
- onClick: ($event) => emit("rowClick", row),
371
- onKeydown: onRowKeydown
372
- }), [
373
- __props.selectable ? (openBlock(), createElementBlock("td", {
374
- key: 0,
375
- class: "w-px px-4 py-3",
376
- onClick: _cache[0] || (_cache[0] = withModifiers(() => {
377
- }, ["stop"]))
372
+ createElementVNode("thead", {
373
+ ref_key: "headEl",
374
+ ref: headEl,
375
+ class: normalizeClass(__props.stickyHeader ? "sticky top-0 z-10 bg-surface-soft" : "")
376
+ }, [
377
+ createElementVNode("tr", _hoisted_2, [
378
+ __props.selectable ? (openBlock(), createElementBlock("th", _hoisted_3, [
379
+ createVNode(_sfc_main$3, {
380
+ "model-value": allChecked.value,
381
+ indeterminate: someChecked.value,
382
+ "onUpdate:modelValue": toggleAll
383
+ }, null, 8, ["model-value", "indeterminate"])
384
+ ])) : createCommentVNode("", true),
385
+ (openBlock(true), createElementBlock(Fragment, null, renderList(visibleColumns.value, (col) => {
386
+ return openBlock(), createElementBlock("th", {
387
+ key: col.key,
388
+ style: normalizeStyle(col.width ? { width: col.width } : void 0),
389
+ class: normalizeClass(["px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted", alignClass(col.align)]),
390
+ "aria-sort": activeSort.value?.key === col.key ? activeSort.value.dir === "asc" ? "ascending" : "descending" : void 0
391
+ }, [
392
+ col.sortable || col.filterable ? (openBlock(), createBlock(_sfc_main$2, {
393
+ key: 0,
394
+ items: headerMenu(col)
395
+ }, {
396
+ default: withCtx(() => [
397
+ createElementVNode("button", {
398
+ type: "button",
399
+ 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", [
400
+ col.align === "right" ? "flex-row-reverse" : "",
401
+ activeSort.value?.key === col.key ? "text-ink" : ""
402
+ ]]),
403
+ onClick: ($event) => toggleSort(col)
404
+ }, [
405
+ createTextVNode(toDisplayString(col.label) + " ", 1),
406
+ !col.sortable ? (openBlock(), createBlock(_sfc_main$4, {
407
+ key: 0,
408
+ name: "lucide:filter",
409
+ size: 12,
410
+ class: normalizeClass(["shrink-0", hiddenValues.value[col.key]?.size ? "text-brand opacity-100" : "opacity-40 group-hover/sort:opacity-70"])
411
+ }, null, 8, ["class"])) : (openBlock(), createBlock(_sfc_main$4, {
412
+ key: 1,
413
+ name: sortIcon(col),
414
+ size: 13,
415
+ class: normalizeClass([
416
+ "shrink-0 transition-[transform,opacity] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)]",
417
+ activeSort.value?.key === col.key ? "opacity-100" : "opacity-40 group-hover/sort:opacity-70"
418
+ ])
419
+ }, null, 8, ["name", "class"]))
420
+ ], 10, _hoisted_5)
421
+ ]),
422
+ _: 2
423
+ }, 1032, ["items"])) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
424
+ createTextVNode(toDisplayString(col.label), 1)
425
+ ], 64))
426
+ ], 14, _hoisted_4);
427
+ }), 128))
428
+ ])
429
+ ], 2),
430
+ createVNode(TransitionGroup, {
431
+ tag: "tbody",
432
+ name: "lp-row",
433
+ css: animating.value,
434
+ class: normalizeClass(unref(loading) && displayRows.value.length ? "opacity-60 transition-opacity" : "transition-opacity"),
435
+ ref: (el) => unref(roving).setContainer(el?.$el ?? null)
436
+ }, {
437
+ default: withCtx(() => [
438
+ displayRows.value.length === 0 ? (openBlock(), createElementBlock("tr", _hoisted_6, [
439
+ createElementVNode("td", {
440
+ colspan: colSpan.value,
441
+ class: "px-4 py-10 text-center text-muted"
378
442
  }, [
379
- createVNode(_sfc_main$3, {
380
- "model-value": selectedSet.value.has(keyFor(row, index)),
381
- "onUpdate:modelValue": (v) => toggleRow(keyFor(row, index), v)
382
- }, null, 8, ["model-value", "onUpdate:modelValue"])
383
- ])) : createCommentVNode("", true),
384
- (openBlock(true), createElementBlock(Fragment, null, renderList(visibleColumns.value, (col) => {
385
- return openBlock(), createElementBlock("td", {
386
- key: col.key,
387
- class: normalizeClass(["px-4 py-3 text-ink", alignClass(col.align)])
443
+ createVNode(_sfc_main$4, {
444
+ name: unref(loading) ? "lucide:loader-circle" : failed.value ? "lucide:circle-alert" : __props.emptyIcon,
445
+ size: 22,
446
+ class: normalizeClass(["mx-auto mb-2 opacity-60", unref(loading) ? "animate-spin" : ""])
447
+ }, null, 8, ["name", "class"]),
448
+ createElementVNode("div", null, toDisplayString(unref(loading) ? __props.loadingLabel : failed.value ? __props.errorLabel : __props.emptyLabel), 1)
449
+ ], 8, _hoisted_7)
450
+ ])) : createCommentVNode("", true),
451
+ (openBlock(true), createElementBlock(Fragment, null, renderList(displayRows.value, (row, index) => {
452
+ return openBlock(), createElementBlock("tr", mergeProps({
453
+ key: keyFor(row, index),
454
+ onContextmenu: ($event) => onRowContextMenu(row),
455
+ class: ["group border-b border-line/60 outline-none transition-colors last:border-0 hover:bg-surface-soft/60 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring data-[active]:bg-surface-soft/40", selectedSet.value.has(keyFor(row, index)) ? "bg-brand-soft/40" : ""]
456
+ }, { ref_for: true }, unref(roving).itemProps(String(keyFor(row, index))), {
457
+ onClick: ($event) => emit("rowClick", row),
458
+ onKeydown: onRowKeydown
459
+ }), [
460
+ __props.selectable ? (openBlock(), createElementBlock("td", {
461
+ key: 0,
462
+ class: "w-px px-4 py-3",
463
+ onClick: _cache[0] || (_cache[0] = withModifiers(() => {
464
+ }, ["stop"]))
388
465
  }, [
389
- renderSlot(_ctx.$slots, `cell-${col.key}`, {
390
- row,
391
- value: row[col.key]
392
- }, () => [
393
- createTextVNode(toDisplayString(row[col.key]), 1)
394
- ], true)
395
- ], 2);
396
- }), 128))
397
- ], 16, _hoisted_8);
398
- }), 128))
399
- ]),
400
- _: 3
401
- }, 512)
402
- ])
403
- ]),
404
- _: 3
405
- }, 8, ["items"])
406
- ]),
407
- _: 3
408
- }, 8, ["bar-inset-top"]);
466
+ createVNode(_sfc_main$3, {
467
+ "model-value": selectedSet.value.has(keyFor(row, index)),
468
+ "onUpdate:modelValue": (v) => toggleRow(keyFor(row, index), v)
469
+ }, null, 8, ["model-value", "onUpdate:modelValue"])
470
+ ])) : createCommentVNode("", true),
471
+ (openBlock(true), createElementBlock(Fragment, null, renderList(visibleColumns.value, (col) => {
472
+ return openBlock(), createElementBlock("td", {
473
+ key: col.key,
474
+ class: normalizeClass(["px-4 py-3 text-ink", alignClass(col.align)])
475
+ }, [
476
+ renderSlot(_ctx.$slots, `cell-${col.key}`, {
477
+ row,
478
+ value: row[col.key]
479
+ }, () => [
480
+ createTextVNode(toDisplayString(row[col.key]), 1)
481
+ ], true)
482
+ ], 2);
483
+ }), 128))
484
+ ], 16, _hoisted_8);
485
+ }), 128))
486
+ ]),
487
+ _: 3
488
+ }, 8, ["css", "class"])
489
+ ], 512)
490
+ ]),
491
+ _: 3
492
+ }, 8, ["items"])
493
+ ]),
494
+ _: 3
495
+ }, 16, ["style", "bar-inset-top"]),
496
+ __props.load && total.value > __props.pageSize ? (openBlock(), createBlock(_sfc_main$5, {
497
+ key: 0,
498
+ page: page.value,
499
+ "onUpdate:page": _cache[1] || (_cache[1] = ($event) => page.value = $event),
500
+ total: total.value,
501
+ "page-size": __props.pageSize,
502
+ disabled: unref(loading)
503
+ }, null, 8, ["page", "total", "page-size", "disabled"])) : createCommentVNode("", true)
504
+ ]);
409
505
  };
410
506
  }
411
507
  });
412
- const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-1fada8e7"]]);
508
+ const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-63738e53"]]);
413
509
  export {
414
510
  LpTable as default
415
511
  };
@@ -1,5 +1,5 @@
1
1
  import { ref, computed } from "vue";
2
- const STEP = 10;
2
+ const STEP = 20;
3
3
  const stack = ref([]);
4
4
  let nextId = 0;
5
5
  function useModalLayer() {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * One debounced, abortable request at a time — the shape every "fetch on input"
3
+ * control in the kit needs: the command palette's `search`, the table's `load`.
4
+ *
5
+ * Two races are lost silently without it: a stale response overwriting a
6
+ * fresher one, and a response landing after the control moved on. The token
7
+ * guards both — the abort signal only asks the consumer to stop, it cannot
8
+ * promise they did.
9
+ */
10
+ export declare function useAsyncTask<T>(): {
11
+ pending: import('vue').Ref<boolean, boolean>;
12
+ run: (task: (signal: AbortSignal) => T | Promise<T>, options: {
13
+ delay?: number;
14
+ apply: (value: T) => void;
15
+ fail?: (error: unknown) => void;
16
+ }) => void;
17
+ cancel: () => void;
18
+ };
@@ -0,0 +1,36 @@
1
+ import { ref, getCurrentScope, onScopeDispose } from "vue";
2
+ function useAsyncTask() {
3
+ const pending = ref(false);
4
+ let timer;
5
+ let controller;
6
+ let token = 0;
7
+ function cancel() {
8
+ if (timer) clearTimeout(timer);
9
+ timer = void 0;
10
+ controller?.abort();
11
+ controller = void 0;
12
+ token += 1;
13
+ pending.value = false;
14
+ }
15
+ function run(task, options) {
16
+ cancel();
17
+ pending.value = true;
18
+ const mine = ++token;
19
+ timer = setTimeout(async () => {
20
+ controller = new AbortController();
21
+ try {
22
+ const value = await task(controller.signal);
23
+ if (mine === token) options.apply(value);
24
+ } catch (error) {
25
+ if (mine === token) options.fail?.(error);
26
+ } finally {
27
+ if (mine === token) pending.value = false;
28
+ }
29
+ }, options.delay ?? 0);
30
+ }
31
+ if (getCurrentScope()) onScopeDispose(cancel);
32
+ return { pending, run, cancel };
33
+ }
34
+ export {
35
+ useAsyncTask
36
+ };
package/dist/index.d.ts CHANGED
@@ -101,7 +101,7 @@ export { default as LpStepper } from './components/LpStepper.vue';
101
101
  export type { Step } from './components/LpStepper.vue';
102
102
  export { default as LpSwitch } from './components/LpSwitch.vue';
103
103
  export { default as LpTable } from './components/LpTable.vue';
104
- export type { SortState, TableColumn } from './components/LpTable.vue';
104
+ export type { SortState, TableColumn, TablePage, TableQuery } from './components/LpTable.vue';
105
105
  export { default as LpTableOfContents } from './components/LpTableOfContents.vue';
106
106
  export type { TocLink } from './components/LpTableOfContents.vue';
107
107
  export { default as LpTabs } from './components/LpTabs.vue';
@@ -142,6 +142,7 @@ export type { UseInputFilterOptions } from './composables/useInputFilter';
142
142
  export { easeOut, easeTravel, easingToken, prefersReducedMotion } from './composables/easing';
143
143
  export { useReveal } from './composables/useReveal';
144
144
  export type { UseReveal, UseRevealOptions } from './composables/useReveal';
145
+ export { useAsyncTask } from './composables/useAsyncTask';
145
146
  export { useRovingFocus } from './composables/useRovingFocus';
146
147
  export type { RovingItem, UseRovingFocusOptions } from './composables/useRovingFocus';
147
148
  export { useFormDraft } from './composables/useFormDraft';
package/dist/index.js CHANGED
@@ -91,6 +91,7 @@ import { mergeClass, useMergedAttrs, useMergedClass } from "./composables/useMer
91
91
  import { useInputFilter } from "./composables/useInputFilter.js";
92
92
  import { easeOut, easeTravel, easingToken, prefersReducedMotion } from "./composables/easing.js";
93
93
  import { useReveal } from "./composables/useReveal.js";
94
+ import { useAsyncTask } from "./composables/useAsyncTask.js";
94
95
  import { useRovingFocus } from "./composables/useRovingFocus.js";
95
96
  import { useFormDraft } from "./composables/useFormDraft.js";
96
97
  import { useSettled } from "./composables/useSettled.js";
@@ -290,6 +291,7 @@ export {
290
291
  m as themeToCssVars,
291
292
  tileUrl,
292
293
  unproject,
294
+ useAsyncTask,
293
295
  useClipboard,
294
296
  useFormDraft,
295
297
  useHotkeys,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.36.1",
3
+ "version": "0.37.1",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {