@nexub/react 7.5.0 → 7.7.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.
Files changed (33) hide show
  1. package/accessibility.json +1 -1
  2. package/dist/components/Badge/Badge.d.ts +9 -0
  3. package/dist/components/Badge/Badge.js +40 -2
  4. package/dist/components/Button/Button.d.ts +1 -1
  5. package/dist/components/CommandPalette/CommandPalette.d.ts +34 -13
  6. package/dist/components/CommandPalette/CommandPalette.js +174 -40
  7. package/dist/components/CommandPalette/index.d.ts +1 -1
  8. package/dist/components/DataTable/DataTable.d.ts +16 -2
  9. package/dist/components/DataTable/DataTable.js +131 -76
  10. package/dist/components/DataTable/ViewOptions.d.ts +15 -0
  11. package/dist/components/DataTable/ViewOptions.js +33 -0
  12. package/dist/components/DataTableVirtual/DataTableVirtual.d.ts +1 -1
  13. package/dist/components/Dialog/Dialog.d.ts +7 -0
  14. package/dist/components/Dialog/Dialog.js +35 -8
  15. package/dist/components/EmptyState/EmptyState.d.ts +7 -1
  16. package/dist/components/EmptyState/EmptyState.js +21 -5
  17. package/dist/components/EmptyState/EmptyStatePreset.d.ts +2 -1
  18. package/dist/components/FilterBuilder/FilterBuilder.js +33 -15
  19. package/dist/components/Heading/Heading.d.ts +1 -1
  20. package/dist/components/IconButton/IconButton.d.ts +5 -1
  21. package/dist/components/IconButton/IconButton.js +14 -3
  22. package/dist/components/Kanban/Kanban.d.ts +1 -1
  23. package/dist/components/Sortable/Sortable.d.ts +2 -2
  24. package/dist/components/Sortable/reorder.d.ts +10 -11
  25. package/dist/components/Sortable/reorder.js +185 -27
  26. package/dist/components/Tabs/Tabs.d.ts +5 -1
  27. package/dist/components/Tabs/Tabs.js +98 -35
  28. package/dist/components/Text/Text.d.ts +1 -1
  29. package/dist/components/Tooltip/Tooltip.d.ts +10 -3
  30. package/dist/components/Tooltip/Tooltip.js +16 -4
  31. package/dist/index.d.ts +2 -0
  32. package/dist/styles.css +1 -1
  33. package/package.json +8 -7
@@ -12,6 +12,7 @@ import {
12
12
  } from "@nexub/core";
13
13
  import { cn } from "../../utils/cn.js";
14
14
  import { useControllableState } from "../../hooks/useControllableState.js";
15
+ import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect.js";
15
16
  import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
16
17
  import { Button } from "../Button/Button.js";
17
18
  import { IconButton } from "../IconButton/IconButton.js";
@@ -24,14 +25,20 @@ function FilterBuilder({ fields, value: valueProp, defaultValue, onValueChange,
24
25
  defaultValue: defaultValue ?? emptyFilter(),
25
26
  onChange: onValueChange
26
27
  });
28
+ const latest = React.useRef(value);
29
+ useIsomorphicLayoutEffect(() => {
30
+ latest.current = value;
31
+ });
32
+ const update = React.useCallback((fn) => setValue(fn(latest.current)), [setValue]);
33
+ const deferred = React.useDeferredValue(value);
27
34
  return /* @__PURE__ */ jsx(
28
- GroupView,
35
+ MemoGroupView,
29
36
  {
30
- group: value,
37
+ group: deferred,
31
38
  fields,
32
39
  depth: 0,
33
40
  maxDepth,
34
- onChange: setValue,
41
+ onChange: update,
35
42
  className
36
43
  }
37
44
  );
