@nexub/react 6.2.0 → 6.4.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.
@@ -0,0 +1,253 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Check, ChevronRight, Minus } from "@nexub/icons";
5
+ import { cn } from "../../utils/cn.js";
6
+ import { useControllableState } from "../../hooks/useControllableState.js";
7
+ import { Spinner } from "../Spinner/Spinner.js";
8
+ const hasKids = (n) => Boolean(n.children?.length || n.hasChildren && !n.children);
9
+ const text = (n) => n.textValue ?? (typeof n.label === "string" ? n.label : "");
10
+ const descendants = (n) => (n.children ?? []).flatMap((c) => [c.id, ...descendants(c)]);
11
+ function TreeView({
12
+ nodes,
13
+ expanded: expandedProp,
14
+ defaultExpanded,
15
+ onExpandedChange,
16
+ selected: selectedProp,
17
+ defaultSelected,
18
+ onSelectedChange,
19
+ checkable = false,
20
+ checked: checkedProp,
21
+ defaultChecked,
22
+ onCheckedChange,
23
+ onLoadChildren,
24
+ className,
25
+ ...aria
26
+ }) {
27
+ const [expanded = [], setExpanded] = useControllableState({
28
+ value: expandedProp,
29
+ defaultValue: defaultExpanded ?? [],
30
+ onChange: onExpandedChange
31
+ });
32
+ const [selected = null, setSelected] = useControllableState({
33
+ value: selectedProp,
34
+ defaultValue: defaultSelected ?? null,
35
+ onChange: onSelectedChange
36
+ });
37
+ const [checked = [], setChecked] = useControllableState({
38
+ value: checkedProp,
39
+ defaultValue: defaultChecked ?? [],
40
+ onChange: onCheckedChange
41
+ });
42
+ const [loading, setLoading] = React.useState(null);
43
+ const [focused, setFocused] = React.useState(null);
44
+ const refs = React.useRef(/* @__PURE__ */ new Map());
45
+ const typed = React.useRef({ text: "", at: 0 });
46
+ const open = React.useMemo(() => new Set(expanded), [expanded]);
47
+ const checkedSet = React.useMemo(() => new Set(checked), [checked]);
48
+ const { all, visible } = React.useMemo(() => {
49
+ const all2 = /* @__PURE__ */ new Map();
50
+ const visible2 = [];
51
+ const walk = (list, level, parent, shown) => {
52
+ for (const node of list) {
53
+ const flat = { node, level, parent };
54
+ all2.set(node.id, flat);
55
+ if (shown) visible2.push(flat);
56
+ if (node.children) walk(node.children, level + 1, node.id, shown && open.has(node.id));
57
+ }
58
+ };
59
+ walk(nodes, 1, null, true);
60
+ return { all: all2, visible: visible2 };
61
+ }, [nodes, open]);
62
+ const tabStop = focused && visible.some((f) => f.node.id === focused) && focused || selected && visible.some((f) => f.node.id === selected) && selected || visible[0]?.node.id;
63
+ const focus = (id) => {
64
+ if (!id) return;
65
+ setFocused(id);
66
+ refs.current.get(id)?.focus();
67
+ };
68
+ const toggle = async (node, to = !open.has(node.id)) => {
69
+ if (!hasKids(node) || to === open.has(node.id)) return;
70
+ setExpanded(to ? [...expanded, node.id] : expanded.filter((id) => id !== node.id));
71
+ if (to && node.hasChildren && !node.children && onLoadChildren) {
72
+ setLoading(node.id);
73
+ try {
74
+ await onLoadChildren(node);
75
+ } finally {
76
+ setLoading((id) => id === node.id ? null : id);
77
+ }
78
+ }
79
+ };
80
+ const toggleCheck = (node) => {
81
+ const next = new Set(checkedSet);
82
+ const on = !next.has(node.id);
83
+ for (const id of [node.id, ...descendants(node)]) {
84
+ if (on) next.add(id);
85
+ else next.delete(id);
86
+ }
87
+ for (let p = all.get(node.id)?.parent; p; p = all.get(p)?.parent ?? null) {
88
+ const kids = all.get(p).node.children ?? [];
89
+ if (kids.every((k) => next.has(k.id))) next.add(p);
90
+ else next.delete(p);
91
+ }
92
+ setChecked([...next]);
93
+ };
94
+ const activate = (node) => {
95
+ if (node.disabled) return;
96
+ if (checkable) toggleCheck(node);
97
+ else setSelected(node.id);
98
+ };
99
+ const onKeyDown = (e, flat) => {
100
+ const { node } = flat;
101
+ const i = visible.findIndex((f) => f.node.id === node.id);
102
+ const rtl = getComputedStyle(e.currentTarget).direction === "rtl";
103
+ const flip = { ArrowLeft: "ArrowRight", ArrowRight: "ArrowLeft" };
104
+ const key = rtl ? flip[e.key] ?? e.key : e.key;
105
+ let handled = true;
106
+ switch (key) {
107
+ case "ArrowDown":
108
+ focus(visible[i + 1]?.node.id);
109
+ break;
110
+ case "ArrowUp":
111
+ focus(visible[i - 1]?.node.id);
112
+ break;
113
+ case "Home":
114
+ focus(visible[0]?.node.id);
115
+ break;
116
+ case "End":
117
+ focus(visible.at(-1)?.node.id);
118
+ break;
119
+ case "ArrowRight":
120
+ if (hasKids(node) && !open.has(node.id)) void toggle(node, true);
121
+ else if (open.has(node.id)) focus(node.children?.[0]?.id);
122
+ break;
123
+ case "ArrowLeft":
124
+ if (open.has(node.id)) void toggle(node, false);
125
+ else focus(flat.parent ?? void 0);
126
+ break;
127
+ case "Enter":
128
+ case " ":
129
+ activate(node);
130
+ break;
131
+ default: {
132
+ handled = false;
133
+ if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) break;
134
+ const now = Date.now();
135
+ const t = typed.current;
136
+ t.text = (now - t.at > 500 ? "" : t.text) + e.key.toLowerCase();
137
+ t.at = now;
138
+ const order = [...visible.slice(i + 1), ...visible.slice(0, i + 1)];
139
+ const hit = order.find((f) => text(f.node).toLowerCase().startsWith(t.text));
140
+ if (hit) {
141
+ focus(hit.node.id);
142
+ handled = true;
143
+ }
144
+ }
145
+ }
146
+ if (handled) {
147
+ e.preventDefault();
148
+ e.stopPropagation();
149
+ }
150
+ };
151
+ const render = (list, level, parent) => list.map((node, index) => {
152
+ const kids = hasKids(node);
153
+ const isOpen = open.has(node.id);
154
+ const isChecked = checkedSet.has(node.id);
155
+ const mixed = !isChecked && descendants(node).some((id) => checkedSet.has(id));
156
+ const flat = { node, level, parent };
157
+ return /* @__PURE__ */ jsxs(
158
+ "li",
159
+ {
160
+ ref: (el) => {
161
+ if (el) refs.current.set(node.id, el);
162
+ else refs.current.delete(node.id);
163
+ },
164
+ role: "treeitem",
165
+ "aria-level": level,
166
+ "aria-setsize": list.length,
167
+ "aria-posinset": index + 1,
168
+ "aria-expanded": kids ? isOpen : void 0,
169
+ "aria-selected": checkable ? void 0 : selected === node.id,
170
+ "aria-checked": checkable ? mixed ? "mixed" : isChecked : void 0,
171
+ "aria-disabled": node.disabled || void 0,
172
+ "aria-busy": loading === node.id || void 0,
173
+ tabIndex: tabStop === node.id ? 0 : -1,
174
+ onKeyDown: (e) => onKeyDown(e, flat),
175
+ onFocus: (e) => {
176
+ if (e.target === e.currentTarget) setFocused(node.id);
177
+ },
178
+ className: "outline-none [&:focus-visible>div]:outline [&:focus-visible>div]:outline-2 [&:focus-visible>div]:-outline-offset-2 [&:focus-visible>div]:outline-[var(--color-primary)]",
179
+ children: [
180
+ /* @__PURE__ */ jsxs(
181
+ "div",
182
+ {
183
+ ref: (el) => {
184
+ if (el) el.style.paddingInlineStart = `${(level - 1) * 1.25 + 0.25}rem`;
185
+ },
186
+ onClick: () => {
187
+ focus(node.id);
188
+ if (kids && !checkable) void toggle(node);
189
+ activate(node);
190
+ },
191
+ className: cn(
192
+ "flex min-h-control-sm cursor-default items-center gap-1.5 rounded-shape-control pe-2 text-body-md text-[var(--color-on-surface)]",
193
+ "hover:bg-[var(--color-surface-container-high)]",
194
+ !checkable && selected === node.id && "bg-[var(--color-secondary-container)] text-[var(--color-on-secondary-container)] hover:bg-[var(--color-secondary-container)]",
195
+ node.disabled && "cursor-not-allowed text-[var(--color-text-disabled)] hover:bg-transparent"
196
+ ),
197
+ children: [
198
+ /* @__PURE__ */ jsx(
199
+ "span",
200
+ {
201
+ "aria-hidden": "true",
202
+ onClick: (e) => {
203
+ if (!kids) return;
204
+ e.stopPropagation();
205
+ focus(node.id);
206
+ void toggle(node);
207
+ },
208
+ className: "inline-flex size-5 shrink-0 items-center justify-center text-[var(--color-on-surface-variant)]",
209
+ children: loading === node.id ? /* @__PURE__ */ jsx(Spinner, { size: "xs" }) : kids ? /* @__PURE__ */ jsx(
210
+ ChevronRight,
211
+ {
212
+ size: "xs",
213
+ className: cn("transition-transform rtl:-scale-x-100", isOpen && "rotate-90 rtl:-rotate-90")
214
+ }
215
+ ) : null
216
+ }
217
+ ),
218
+ checkable && /* @__PURE__ */ jsx(
219
+ "span",
220
+ {
221
+ "aria-hidden": "true",
222
+ className: cn(
223
+ "inline-flex size-4 shrink-0 items-center justify-center rounded-[4px] border-2",
224
+ isChecked || mixed ? "border-[var(--color-primary)] bg-[var(--color-primary)] text-[var(--color-on-primary)]" : "border-[var(--color-outline)]"
225
+ ),
226
+ children: isChecked ? /* @__PURE__ */ jsx(Check, { size: "xs" }) : mixed ? /* @__PURE__ */ jsx(Minus, { size: "xs" }) : null
227
+ }
228
+ ),
229
+ node.icon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "inline-flex shrink-0 text-[var(--color-on-surface-variant)]", children: node.icon }),
230
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: node.label })
231
+ ]
232
+ }
233
+ ),
234
+ kids && isOpen && node.children && /* @__PURE__ */ jsx("ul", { role: "group", className: "m-0 list-none p-0", children: render(node.children, level + 1, node.id) })
235
+ ]
236
+ },
237
+ node.id
238
+ );
239
+ });
240
+ return /* @__PURE__ */ jsx(
241
+ "ul",
242
+ {
243
+ role: "tree",
244
+ "aria-multiselectable": checkable || void 0,
245
+ className: cn("m-0 flex list-none flex-col p-0", className),
246
+ ...aria,
247
+ children: render(nodes, 1, null)
248
+ }
249
+ );
250
+ }
251
+ export {
252
+ TreeView
253
+ };
@@ -0,0 +1,2 @@
1
+ export { TreeView } from './TreeView.js';
2
+ export type { TreeViewProps, TreeNode } from './TreeView.js';
@@ -0,0 +1,4 @@
1
+ import { TreeView } from "./TreeView.js";
2
+ export {
3
+ TreeView
4
+ };
package/dist/index.d.ts CHANGED
@@ -226,6 +226,14 @@ export { DateTimePicker } from './components/DateTimePicker/index.js';
226
226
  export type { DateTimePickerProps } from './components/DateTimePicker/index.js';
