@nexub/react 6.1.0 → 6.3.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
@@ -220,6 +220,14 @@ export { Fieldset } from './components/Fieldset/index.js';
220
220
  export type { FieldsetProps } from './components/Fieldset/index.js';
221
221
  export { InlineEdit } from './components/InlineEdit/index.js';
222
222
  export type { InlineEditProps } from './components/InlineEdit/index.js';
223
+ export { TimePicker } from './components/TimePicker/index.js';
224
+ export type { TimePickerProps } from './components/TimePicker/index.js';
225
+ export { DateTimePicker } from './components/DateTimePicker/index.js';
226
+ export type { DateTimePickerProps } from './components/DateTimePicker/index.js';
227
+ export { EventCalendar } from './components/EventCalendar/index.js';
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';
223
231
  export type { OTPInputProps } from './components/OTPInput/index.js';
224
232
  export { DatePicker } from './components/DatePicker/index.js';
225
233
  export type { DatePickerProps } from './components/DatePicker/index.js';
package/dist/index.js CHANGED
@@ -124,6 +124,10 @@ import { OTPInput } from "./components/OTPInput/index.js";
124
124
  import { Form, useForm } from "./components/Form/index.js";
125
125
  import { Fieldset } from "./components/Fieldset/index.js";
126
126
  import { InlineEdit } from "./components/InlineEdit/index.js";
127
+ import { TimePicker } from "./components/TimePicker/index.js";
128
+ import { DateTimePicker } from "./components/DateTimePicker/index.js";
129
+ import { EventCalendar } from "./components/EventCalendar/index.js";
130
+ import { TreeView } from "./components/TreeView/index.js";
127
131
  import { DatePicker } from "./components/DatePicker/index.js";
128
132
  import { DateRangePicker } from "./components/DateRangePicker/index.js";
129
133
  import { todayIn } from "./components/DateRangePicker/index.js";
@@ -169,6 +173,7 @@ export {
169
173
  DataTable,
170
174
  DatePicker,
171
175
  DateRangePicker,
176
+ DateTimePicker,
172
177
  Dialog,
173
178
  DiffView,
174
179
  DonutChart,
@@ -177,6 +182,7 @@ export {
177
182
  EmptyState,
178
183
  EmptyStatePreset,
179
184
  ErrorState,
185
+ EventCalendar,
180
186
  EyebrowChip,
181
187
  Fade,
182
188
  Fieldset,
@@ -266,6 +272,7 @@ export {
266
272
  ThemeProvider,
267
273
  ThemeScript,
268
274
  ThinkingIndicator,
275
+ TimePicker,
269
276
  Toast,
270
277
  Toaster,
271
278
  Toggle,
@@ -275,6 +282,7 @@ export {
275
282
  Tooltip,
276
283
  Provider as TooltipProvider,
277
284
  TopNav,
285
+ TreeView,
278
286
  VisuallyHidden,
279
287
  addonVariants,
280
288
  alertVariants,