@@ -40,7 +47,7 @@ FilterBuilder.displayName = "FilterBuilder";
40
47
  function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, className }) {
41
48
  const t = useNexubMessages();
42
49
  const first = fields[0];
43
- const setRules = (rules) => onChange({ ...group, rules });
50
+ const setRules = (fn) => onChange((g) => ({ ...g, rules: fn(g.rules) }));
44
51
  const newRule = () => newFilterRule(first);
45
52
  return /* @__PURE__ */ jsxs(
46
53
  "div",
@@ -60,7 +67,7 @@ function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, classNa
60
67
  size: "sm",
61
68
  "aria-label": t.filterGroup(group.combinator),
62
69
  value: group.combinator,
63
- onValueChange: (v) => v && onChange({ ...group, combinator: v }),
70
+ onValueChange: (v) => v && onChange((g) => ({ ...g, combinator: v })),
64
71
  children: [
65
72
  /* @__PURE__ */ jsx(SegmentedControl.Item, { value: "and", children: t.matchAll }),
66
73
  /* @__PURE__ */ jsx(SegmentedControl.Item, { value: "or", children: t.matchAny })
@@ -69,13 +76,13 @@ function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, classNa
69
76
  ),
70
77
  onRemove && /* @__PURE__ */ jsx(IconButton, { label: t.removeGroup, variant: "ghost", size: "sm", className: "ms-auto", onClick: onRemove, children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) })
71
78
  ] }),
72
- group.rules.map((r, i) => {
73
- const replace = (next) => setRules(group.rules.map((x, j) => j === i ? next : x));
74
- const remove = () => setRules(group.rules.filter((_, j) => j !== i));
79
+ group.rules.map((r) => {
80
+ const replace = (fn) => setRules((rules) => rules.map((x) => x.id === r.id ? fn(x) : x));
81
+ const remove = () => setRules((rules) => rules.filter((x) => x.id !== r.id));
75
82
  return isFilterGroup(r) ? /* @__PURE__ */ jsx(GroupView, { group: r, fields, depth: depth + 1, maxDepth, onChange: replace, onRemove: remove }, r.id) : /* @__PURE__ */ jsx(RuleRow, { rule: r, fields, onChange: replace, onRemove: remove }, r.id);
76
83
  }),
77
84
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
78
- /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setRules([...group.rules, newRule()]), disabled: !first, children: [
85
+ /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setRules((rules) => [...rules, newRule()]), disabled: !first, children: [
79
86
  /* @__PURE__ */ jsx(Plus, { size: "sm", "aria-hidden": "true" }),
80
87
  t.addFilter
81
88
  ] }),
@@ -84,7 +91,7 @@ function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, classNa
84
91
  {
85
92
  variant: "ghost",
86
93
  size: "sm",
87
- onClick: () => setRules([...group.rules, { id: uid(), combinator: "and", rules: [newRule()] }]),
94
+ onClick: () => setRules((rules) => [...rules, { id: uid(), combinator: "and", rules: [newRule()] }]),
88
95
  disabled: !first,
89
96
  children: [
90
97
  /* @__PURE__ */ jsx(Plus, { size: "sm", "aria-hidden": "true" }),
@@ -97,6 +104,7 @@ function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, classNa
97
104
  }
98
105
  );
99
106
  }
