@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-7884aed8],
15
- .lp-row-leave-active[data-v-7884aed8] {
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-7884aed8] {
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-7884aed8],
24
- .lp-row-leave-to[data-v-7884aed8] {
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
- .lp-row-leave-active[data-v-7884aed8] {
29
- position: absolute;
30
- width: 100%;
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-7884aed8],
37
- .lp-row-leave-active[data-v-7884aed8],
38
- .lp-row-move[data-v-7884aed8] {
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 } from "../chunks/LpContextMenu-D0mzXqA7.js";
4
- import { _ as _sfc_main$3 } from "../chunks/LpIcon-CCnX5_2j.js";
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-DMOyAUOh.css';const _hoisted_1 = { class: "w-full border-collapse text-sm" };
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(), createElementBlock("button", {
214
+ col.sortable ? (openBlock(), createBlock(_sfc_main$3, {
183
215
  key: 0,
184
- type: "button",
185
- 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", [
186
- col.align === "right" ? "flex-row-reverse" : "",
187
- activeSort.value?.key === col.key ? "text-ink" : ""
188
- ]]),
189
- onClick: ($event) => toggleSort(col)
190
- }, [
191
- createTextVNode(toDisplayString(col.label) + " ", 1),
192
- createVNode(_sfc_main$3, {
193
- name: sortIcon(col),
194
- size: 13,
195
- class: normalizeClass([
196
- "shrink-0 transition-[transform,opacity] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)]",
197
- activeSort.value?.key === col.key ? "opacity-100" : "opacity-40 group-hover/sort:opacity-70"
198
- ])
199
- }, null, 8, ["name", "class"])
200
- ], 10, _hoisted_5)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
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$3, {
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$4, {
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-7884aed8"]]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.29.1",
3
+ "version": "0.29.3",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -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
- .lp-row-leave-active {
379
- position: absolute;
380
- width: 100%;
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>