@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.
- package/accessibility.json +1 -1
- package/dist/components/Badge/Badge.d.ts +9 -0
- package/dist/components/Badge/Badge.js +40 -2
- package/dist/components/Button/Button.d.ts +1 -1
- package/dist/components/CommandPalette/CommandPalette.d.ts +34 -13
- package/dist/components/CommandPalette/CommandPalette.js +174 -40
- package/dist/components/CommandPalette/index.d.ts +1 -1
- package/dist/components/DataTable/DataTable.d.ts +16 -2
- package/dist/components/DataTable/DataTable.js +131 -76
- package/dist/components/DataTable/ViewOptions.d.ts +15 -0
- package/dist/components/DataTable/ViewOptions.js +33 -0
- package/dist/components/DataTableVirtual/DataTableVirtual.d.ts +1 -1
- package/dist/components/Dialog/Dialog.d.ts +7 -0
- package/dist/components/Dialog/Dialog.js +35 -8
- package/dist/components/EmptyState/EmptyState.d.ts +7 -1
- package/dist/components/EmptyState/EmptyState.js +21 -5
- package/dist/components/EmptyState/EmptyStatePreset.d.ts +2 -1
- package/dist/components/FilterBuilder/FilterBuilder.js +33 -15
- package/dist/components/Heading/Heading.d.ts +1 -1
- package/dist/components/IconButton/IconButton.d.ts +5 -1
- package/dist/components/IconButton/IconButton.js +14 -3
- package/dist/components/Kanban/Kanban.d.ts +1 -1
- package/dist/components/Sortable/Sortable.d.ts +2 -2
- package/dist/components/Sortable/reorder.d.ts +10 -11
- package/dist/components/Sortable/reorder.js +185 -27
- package/dist/components/Tabs/Tabs.d.ts +5 -1
- package/dist/components/Tabs/Tabs.js +98 -35
- package/dist/components/Text/Text.d.ts +1 -1
- package/dist/components/Tooltip/Tooltip.d.ts +10 -3
- package/dist/components/Tooltip/Tooltip.js +16 -4
- package/dist/index.d.ts +2 -0
- package/dist/styles.css +1 -1
- 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
|
-
|
|
35
|
+
MemoGroupView,
|
|
29
36
|
{
|
|
30
|
-
group:
|
|
37
|
+
group: deferred,
|
|
31
38
|
fields,
|
|
32
39
|
depth: 0,
|
|
33
40
|
maxDepth,
|
|
34
|
-
onChange:
|
|
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 = (
|
|
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({ ...
|
|
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
|
|
73
|
-
const replace = (
|
|
74
|
-
const remove = () => setRules(
|
|
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([...
|
|
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([...
|
|
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({ ...
|
|
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({ ...
|
|
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({ ...
|
|
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:
|
|
138
|
-
onChange: (e) =>
|
|
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" | "
|
|
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" | "
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
60
|
-
if (
|
|
175
|
+
ref: (el) => {
|
|
176
|
+
if (el) listEls.current.set(list, el);
|
|
177
|
+
else listEls.current.delete(list);
|
|
61
178
|
},
|
|
62
|
-
|
|
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<
|
|
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
|
};
|