@leavepulse/ui 0.49.1 → 0.51.0

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,17 +11,17 @@
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-4fa70e89],
15
- .lp-row-leave-active[data-v-4fa70e89] {
14
+ .lp-row-enter-active[data-v-5877be26],
15
+ .lp-row-leave-active[data-v-5877be26] {
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-4fa70e89] {
20
+ .lp-row-move[data-v-5877be26] {
21
21
  transition: transform var(--duration-medium) var(--ease-emphasized);
22
22
  }
23
- .lp-row-enter-from[data-v-4fa70e89],
24
- .lp-row-leave-to[data-v-4fa70e89] {
23
+ .lp-row-enter-from[data-v-5877be26],
24
+ .lp-row-leave-to[data-v-5877be26] {
25
25
  opacity: 0;
26
26
  transform: translateX(-0.5rem);
27
27
  }
@@ -35,7 +35,7 @@
35
35
  * drops out of the table layout entirely. Nothing sits below this row to be
36
36
  * dragged around, so simply not animating its exit costs nothing.
37
37
  */
38
- .lp-row-empty.lp-row-leave-active[data-v-4fa70e89] {
38
+ .lp-row-empty.lp-row-leave-active[data-v-5877be26] {
39
39
  display: none;
40
40
  }
41
41
 
@@ -54,7 +54,7 @@
54
54
  * once anything is selected. An earlier version collapsed the column to nothing
55
55
  * and floated the hover box over the first cell, where it sat on the text.
56
56
  */
57
- .lp-tick[data-v-4fa70e89] {
57
+ .lp-tick[data-v-5877be26] {
58
58
  display: flex;
59
59
  align-items: center;
60
60
  max-width: 0;
@@ -66,17 +66,17 @@
66
66
  padding var(--duration-medium) var(--ease-emphasized),
67
67
  opacity var(--duration-fast) ease;
68
68
  }
69
- .lp-tick-open[data-v-4fa70e89] {
69
+ .lp-tick-open[data-v-5877be26] {
70
70
  max-width: 3.25rem;
71
71
  padding-inline: 1rem;
72
72
  opacity: 1;
73
73
  }
74
- .lp-tick-hidden[data-v-4fa70e89],
75
- .lp-tick-cue[data-v-4fa70e89] {
74
+ .lp-tick-hidden[data-v-5877be26],
75
+ .lp-tick-cue[data-v-5877be26] {
76
76
  opacity: 0;
77
77
  }
78
- tr:hover .lp-tick-cue[data-v-4fa70e89],
79
- .lp-tick-cue[data-v-4fa70e89]:focus-within {
78
+ tr:hover .lp-tick-cue[data-v-5877be26],
79
+ .lp-tick-cue[data-v-5877be26]:focus-within {
80
80
  opacity: 1;
81
81
  }
82
82
 
@@ -84,15 +84,15 @@ tr:hover .lp-tick-cue[data-v-4fa70e89],
84
84
  turned down the rows change without animating rather than not changing. */
85
85
  /* Only where there is a hover to reveal it; a finger long-presses instead. */
86
86
  @media (hover: none) {
87
- tr:hover .lp-tick-cue[data-v-4fa70e89] {
87
+ tr:hover .lp-tick-cue[data-v-5877be26] {
88
88
  opacity: 0;
89
89
  }
90
90
  }
91
91
  @media (prefers-reduced-motion: reduce) {
92
- .lp-row-enter-active[data-v-4fa70e89],
93
- .lp-row-leave-active[data-v-4fa70e89],
94
- .lp-row-move[data-v-4fa70e89],
95
- .lp-tick[data-v-4fa70e89] {
92
+ .lp-row-enter-active[data-v-5877be26],
93
+ .lp-row-leave-active[data-v-5877be26],
94
+ .lp-row-move[data-v-5877be26],
95
+ .lp-tick[data-v-5877be26] {
96
96
  transition: none;
97
97
  }
98
98
  }
@@ -1,4 +1,4 @@
1
- import { defineComponent, ref, watch, useId, computed, openBlock, createBlock, unref, withCtx, createVNode, normalizeClass, createElementBlock, Fragment, renderList, createCommentVNode, createElementVNode, toDisplayString, renderSlot } from "vue";
1
+ import { defineComponent, ref, watch, nextTick, useId, computed, openBlock, createBlock, unref, withCtx, createVNode, normalizeClass, createElementBlock, Fragment, renderList, createCommentVNode, createElementVNode, toDisplayString, renderSlot } from "vue";
2
2
  import { useReducedMotion, Motion } from "motion-v";
3
3
  import { TabsRoot, TabsList, TabsTrigger, TabsContent } from "reka-ui";
4
4
  import { usePillTransition } from "../composables/usePillTransition.js";
@@ -25,6 +25,19 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
25
25
  if (value !== void 0) current.value = value;
26
26
  }
27
27
  );
28
+ const list = ref(null);
29
+ watch(current, () => {
30
+ void nextTick(() => {
31
+ const row = list.value?.$el;
32
+ const active = row?.querySelector('[data-state="active"]');
33
+ if (!row || !active || row.scrollWidth <= row.clientWidth) return;
34
+ const start = active.offsetLeft - row.offsetLeft;
35
+ const end = start + active.offsetWidth;
36
+ if (start >= row.scrollLeft && end <= row.scrollLeft + row.clientWidth) return;
37
+ const left = start < row.scrollLeft ? start : end - row.clientWidth;
38
+ row.scrollTo({ left, behavior: reduceMotion.value ? "auto" : "smooth" });
39
+ });
40
+ });
28
41
  function select(value) {
29
42
  if (props.modelValue === void 0) current.value = value;
30
43
  emit("update:modelValue", value);
@@ -50,7 +63,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
50
63
  }, {
51
64
  default: withCtx(() => [
52
65
  createVNode(unref(TabsList), {
53
- class: normalizeClass(["gap-1", [
66
+ ref_key: "list",
67
+ ref: list,
68
+ class: normalizeClass(["max-w-full gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", [
54
69
  __props.block ? "flex w-full" : "inline-flex",
55
70
  __props.variant === "contained" ? "rounded-control border border-line bg-surface-soft p-1" : "",
56
71
  __props.variant === "underline" ? "border-b border-line" : ""
@@ -62,7 +77,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
62
77
  return openBlock(), createBlock(unref(TabsTrigger), {
63
78
  key: item.value,
64
79
  value: item.value,
65
- class: normalizeClass(["relative inline-flex items-center justify-center gap-1.5 text-sm outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring", [
80
+ class: normalizeClass(["relative inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap text-sm outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring", [
66
81
  __props.block ? "flex-1" : "",
67
82
  __props.variant === "underline" ? "rounded-t-md px-3 pt-1.5 pb-2" : "rounded-md px-3 py-1.5",
68
83
  __props.accent ? "data-[state=active]:text-brand" : "data-[state=active]:text-ink",
@@ -34,6 +34,13 @@ export interface TableColumn<Row> {
34
34
  /** Keep this column out of the show/hide list — an actions column has no
35
35
  * name to offer and nothing to gain from being hidden. */
36
36
  alwaysVisible?: boolean;
37
+ /**
38
+ * Leave the column out while the table is narrower than this many pixels
39
+ * (a phone, a narrow card), so the columns that matter keep their room
40
+ * instead of the whole table scrolling sideways. Wider than that, the
41
+ * user's own show/hide applies as usual.
42
+ */
43
+ hideBelow?: number;
37
44
  }
38
45
  export interface TableLabels {
39
46
  [key: string]: string;
@@ -18,7 +18,7 @@ import { _ as _sfc_main$1 } from "../chunks/LpButton-CKSwC6Mu.js";
18
18
  import { toCsv, downloadFile, fileStem } from "./exportData.js";
19
19
  import { summarizeShown, parseTableState, serializeTableState, countValues } from "./tableFilter.js";
20
20
  import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
21
- import '../assets/LpTable-xWwI4KPZ.css';const _hoisted_1 = {
21
+ import '../assets/LpTable-BAMDVi1u.css';const _hoisted_1 = {
22
22
  key: 0,
23
23
  class: "flex justify-end"
24
24
  };
@@ -43,19 +43,18 @@ const _hoisted_9 = {
43
43
  class: "flex items-center gap-2"
44
44
  };
45
45
  const _hoisted_10 = { class: "hidden sm:inline" };
46
- const _hoisted_11 = { class: "w-full border-collapse text-sm" };
47
- const _hoisted_12 = { class: "border-b border-line bg-surface-soft" };
48
- const _hoisted_13 = ["inert"];
49
- const _hoisted_14 = ["aria-sort"];
50
- const _hoisted_15 = ["onClick"];
51
- const _hoisted_16 = ["aria-label", "data-lp-filter"];
52
- const _hoisted_17 = {
46
+ const _hoisted_11 = { class: "border-b border-line bg-surface-soft" };
47
+ const _hoisted_12 = ["inert"];
48
+ const _hoisted_13 = ["aria-sort"];
49
+ const _hoisted_14 = ["onClick"];
50
+ const _hoisted_15 = ["aria-label", "data-lp-filter"];
51
+ const _hoisted_16 = {
53
52
  key: "lp-table-empty",
54
53
  class: "lp-row-empty"
55
54
  };
56
- const _hoisted_18 = ["colspan"];
57
- const _hoisted_19 = ["onContextmenu", "data-paint-key", "onClick", "onPointerdown"];
58
- const _hoisted_20 = ["onPointerdown"];
55
+ const _hoisted_17 = ["colspan"];
56
+ const _hoisted_18 = ["onContextmenu", "data-paint-key", "onClick", "onPointerdown"];
57
+ const _hoisted_19 = ["onPointerdown"];
59
58
  const __default__ = { inheritAttrs: false };
60
59
  const _sfc_main = /* @__PURE__ */ defineComponent({
61
60
  ...__default__,
@@ -203,8 +202,25 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
203
202
  change();
204
203
  }
205
204
  const hiddenColumns = ref(/* @__PURE__ */ new Set());
205
+ const tableEl = ref(null);
206
+ const tableWidth = ref(Number.POSITIVE_INFINITY);
207
+ let widthObserver;
208
+ watch(
209
+ tableEl,
210
+ (table) => {
211
+ widthObserver?.disconnect();
212
+ const el = table?.closest("[data-reka-scroll-area-viewport]") ?? table?.parentElement;
213
+ if (!el || typeof ResizeObserver === "undefined") return;
214
+ widthObserver = new ResizeObserver(() => tableWidth.value = el.clientWidth);
215
+ widthObserver.observe(el);
216
+ tableWidth.value = el.clientWidth;
217
+ },
218
+ { flush: "post" }
219
+ );
206
220
  const visibleColumns = computed(
207
- () => props.columns.filter((c) => !hiddenColumns.value.has(c.key))
221
+ () => props.columns.filter(
222
+ (c) => !hiddenColumns.value.has(c.key) && !(c.hideBelow && tableWidth.value < c.hideBelow)
223
+ )
208
224
  );
209
225
  function valuesOf(col) {
210
226
  if (props.load && facets.value[col.key]) return facets.value[col.key];
@@ -606,6 +622,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
606
622
  );
607
623
  onBeforeUnmount(() => {
608
624
  ro?.disconnect();
625
+ widthObserver?.disconnect();
609
626
  if (motionTimer) clearTimeout(motionTimer);
610
627
  });
611
628
  const barInsetTop = computed(
@@ -738,13 +755,17 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
738
755
  always: ""
739
756
  }, {
740
757
  default: withCtx(() => [
741
- createElementVNode("table", _hoisted_11, [
758
+ createElementVNode("table", {
759
+ ref_key: "tableEl",
760
+ ref: tableEl,
761
+ class: "w-full border-collapse text-sm"
762
+ }, [
742
763
  createElementVNode("thead", {
743
764
  ref_key: "headEl",
744
765
  ref: headEl,
745
766
  class: normalizeClass(__props.stickyHeader ? "sticky top-0 z-10 bg-surface-soft" : "")
746
767
  }, [
747
- createElementVNode("tr", _hoisted_12, [
768
+ createElementVNode("tr", _hoisted_11, [
748
769
  __props.selectable ? (openBlock(), createElementBlock("th", {
749
770
  key: 0,
750
771
  class: normalizeClass(["w-px p-0", cellY.value])
@@ -758,7 +779,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
758
779
  indeterminate: someChecked.value,
759
780
  "onUpdate:modelValue": toggleAll
760
781
  }, null, 8, ["model-value", "indeterminate"])
761
- ], 10, _hoisted_13)
782
+ ], 10, _hoisted_12)
762
783
  ], 2)) : createCommentVNode("", true),
763
784
  (openBlock(true), createElementBlock(Fragment, null, renderList(visibleColumns.value, (col) => {
764
785
  return openBlock(), createElementBlock("th", {
@@ -790,7 +811,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
790
811
  activeSort.value?.key === col.key ? "opacity-100" : "opacity-40 group-hover/sort:opacity-70"
791
812
  ])
792
813
  }, null, 8, ["name", "class"])) : createCommentVNode("", true)
793
- ], 10, _hoisted_15)
814
+ ], 10, _hoisted_14)
794
815
  ]),
795
816
  _: 2
796
817
  }, 1032, ["items"])) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
@@ -813,7 +834,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
813
834
  name: hiddenValues.value[col.key]?.size ? "lucide:filter-x" : "lucide:filter",
814
835
  size: 12
815
836
  }, null, 8, ["name"])
816
- ], 10, _hoisted_16)
837
+ ], 10, _hoisted_15)
817
838
  ]),
818
839
  default: withCtx(() => [
819
840
  createVNode(_sfc_main$9, {
@@ -825,7 +846,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
825
846
  ]),
826
847
  _: 2
827
848
  }, 1032, ["aria-label"])) : createCommentVNode("", true)
828
- ], 14, _hoisted_14);
849
+ ], 14, _hoisted_13);
829
850
  }), 128))
830
851
  ])
831
852
  ], 2),
@@ -837,7 +858,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
837
858
  ref: (el) => unref(roving).setContainer(el?.$el ?? null)
838
859
  }, {
839
860
  default: withCtx(() => [
840
- displayRows.value.length === 0 ? (openBlock(), createElementBlock("tr", _hoisted_17, [
861
+ displayRows.value.length === 0 ? (openBlock(), createElementBlock("tr", _hoisted_16, [
841
862
  createElementVNode("td", {
842
863
  colspan: colSpan.value,
843
864
  class: "px-4 py-10 text-center text-muted"
@@ -860,7 +881,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
860
881
  ]),
861
882
  _: 1
862
883
  })) : createCommentVNode("", true)
863
- ], 8, _hoisted_18)
884
+ ], 8, _hoisted_17)
864
885
  ])) : createCommentVNode("", true),
865
886
  (openBlock(true), createElementBlock(Fragment, null, renderList(displayRows.value, (row, index) => {
866
887
  return openBlock(), createElementBlock("tr", mergeProps({
@@ -888,7 +909,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
888
909
  "onUpdate:modelValue": (v) => toggleRow(keyFor(row, index), v)
889
910
  }, null, 8, ["model-value", "onUpdate:modelValue"])
890
911
  ], 2)
891
- ], 42, _hoisted_20)) : createCommentVNode("", true),
912
+ ], 42, _hoisted_19)) : createCommentVNode("", true),
892
913
  (openBlock(true), createElementBlock(Fragment, null, renderList(visibleColumns.value, (col, ci) => {
893
914
  return openBlock(), createElementBlock("td", {
894
915
  key: col.key,
@@ -902,12 +923,12 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
902
923
  ], true)
903
924
  ], 2);
904
925
  }), 128))
905
- ], 16, _hoisted_19);
926
+ ], 16, _hoisted_18);
906
927
  }), 128))