107
+ const MemoGroupView = React.memo(GroupView);
100
108
  function RuleRow({
101
109
  rule,
102
110
  fields,
@@ -104,6 +112,12 @@ function RuleRow({
104
112
  onRemove
105
113
  }) {
106
114
  const t = useNexubMessages();
115
+ const [draft, setDraft] = React.useState(rule.value);
116
+ const [seen, setSeen] = React.useState(rule.value);
117
+ if (rule.value !== seen) {
118
+ setSeen(rule.value);
119
+ setDraft(rule.value);
120
+ }
107
121
  const field = fields.find((f) => f.id === rule.field) ?? fields[0];
108
122
  const ops = filterOperators[field?.type ?? "text"];
109
123
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
@@ -114,7 +128,7 @@ function RuleRow({
114
128
  onValueChange: (id) => {
115
129
  const f = fields.find((x) => x.id === id);
116
130
  const nextOps = filterOperators[f?.type ?? "text"];
117
- onChange({ ...rule, field: id, operator: nextOps.includes(rule.operator) ? rule.operator : nextOps[0], value: "" });
131
+ onChange((r) => ({ ...r, field: id, operator: nextOps.includes(r.operator) ? r.operator : nextOps[0], value: "" }));
118
132
  },
119
133
  children: [
120
134
  /* @__PURE__ */ jsx(Select.Trigger, { "aria-label": t.filterField, className: "w-auto min-w-36", children: /* @__PURE__ */ jsx(Select.Value, {}) }),
@@ -122,11 +136,11 @@ function RuleRow({
122
136
  ]
123
137
  }
124
138
  ),
125
- /* @__PURE__ */ jsxs(Select.Root, { value: rule.operator, onValueChange: (op) => onChange({ ...rule, operator: op }), children: [
139
+ /* @__PURE__ */ jsxs(Select.Root, { value: rule.operator, onValueChange: (op) => onChange((r) => ({ ...r, operator: op })), children: [
126
140
  /* @__PURE__ */ jsx(Select.Trigger, { "aria-label": t.filterOperator, className: "w-auto min-w-32", children: /* @__PURE__ */ jsx(Select.Value, {}) }),
127
141
  /* @__PURE__ */ jsx(Select.Content, { children: ops.map((op) => /* @__PURE__ */ jsx(Select.Item, { value: op, children: t.filterOperators[op] }, op)) })
128
142
  ] }),
129
- rule.operator !== "isEmpty" && (field?.type === "select" ? /* @__PURE__ */ jsxs(Select.Root, { value: rule.value || void 0, onValueChange: (v) => onChange({ ...rule, value: v }), children: [
143
+ rule.operator !== "isEmpty" && (field?.type === "select" ? /* @__PURE__ */ jsxs(Select.Root, { value: rule.value || void 0, onValueChange: (v) => onChange((r) => ({ ...r, value: v })), children: [
130
144
  /* @__PURE__ */ jsx(Select.Trigger, { "aria-label": t.filterValue, className: "w-auto min-w-36", children: /* @__PURE__ */ jsx(Select.Value, {}) }),
131
145
  /* @__PURE__ */ jsx(Select.Content, { children: field.options?.map((o) => /* @__PURE__ */ jsx(Select.Item, { value: o.value, children: o.label }, o.value)) })
132
146
  ] }) : /* @__PURE__ */ jsx(
@@ -134,8 +148,12 @@ function RuleRow({
134
148
  {
135
149
  "aria-label": t.filterValue,
136
150
  type: field?.type === "number" ? "number" : field?.type === "date" ? "date" : "text",
137
- value: rule.value,
138
- onChange: (e) => onChange({ ...rule, value: e.target.value }),
151
+ value: draft,
152
+ onChange: (e) => {
153
+ const v = e.target.value;
154
+ setDraft(v);
155
+ onChange((r) => ({ ...r, value: v }));
156
+ },
139
157
  wrapperClassName: "w-auto min-w-36 flex-1"
140
158
  }
141
159
  )),
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import type { ToneValue } from '../Button/Button.js';
3
3
  export declare const headingVariants: (props?: ({
4
- size?: "display" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | null | undefined;
4
+ size?: "display" | "h2" | "h3" | "h1" | "h4" | "h5" | "h6" | null | undefined;
5
5
  weight?: "bold" | "regular" | "medium" | "semibold" | "extrabold" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  type HeadingSize = 'display' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
@@ -1,11 +1,15 @@
1
1
  import * as React from 'react';
2
2
  import { type VariantProps } from 'class-variance-authority';
3
3
  export declare const iconButtonVariants: (props?: ({
4
- variant?: "outline" | "solid" | "ghost" | "subtle" | null | undefined;
4
+ variant?: "outline" | "solid" | "subtle" | "ghost" | null | undefined;
5
5
  size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof iconButtonVariants> {
8
8
  label: string;
9
9
  asChild?: boolean;
10
+ /** Spinner replaces the icon (size kept); clicks are blocked, focus stays. */
11
+ loading?: boolean;
12
+ /** Built-in tooltip: content, or `true` to show `label`. No Provider needed. */
13
+ tooltip?: React.ReactNode;
10
14
  }
11
15
  export declare const IconButton: React.ForwardRefExoticComponent<IconButtonProps & React.RefAttributes<HTMLButtonElement>>;
@@ -1,9 +1,12 @@
1
+ "use client";
1
2
  import { jsx } from "react/jsx-runtime";
2
3
  import * as React from "react";
3
4
  import { Slot } from "@radix-ui/react-slot";
4
5
  import { cva } from "class-variance-authority";
5
6
  import { cn } from "../../utils/cn.js";
6
7
  import { disabledContainer, disabledText, pressable } from "../../utils/control.js";
8
+ import { spinnerVariants } from "../Spinner/Spinner.js";
9
+ const LazyTooltip = React.lazy(() => import("../Tooltip/Tooltip.js").then((m) => ({ default: m.Tooltip })));
7
10
  const iconButtonVariants = cva(
8
11
  [pressable, "relative inline-flex items-center justify-center flex-shrink-0"],
9
12
  {
@@ -30,18 +33,26 @@ const iconButtonVariants = cva(
30
33
  }
31
34
  );
32
35
  const IconButton = React.forwardRef(
33
- ({ className, variant, size, label, asChild = false, children, ...props }, ref) => {
36
+ ({ className, variant, size, label, asChild = false, loading, tooltip, onClick, children, ...props }, ref) => {
34
37
  const Comp = asChild ? Slot : "button";
35
- return /* @__PURE__ */ jsx(
38
+ const button = /* @__PURE__ */ jsx(
36
39
  Comp,
37
40
  {
38
41
  ref,
39
42
  "aria-label": label,
43
+ "aria-busy": loading || void 0,
44
+ "aria-disabled": loading || props["aria-disabled"] || void 0,
40
45
  className: cn(iconButtonVariants({ variant, size }), "nx-hit", className),
46
+ onClick: (e) => {
47
+ if (loading) return e.preventDefault();
48
+ onClick?.(e);
49
+ },
41
50
  ...props,
42
- children
51
+ children: loading && !asChild ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn(spinnerVariants({ size: size === "xs" ? "xs" : "sm" }), "border-current border-t-transparent") }) : children
43
52
  }
44
53
  );
54
+ if (tooltip == null || tooltip === false) return button;
55
+ return /* @__PURE__ */ jsx(React.Suspense, { fallback: button, children: /* @__PURE__ */ jsx(LazyTooltip, { label: tooltip === true ? label : tooltip, children: button }) });
45
56
  }
46
57
  );
47
58
  IconButton.displayName = "IconButton";
@@ -14,7 +14,7 @@ export interface KanbanProps<T> {
14
14
  className?: string;
15
15
  }
16
16
  /**
17
- * Columns of cards people can move: drag a card, or focus its handle and press
17
+ * Columns of cards people can move: drag a card's handle (Escape cancels), or focus it and press
18
18
  * Alt+Arrow keys (up/down within a column, left/right across columns).
19
19
  */
20
20
  export declare function Kanban<T>({ columns, getId, getLabel, onChange, renderCard, className }: KanbanProps<T>): React.JSX.Element;
@@ -12,7 +12,7 @@ export interface SortableProps<T> {
12
12
  className?: string;
13
13
  }
14
14
  /**
15
- * A list people can reorder: drag an item, or focus its handle and press
16
- * Alt+ArrowUp / Alt+ArrowDown. Each move is announced.
15
+ * A list people can reorder: drag an item's handle (mouse, pen or touch; Escape
16
+ * cancels), or focus the handle and press Alt+ArrowUp / Alt+ArrowDown. Each move is announced.
17
17
  */
18
18
  export declare function Sortable<T>({ items, getId, getLabel, onReorder, children, className, ...aria }: SortableProps<T>): React.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- /** Internal: lists of items, moved by drag (native) or Alt+Arrow on a handle. */
2
+ /** Internal: lists of items, moved by dragging a handle (Pointer Events) or Alt+Arrow on it. */
3
3
  export type Pos = [list: number, index: number];
4
4
  export declare function moveItem<T>(lists: T[][], [fc, fi]: Pos, [tc, ti]: Pos): T[][];
5
5
  interface ReorderOptions<T> {
@@ -10,18 +10,17 @@ interface ReorderOptions<T> {
10
10
  listLabels?: string[];
11
11
  onMove: (next: T[][]) => void;
12
12
  }
13
- /** Shared engine: drag state, keyboard moves, focus after a move, announcements. */
13
+ type ItemProps = {
14
+ 'data-nx-reorder-item': '';
15
+ 'data-nx-dragging'?: '';
16
+ 'data-nx-drop'?: 'before' | 'after';
17
+ };
18
+ /** Shared engine: pointer drag, keyboard moves, focus after a move, FLIP settle, announcements. */
14
19
  export declare function useReorder<T>({ lists, getId, getLabel, listLabels, onMove }: ReorderOptions<T>): {
15
- itemProps: (pos: Pos) => {
16
- draggable: boolean;
17
- onDragStart: (e: React.DragEvent) => void;
18
- onDragOver: (e: React.DragEvent) => void;
19
- onDrop: (e: React.DragEvent) => void;
20
- onDragEnd: () => void;
21
- };
20
+ itemProps: (pos: Pos) => ItemProps;
22
21
  listProps: (list: number) => {
23
- onDragOver: (e: React.DragEvent) => void;
24
- onDrop: (e: React.DragEvent) => void;
22
+ ref: (el: HTMLElement | null) => void;
23
+ 'data-nx-drop': string | undefined;
25
24
  };
26
25
  handle: (pos: Pos, className?: string) => React.JSX.Element;
27
26
  live: React.JSX.Element;
@@ -11,18 +11,69 @@ function moveItem(lists, [fc, fi], [tc, ti]) {
11
11
  next[tc].splice(Math.min(ti, next[tc].length), 0, item);
12
12
  return next;
13
13
  }
14
+ const THRESHOLD = 4;
15
+ const EDGE = 48;
16
+ const MAX_SPEED = 16;
17
+ const itemOf = (el) => el.closest("[data-nx-reorder-item]");
18
+ function scrollParent(el, axis) {
19
+ for (let p = el.parentElement; p; p = p.parentElement) {
20
+ const s = getComputedStyle(p);
21
+ const scrolls = axis === "y" ? /auto|scroll/.test(s.overflowY) && p.scrollHeight > p.clientHeight : /auto|scroll/.test(s.overflowX) && p.scrollWidth > p.clientWidth;
22
+ if (scrolls) return p;
23
+ }
24
+ return document.scrollingElement;
25
+ }
26
+ function edgeSpeed(at, start, end) {
27
+ if (at < start + EDGE) return -MAX_SPEED * Math.min(1, (start + EDGE - at) / EDGE);
28
+ if (at > end - EDGE) return MAX_SPEED * Math.min(1, (at - end + EDGE) / EDGE);
29
+ return 0;
30
+ }
31
+ const sameMark = (a, b) => a === b || !!a && !!b && a.edge === b.edge && ("id" in a ? "id" in b && a.id === b.id : "list" in b && a.list === b.list);
14
32
  function useReorder({ lists, getId, getLabel, listLabels, onMove }) {
15
33
  const t = useNexubMessages();
16
34
  const { dir } = useLocale();
17
- const dragged = React.useRef(null);
18
35
  const handles = React.useRef(/* @__PURE__ */ new Map());
36
+ const listEls = React.useRef(/* @__PURE__ */ new Map());
19
37
  const refocus = React.useRef(null);
38
+ const drag = React.useRef(null);
39
+ const flipFrom = React.useRef(null);
20
40
  const [announcement, setAnnouncement] = React.useState("");
41
+ const [dragging, setDragging] = React.useState(null);
42
+ const [mark, setMark] = React.useState(null);
43
+ const itemCache = React.useRef(/* @__PURE__ */ new Map());
21
44
  const hintId = React.useId();
22
45
  React.useEffect(() => {
23
46
  if (refocus.current) handles.current.get(refocus.current)?.focus();
24
47
  refocus.current = null;
25
48
  });
49
+ React.useLayoutEffect(() => {
50
+ const from = flipFrom.current;
51
+ flipFrom.current = null;
52
+ if (!from || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
53
+ for (const [id, h] of handles.current) {
54
+ const el = itemOf(h);
55
+ const old = from.get(id);
56
+ if (!el || !old) continue;
57
+ const now = el.getBoundingClientRect();
58
+ const dx = old.left - now.left;
59
+ const dy = old.top - now.top;
60
+ if (!dx && !dy) continue;
61
+ el.style.transition = "none";
62
+ el.style.transform = `translate(${dx}px, ${dy}px)`;
63
+ el.getBoundingClientRect();
64
+ el.style.transition = "transform var(--motion-spring-spatial-default-duration) var(--motion-spring-spatial-default-easing)";
65
+ el.style.transform = "";
66
+ el.addEventListener("transitionend", () => el.style.transition = "", { once: true });
67
+ }
68
+ });
69
+ const snapshot = () => {
70
+ const rects = /* @__PURE__ */ new Map();
71
+ for (const [id, h] of handles.current) {
72
+ const el = itemOf(h);
73
+ if (el) rects.set(id, el.getBoundingClientRect());
74
+ }
75
+ return rects;
76
+ };
26
77
  const move = (from, to, keepFocus) => {
27
78
  if (from[0] === to[0] && from[1] === to[1]) return;
28
79
  const item = lists[from[0]][from[1]];
@@ -30,40 +81,102 @@ function useReorder({ lists, getId, getLabel, listLabels, onMove }) {
30
81
  const [tc] = to;
31
82
  const ti = next[tc].indexOf(item);
32
83
  if (keepFocus) refocus.current = getId(item);
84
+ flipFrom.current ??= snapshot();
33
85
  onMove(next);
34
86
  const total = next[tc].length;
35
87
  setAnnouncement(
36
88
  from[0] !== tc && listLabels?.[tc] ? t.itemMovedToColumn(getLabel(item), listLabels[tc], ti + 1, total) : t.itemMoved(getLabel(item), ti + 1, total)
37
89
  );
38
90
  };
39
- const itemProps = (pos) => ({
40
- draggable: true,
41
- onDragStart: (e) => {
42
- dragged.current = pos;
43
- e.dataTransfer.effectAllowed = "move";
44
- },
45
- onDragOver: (e) => {
46
- if (dragged.current) e.preventDefault();
47
- },
48
- onDrop: (e) => {
49
- e.preventDefault();
50
- e.stopPropagation();
51
- if (dragged.current) move(dragged.current, pos, false);
52
- dragged.current = null;
53
- },
54
- onDragEnd: () => {
55
- dragged.current = null;
91
+ const targetAt = (d) => {
92
+ let list = d.from[0];
93
+ if (lists.length > 1) {
94
+ let best = Infinity;
95
+ for (const [l, el] of listEls.current) {
96
+ const r = el.getBoundingClientRect();
97
+ const dist = Math.hypot(Math.max(r.left - d.x, 0, d.x - r.right), Math.max(r.top - d.y, 0, d.y - r.bottom));
98
+ if (dist < best) [best, list] = [dist, l];
99
+ }
56
100
  }
57
- });
101
+ const others = lists[list].filter((it) => getId(it) !== d.id);
102
+ let k = others.findIndex((it) => {
103
+ const h = handles.current.get(getId(it));
104
+ const r = h && itemOf(h)?.getBoundingClientRect();
105
+ return r && d.y < r.top + r.height / 2;
106
+ });
107
+ if (k < 0) k = others.length;
108
+ const to = [list, k];
109
+ if (list === d.from[0] && k === d.from[1]) return { to, mark: null };
110
+ const m = k < others.length ? { id: getId(others[k]), edge: "before" } : others.length ? { id: getId(others[others.length - 1]), edge: "after" } : { list, edge: "inside" };
111
+ return { to, mark: m };
112
+ };
113
+ const update = (d) => {
114
+ if (d.el && d.rect0) {
115
+ const r = d.el.getBoundingClientRect();
116
+ d.tx = d.x - d.x0 - (r.left - d.tx - d.rect0.left);
117
+ d.ty = d.y - d.y0 - (r.top - d.ty - d.rect0.top);
118
+ d.el.style.translate = `${d.tx}px ${d.ty}px`;
119
+ }
120
+ const { to, mark: m } = targetAt(d);
121
+ d.target = m ? to : null;
122
+ if (!sameMark(d.mark, m)) setMark(d.mark = m);
123
+ };
124
+ const tick = () => {
125
+ const d = drag.current;
126
+ if (!d?.active) return;
127
+ let scrolled = false;
128
+ d.scrollers.forEach((s, i) => {
129
+ if (!s) return;
130
+ const page = s === document.scrollingElement;
131
+ const r = page ? { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight } : s.getBoundingClientRect();
132
+ const v = i === 0 ? edgeSpeed(d.x, r.left, r.right) : edgeSpeed(d.y, r.top, r.bottom);
133
+ if (!v) return;
134
+ const before = i === 0 ? s.scrollLeft : s.scrollTop;
135
+ if (i === 0) s.scrollLeft += v;
136
+ else s.scrollTop += v;
137
+ scrolled ||= (i === 0 ? s.scrollLeft : s.scrollTop) !== before;
138
+ });
139
+ if (scrolled) update(d);
140
+ d.raf = requestAnimationFrame(tick);
141
+ };
142
+ const finish = (commit) => {
143
+ const d = drag.current;
144
+ if (!d) return;
145
+ drag.current = null;
146
+ window.removeEventListener("keydown", d.onKey, true);
147
+ if (!d.active) return;
148
+ cancelAnimationFrame(d.raf);
149
+ flipFrom.current = snapshot();
150
+ if (d.el) d.el.style.translate = "";
151
+ if (commit && d.target) move(d.from, d.target, false);
152
+ setDragging(null);
153
+ setMark(null);
154
+ };
155
+ React.useEffect(() => () => finish(false), []);
156
+ const begin = (d) => {
157
+ d.active = true;
158
+ d.el = itemOf(d.handle) ?? d.handle;
159
+ d.rect0 = d.el.getBoundingClientRect();
160
+ d.scrollers = [scrollParent(d.el, "x"), scrollParent(d.el, "y")];
161
+ setDragging(d.id);
162
+ d.raf = requestAnimationFrame(tick);
163
+ };
164
+ const itemProps = (pos) => {
165
+ const id = getId(lists[pos[0]][pos[1]]);
166
+ const lifted = dragging === id ? "" : void 0;
167
+ const drop = mark && "id" in mark && mark.id === id ? mark.edge : void 0;
168
+ const prev = itemCache.current.get(id);
169
+ if (prev && prev["data-nx-dragging"] === lifted && prev["data-nx-drop"] === drop) return prev;
170
+ const next = { "data-nx-reorder-item": "", "data-nx-dragging": lifted, "data-nx-drop": drop };
171
+ itemCache.current.set(id, next);
172
+ return next;
173
+ };
58
174
  const listProps = (list) => ({
59
- onDragOver: (e) => {
60
- if (dragged.current) e.preventDefault();
175
+ ref: (el) => {
176
+ if (el) listEls.current.set(list, el);
177
+ else listEls.current.delete(list);
61
178
  },
62
- onDrop: (e) => {
63
- e.preventDefault();
64
- if (dragged.current) move(dragged.current, [list, lists[list].length], false);
65
- dragged.current = null;
66
- }
179
+ "data-nx-drop": mark && "list" in mark && mark.list === list ? "inside" : void 0
67
180
  });
68
181
  const handle = (pos, className) => {
69
182
  const item = lists[pos[0]][pos[1]];
@@ -78,6 +191,51 @@ function useReorder({ lists, getId, getLabel, listLabels, onMove }) {
78
191
  },
79
192
  "aria-label": t.moveItem(getLabel(item)),
80
193
  "aria-describedby": hintId,
194
+ onPointerDown: (e) => {
195
+ if (e.button !== 0 || drag.current) return;
196
+ const { clientX: x, clientY: y } = e;
197
+ const d = {
198
+ from: pos,
199
+ id,
200
+ handle: e.currentTarget,
201
+ x0: x,
202
+ y0: y,
203
+ x,
204
+ y,
205
+ active: false,
206
+ tx: 0,
207
+ ty: 0,
208
+ target: null,
209
+ mark: null,
210
+ scrollers: [null, null],
211
+ raf: 0,
212
+ onKey: (k) => {
213
+ if (k.key !== "Escape") return;
214
+ k.preventDefault();
215
+ k.stopPropagation();
216
+ finish(false);
217
+ }
218
+ };
219
+ drag.current = d;
220
+ window.addEventListener("keydown", d.onKey, true);
221
+ try {
222
+ e.currentTarget.setPointerCapture(e.pointerId);
223
+ } catch {
224
+ }
225
+ },
226
+ onPointerMove: (e) => {
227
+ const d = drag.current;
228
+ if (!d || d.id !== id) return;
229
+ d.x = e.clientX;
230
+ d.y = e.clientY;
231
+ if (!d.active) {
232
+ if (Math.hypot(d.x - d.x0, d.y - d.y0) < THRESHOLD) return;
233
+ begin(d);
234
+ }
235
+ update(d);
236
+ },
237
+ onPointerUp: () => finish(true),
238
+ onPointerCancel: () => finish(false),
81
239
  onKeyDown: (e) => {
82
240
  if (!e.altKey) return;
83
241
  const [c, i] = pos;
@@ -94,7 +252,7 @@ function useReorder({ lists, getId, getLabel, listLabels, onMove }) {
94
252
  move(pos, to, true);
95
253
  },
96
254
  className: cn(
97
- "inline-flex size-7 shrink-0 cursor-grab items-center justify-center rounded-shape-min text-[var(--color-on-surface-variant)]",
255
+ "inline-flex size-7 shrink-0 cursor-grab touch-none select-none items-center justify-center rounded-shape-min text-[var(--color-on-surface-variant)]",
98
256
  "hover:bg-[var(--color-surface-container-high)] active:cursor-grabbing",
99
257
  className
100
258
  ),
@@ -1,8 +1,12 @@
1
1
  import * as React from 'react';
2
2
  import * as RadixTabs from '@radix-ui/react-tabs';
3
+ export interface TabsListProps extends RadixTabs.TabsListProps {
4
+ /** `line` (default): underline. `pill`: a raised thumb slides behind the active tab. */
5
+ variant?: 'line' | 'pill';
6
+ }
3
7
  export declare const Tabs: {
4
8
  Root: React.ForwardRefExoticComponent<RadixTabs.TabsProps & React.RefAttributes<HTMLDivElement>>;
5
- List: React.ForwardRefExoticComponent<RadixTabs.TabsListProps & React.RefAttributes<HTMLDivElement>>;
9
+ List: React.ForwardRefExoticComponent<TabsListProps & React.RefAttributes<HTMLDivElement>>;
6
10
  Trigger: React.ForwardRefExoticComponent<RadixTabs.TabsTriggerProps & React.RefAttributes<HTMLButtonElement>>;
7
11
  Content: React.ForwardRefExoticComponent<RadixTabs.TabsContentProps & React.RefAttributes<HTMLDivElement>>;
8
12
  };