227
227
  export { EventCalendar } from './components/EventCalendar/index.js';
228
228
  export type { EventCalendarProps, EventCalendarView, CalendarEvent, EventTone } from './components/EventCalendar/index.js';
229
+ export { TreeView } from './components/TreeView/index.js';
230
+ export type { TreeViewProps, TreeNode } from './components/TreeView/index.js';
231
+ export { ResizablePanels } from './components/ResizablePanels/index.js';
232
+ export type { ResizablePanelsProps } from './components/ResizablePanels/index.js';
233
+ export { Stepper } from './components/Stepper/index.js';
234
+ export type { StepperProps, StepperStep } from './components/Stepper/index.js';
235
+ export { ActionBar } from './components/ActionBar/index.js';
236
+ export type { ActionBarProps } from './components/ActionBar/index.js';
229
237
  export type { OTPInputProps } from './components/OTPInput/index.js';
230
238
  export { DatePicker } from './components/DatePicker/index.js';
231
239
  export type { DatePickerProps } from './components/DatePicker/index.js';
package/dist/index.js CHANGED
@@ -127,6 +127,10 @@ import { InlineEdit } from "./components/InlineEdit/index.js";
127
127
  import { TimePicker } from "./components/TimePicker/index.js";
128
128
  import { DateTimePicker } from "./components/DateTimePicker/index.js";