907
928
  ]),
908
929
  _: 3
909
930
  }, 8, ["css", "class"])
910
- ])
931
+ ], 512)
911
932
  ]),
912
933
  _: 3
913
934
  }, 8, ["items"])
@@ -926,7 +947,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
926
947
  };
927
948
  }
928
949
  });
929
- const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-4fa70e89"]]);
950
+ const LpTable = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-5877be26"]]);
930
951
  export {
931
952
  LpTable as default
932
953
  };
@@ -22,13 +22,13 @@ type __VLS_Props = {
22
22
  /** Stretch the bar to full width with equal-share triggers. */
23
23
  block?: boolean;
24
24
  };
25
- declare var __VLS_49: {
25
+ declare var __VLS_51: {
26
26
  value: string;
27
- }, __VLS_51: {};
27
+ }, __VLS_53: {};
28
28
  type __VLS_Slots = {} & {
29
- panel?: (props: typeof __VLS_49) => any;
29
+ panel?: (props: typeof __VLS_51) => any;
30
30
  } & {
31
- default?: (props: typeof __VLS_51) => any;
31
+ default?: (props: typeof __VLS_53) => any;
32
32
  };
33
33
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
34
34
  "update:modelValue": (value: string) => any;
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpTabs-29iW1XOx.js";
1
+ import { _ as _sfc_main } from "../chunks/LpTabs-UM0FQmPA.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
package/dist/index.js CHANGED
@@ -96,7 +96,7 @@ import { _ as _69 } from "./chunks/LpSwitch-OrcOJX9m.js";
96
96
  import { default as default12 } from "./components/LpTable.vue.js";
97
97
  import { compareValues } from "./components/tableSort.js";
98
98
  import { _ as _70 } from "./chunks/LpTableOfContents-BUcAfGPQ.js";
99
- import { _ as _71 } from "./chunks/LpTabs-29iW1XOx.js";
99
+ import { _ as _71 } from "./chunks/LpTabs-UM0FQmPA.js";
100
100
  import { _ as _72 } from "./chunks/LpTagsInput-D1SuqbhB.js";
101
101
  import { addTags, splitTags } from "./components/tags.js";
102
102
  import { _ as _73 } from "./chunks/LpTextarea-uuScWo6-.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.49.1",
3
+ "version": "0.51.0",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -59,6 +59,13 @@ export interface TableColumn<Row> {
59
59
  /** Keep this column out of the show/hide list — an actions column has no
60
60
  * name to offer and nothing to gain from being hidden. */
61
61
  alwaysVisible?: boolean
62
+ /**
63
+ * Leave the column out while the table is narrower than this many pixels
64
+ * (a phone, a narrow card), so the columns that matter keep their room
65
+ * instead of the whole table scrolling sideways. Wider than that, the
66
+ * user's own show/hide applies as usual.
67
+ */
68
+ hideBelow?: number
62
69
  }
63
70
 
64
71
  export interface TableLabels {
@@ -430,8 +437,30 @@ function withMotion(change: () => void) {
430
437
  }
431
438
  const hiddenColumns = ref<Set<string>>(new Set())
432
439
 
440
+ // The table's own width, for `hideBelow`: what decides is the room the table
441
+ // has, not the window, which a sidebar or a card can take most of.
442
+ // Measured on the scroll viewport around the table: that is the room it has,
443
+ // while the table itself widens with whatever it overflows by.
444
+ const tableEl = ref<HTMLElement | null>(null)
445
+ const tableWidth = ref(Number.POSITIVE_INFINITY)
446
+ let widthObserver: ResizeObserver | undefined
447
+ watch(
448
+ tableEl,
449
+ (table) => {
450
+ widthObserver?.disconnect()
451
+ const el = table?.closest<HTMLElement>("[data-reka-scroll-area-viewport]") ?? table?.parentElement
452
+ if (!el || typeof ResizeObserver === "undefined") return
453
+ widthObserver = new ResizeObserver(() => (tableWidth.value = el.clientWidth))
454
+ widthObserver.observe(el)
455
+ tableWidth.value = el.clientWidth
456
+ },
457
+ { flush: "post" },
458
+ )
459
+
433
460
  const visibleColumns = computed(() =>
434
- props.columns.filter((c) => !hiddenColumns.value.has(c.key)),
461
+ props.columns.filter(
462
+ (c) => !hiddenColumns.value.has(c.key) && !(c.hideBelow && tableWidth.value < c.hideBelow),
463
+ ),
435
464
  )
436
465
 
437
466
  /** The distinct values a filterable column holds, in first-seen order. */
@@ -997,6 +1026,7 @@ watch(
997
1026
  )
998
1027
  onBeforeUnmount(() => {
999
1028
  ro?.disconnect()
1029
+ widthObserver?.disconnect()
1000
1030
  if (motionTimer) clearTimeout(motionTimer)
1001
1031
  })
1002
1032
 
@@ -1111,7 +1141,7 @@ const barInsetTop = computed(() =>
1111
1141
  reaches: rows and headers carry their own and take the event first, so
1112
1142
  this is the fallback rather than a competitor. -->
1113
1143
  <LpContextMenu :items="contextMenu" always>
1114
- <table class="w-full border-collapse text-sm">
1144
+ <table ref="tableEl" class="w-full border-collapse text-sm">
1115
1145
  <!-- The background belongs on `thead` itself, not only on its row:
1116
1146
  `border-collapse` collapses the borders, and with a sticky header the
1117
1147
  rows show through that seam — a band of body text rides across the
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { Motion, useReducedMotion } from "motion-v"
3
3
  import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "reka-ui"
4
- import { computed, ref, useId, watch } from "vue"
4
+ import { computed, nextTick, ref, useId, watch } from "vue"
5
5
  import { usePillTransition } from "../composables/usePillTransition"
6
6
  import LpIcon from "./LpIcon.vue"
7
7
 
@@ -45,6 +45,23 @@ watch(
45
45
  },
46
46
  )
47
47
 
48
+ // A row wider than its container scrolls sideways (a phone, a narrow card)
49
+ // instead of wrapping each label onto two lines; the active tab is brought
50
+ // into view whenever it changes, including from outside.
51
+ const list = ref<{ $el: HTMLElement } | null>(null)
52
+ watch(current, () => {
53
+ void nextTick(() => {
54
+ const row = list.value?.$el
55
+ const active = row?.querySelector<HTMLElement>('[data-state="active"]')
56
+ if (!row || !active || row.scrollWidth <= row.clientWidth) return
57
+ const start = active.offsetLeft - row.offsetLeft
58
+ const end = start + active.offsetWidth
59
+ if (start >= row.scrollLeft && end <= row.scrollLeft + row.clientWidth) return
60
+ const left = start < row.scrollLeft ? start : end - row.clientWidth
61
+ row.scrollTo({ left, behavior: reduceMotion.value ? "auto" : "smooth" })
62
+ })
63
+ })
64
+
48
65
  function select(value: string) {
49
66
  if (props.modelValue === undefined) current.value = value
50
67
  emit("update:modelValue", value)
@@ -101,7 +118,8 @@ function releaseFocusAfterPointer(event: PointerEvent) {
101
118
  @update:model-value="(v) => select(v as string)"
102
119
  >
103
120
  <TabsList
104
- class="gap-1"
121
+ ref="list"
122
+ class="max-w-full gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
105
123
  :class="[
106
124
  block ? 'flex w-full' : 'inline-flex',
107
125
  variant === 'contained' ? 'rounded-control border border-line bg-surface-soft p-1' : '',
@@ -113,7 +131,7 @@ function releaseFocusAfterPointer(event: PointerEvent) {
113
131
  v-for="item in items"
114
132
  :key="item.value"
115
133
  :value="item.value"
116
- class="relative inline-flex items-center justify-center gap-1.5 text-sm outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring"
134
+ class="relative inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap text-sm outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring"
117
135
  :class="[
118
136
  block ? 'flex-1' : '',
119
137
  variant === 'underline' ? 'rounded-t-md px-3 pt-1.5 pb-2' : 'rounded-md px-3 py-1.5',