129
129
  import { EventCalendar } from "./components/EventCalendar/index.js";
130
+ import { TreeView } from "./components/TreeView/index.js";
131
+ import { ResizablePanels } from "./components/ResizablePanels/index.js";
132
+ import { Stepper } from "./components/Stepper/index.js";
133
+ import { ActionBar } from "./components/ActionBar/index.js";
130
134
  import { DatePicker } from "./components/DatePicker/index.js";
131
135
  import { DateRangePicker } from "./components/DateRangePicker/index.js";
132
136
  import { todayIn } from "./components/DateRangePicker/index.js";
@@ -134,6 +138,7 @@ import { ThemeProvider, ThemeScript, useTheme } from "./components/ThemeProvider
134
138
  import { Fade, Slide, Scale, Collapse, ListTransition, SkeletonSwap, usePrefersReducedMotion, Reveal, Float, Marquee } from "./components/Motion/index.js";
135
139
  export {
136
140
  Accordion,
141
+ ActionBar,
137
142
  Alert,
138
143
  AlertDialog,
139
144
  Amount,
@@ -238,6 +243,7 @@ export {
238
243
  RadioCardGroup,
239
244
  RadioGroup,
240
245
  Rating,
246
+ ResizablePanels,
241
247
  Reveal,
242
248
  SavedViews,
243
249
  Scale,
@@ -261,6 +267,7 @@ export {
261
267
  Spinner,
262
268
  SplitButton,
263
269
  Stack,
270
+ Stepper,
264
271
  StreamingText,
265
272
  Switch,
266
273
  Table,
@@ -281,6 +288,7 @@ export {
281
288
  Tooltip,
282
289
  Provider as TooltipProvider,
283
290
  TopNav,
291
+ TreeView,
284
292
  VisuallyHidden,
285
293
  addonVariants,
286
294
  alertVariants,