@tensor_1/react 2.6.0 → 2.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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ButtonHTMLAttributes, ReactNode, InputHTMLAttributes, TextareaHTMLAttributes, SelectHTMLAttributes, CSSProperties, HTMLAttributes, AnchorHTMLAttributes, TdHTMLAttributes, TableHTMLAttributes, ThHTMLAttributes } from 'react';
2
+ import { RefObject, ButtonHTMLAttributes, ReactNode, InputHTMLAttributes, TextareaHTMLAttributes, SelectHTMLAttributes, CSSProperties, HTMLAttributes, AnchorHTMLAttributes, TdHTMLAttributes, TableHTMLAttributes, ThHTMLAttributes } from 'react';
3
3
 
4
4
  declare function cx(...args: (string | false | null | undefined | 0)[]): string;
5
5
 
@@ -140,6 +140,82 @@ interface UseTableResult<T> {
140
140
  */
141
141
  declare function useTable<T extends Row>(data: readonly T[], options?: UseTableOptions): UseTableResult<T>;
142
142
 
143
+ /**
144
+ * uix overlay positioning — framework-agnostic, dependency-free anchored placement.
145
+ *
146
+ * Pure geometry (no DOM): given the anchor's rect, the floating element's size, and
147
+ * the viewport, compute where to put the floating element with two behaviours:
148
+ * • flip — if the preferred side doesn't fit, use the opposite side (or whichever
149
+ * side has more room);
150
+ * • shift — slide along the cross axis so the element stays within the viewport.
151
+ *
152
+ * The React hook (useAnchoredPosition) and the vanilla styleguide both feed this
153
+ * getBoundingClientRect() values, so anchored overlays land in the same place in every
154
+ * browser — instead of relying on CSS anchor() positioning, which is Chromium-only and
155
+ * silently detaches the overlay from its trigger everywhere else (UIX-FIX-02).
156
+ *
157
+ * Everything here is deterministic and unit-tested (see overlay-position.test.mjs).
158
+ */
159
+ type Side = 'top' | 'bottom' | 'left' | 'right';
160
+ type Align = 'start' | 'center' | 'end';
161
+ type Placement = Side | `${Side}-${Align}`;
162
+ /** A viewport-space rectangle, as returned by getBoundingClientRect(). */
163
+ interface Rect {
164
+ x: number;
165
+ y: number;
166
+ width: number;
167
+ height: number;
168
+ }
169
+ interface Size {
170
+ width: number;
171
+ height: number;
172
+ }
173
+ interface PositionOptions {
174
+ /** Preferred placement. Default `'bottom-start'`; a bare side (e.g. `'top'`) centers on the cross axis. */
175
+ placement?: Placement;
176
+ /** Gap between the anchor and the floating element, in px. Default 6. */
177
+ offset?: number;
178
+ /** Minimum gap kept from each viewport edge when shifting, in px. Default 8. */
179
+ padding?: number;
180
+ /** Flip to the opposite side when the preferred one doesn't fit. Default true. */
181
+ flip?: boolean;
182
+ /** Slide along the cross axis to stay on-screen. Default true. */
183
+ shift?: boolean;
184
+ }
185
+ interface PositionResult {
186
+ /** Viewport-space left, for `position: fixed`. */
187
+ x: number;
188
+ /** Viewport-space top, for `position: fixed`. */
189
+ y: number;
190
+ /** The placement actually used (after any flip). */
191
+ placement: Placement;
192
+ side: Side;
193
+ align: Align;
194
+ }
195
+ /**
196
+ * Compute the floating element's viewport-space position anchored to `anchor`.
197
+ * Coordinates are for `position: fixed` (same space as getBoundingClientRect).
198
+ */
199
+ declare function computePosition(anchor: Rect, floating: Size, viewport: Size, options?: PositionOptions): PositionResult;
200
+
201
+ interface UseAnchoredPositionOptions {
202
+ /** Reposition while true, and (re)attach scroll/resize listeners. */
203
+ open: boolean;
204
+ placement?: Placement;
205
+ offset?: number;
206
+ padding?: number;
207
+ }
208
+ /**
209
+ * Position a floating element (a native-popover overlay) against an anchor with
210
+ * cross-browser flip/shift — the DOM half of the overlay-position engine (UIX-FIX-02).
211
+ *
212
+ * Measures both elements with getBoundingClientRect, applies `position: fixed` + left/top
213
+ * (overriding the popover's default centered `inset:0; margin:auto`), and keeps the overlay
214
+ * glued to its anchor on scroll/resize while `open`. Returns a `reposition` fn for callers
215
+ * that want to nudge it manually (e.g. right when a native popover's `toggle` fires).
216
+ */
217
+ declare function useAnchoredPosition(anchor: RefObject<HTMLElement | null> | HTMLElement | null | undefined, floatingRef: RefObject<HTMLElement | null>, { open, placement, offset, padding }: UseAnchoredPositionOptions): () => void;
218
+
143
219
  interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
144
220
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'link';
145
221
  size?: 'sm' | 'md' | 'lg';
@@ -654,16 +730,19 @@ interface StatProps extends HTMLAttributes<HTMLDivElement> {
654
730
  declare function Stat({ label, value, icon, meta, trend, className, ...props }: StatProps): react.JSX.Element;
655
731
 
656
732
  interface TooltipProps extends HTMLAttributes<HTMLSpanElement> {
657
- /** Tooltip text. Rendered via the CSS `[data-uix-tip]` hover/focus tooltip. */
733
+ /** Tooltip text. */
658
734
  label: string;
735
+ /** Preferred placement. Default `'top'`. */
736
+ placement?: Placement;
659
737
  children?: ReactNode;
660
738
  }
661
739
  /**
662
- * Thin wrapper over the CSS-only tooltip (`[data-uix-tip]`). Wraps its children
663
- * in a span carrying the attribute; the bubble is drawn entirely in CSS on
664
- * hover/focus. For keyboard reveal, ensure a focusable child or pass `tabIndex`.
740
+ * Tooltip whose bubble renders in the top layer via the native Popover API, so it
741
+ * escapes any `overflow: hidden/scroll` ancestor that would clip the old CSS-only
742
+ * `[data-uix-tip]` tooltip (UIX-FIX-02). Positioned by the shared overlay engine
743
+ * (flip/shift), shown on hover and keyboard focus, dismissed on blur or Escape.
665
744
  */
666
- declare function Tooltip({ label, children, ...props }: TooltipProps): react.JSX.Element;
745
+ declare function Tooltip({ label, placement, className, children, ...props }: TooltipProps): react.JSX.Element;
667
746
 
668
747
  interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
669
748
  icon?: ReactNode;
@@ -832,13 +911,26 @@ interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
832
911
  * wires to it via `popoverTarget={id}`. Pass `"manual"` to control open state yourself.
833
912
  */
834
913
  popover?: 'auto' | 'manual';
914
+ /**
915
+ * Element (or ref) to anchor against. When set, the popover is placed with
916
+ * cross-browser JS positioning — flip when it won't fit, shift to stay on-screen —
917
+ * instead of relying on CSS `anchor()`, which is Chromium-only and detaches the
918
+ * popover from its trigger everywhere else. The native Popover API still provides
919
+ * the top layer (so it escapes `overflow` clipping) and light-dismiss.
920
+ */
921
+ anchor?: RefObject<HTMLElement | null> | HTMLElement | null;
922
+ /** Preferred placement when `anchor` is set. Default `'bottom-start'`. */
923
+ placement?: Placement;
924
+ /** Gap between the trigger and the popover, in px. Default 6. */
925
+ offset?: number;
835
926
  children?: ReactNode;
836
927
  }
837
928
  /**
838
- * Anchored surface over `.uix-popover`, using the native Popover API.
839
- * Compose ITSM things like a filter popover by putting controls inside.
929
+ * Anchored surface over `.uix-popover`, using the native Popover API. Compose ITSM
930
+ * things like a filter popover, menu, or rich select by putting controls inside.
931
+ * Pass `anchor` to get collision-aware, cross-browser placement (UIX-FIX-02).
840
932
  */
841
- declare function Popover({ popover, children, className, ...props }: PopoverProps): react.JSX.Element;
933
+ declare function Popover({ popover, anchor, placement, offset, className, children, ...props }: PopoverProps): react.JSX.Element;
842
934
 
843
935
  interface CommandPaletteProps extends HTMLAttributes<HTMLDivElement> {
844
936
  /** Props for the search input (value/onChange/placeholder wired by the consumer). */
@@ -924,8 +1016,11 @@ interface TreeNodeProps {
924
1016
  node: TreeNodeData;
925
1017
  expanded: Set<string>;
926
1018
  selected?: string;
1019
+ selectable: boolean;
1020
+ tabbableId?: string;
927
1021
  onToggle: (id: string) => void;
928
1022
  onSelect?: (id: string) => void;
1023
+ onFocusNode: (id: string) => void;
929
1024
  level?: number;
930
1025
  }
931
1026
  interface TreeProps extends Omit<HTMLAttributes<HTMLUListElement>, 'onSelect' | 'onChange'> {
@@ -935,7 +1030,65 @@ interface TreeProps extends Omit<HTMLAttributes<HTMLUListElement>, 'onSelect' |
935
1030
  selected?: string;
936
1031
  onToggle?: (id: string) => void;
937
1032
  onSelect?: (id: string) => void;
1033
+ /**
1034
+ * Virtualize a large tree — render only the rows in view (UIX-FIX-05). Defaults to auto
1035
+ * (on past the engine's `shouldVirtualize` threshold of visible rows). When on, the tree is
1036
+ * flattened to a scrollable list of `maxHeight` with fixed-height rows; a11y is preserved via
1037
+ * `aria-level` / `aria-setsize` / `aria-posinset` instead of nested `role="group"`s.
1038
+ */
1039
+ virtualize?: boolean;
1040
+ /** Fixed row height (px) used for virtualization. Default 32. */
1041
+ rowHeight?: number;
1042
+ /** Scroll-viewport height (px) when virtualizing. Default 384. */
1043
+ maxHeight?: number;
938
1044
  }
939
- declare function Tree({ nodes, expanded: controlledExpanded, defaultExpanded, selected, onToggle, onSelect, className, ...props }: TreeProps): react.JSX.Element;
1045
+ declare function Tree({ nodes, expanded: controlledExpanded, defaultExpanded, selected, onToggle, onSelect, virtualize, rowHeight, maxHeight, className, ...props }: TreeProps): react.JSX.Element;
1046
+
1047
+ /**
1048
+ * uix tree model — framework-agnostic, dependency-free tree flattening + keyboard nav.
1049
+ *
1050
+ * Pure functions (no DOM, no React): flatten a nested tree into the linear list of *visible*
1051
+ * rows (respecting expansion) so it can be virtualized with the table engine's virtualWindow,
1052
+ * and resolve a keyboard key into a navigation decision. Both the plain and the virtualized
1053
+ * <Tree> render paths use these, so the two behave identically and keyboard nav works even for
1054
+ * rows that are scrolled out of the virtual window (UIX-FIX-05). Unit-tested (tree-model.test.mjs).
1055
+ */
1056
+ /** Minimal shape a node must have; the concrete node (label, icon, …) rides along as `T`. */
1057
+ interface TreeLike<T> {
1058
+ id: string;
1059
+ children?: T[];
1060
+ }
1061
+ interface FlatNode<T> {
1062
+ node: T;
1063
+ /** 1-based depth, for aria-level. */
1064
+ level: number;
1065
+ /** number of siblings at this level (this node's parent's child count), for aria-setsize. */
1066
+ setsize: number;
1067
+ /** 1-based index among those siblings, for aria-posinset. */
1068
+ posinset: number;
1069
+ hasChildren: boolean;
1070
+ isExpanded: boolean;
1071
+ }
1072
+ /**
1073
+ * Flatten a nested tree into the visible rows, in DOM/reading order. A node's children follow
1074
+ * it immediately and are included only when the node is in `expanded`.
1075
+ */
1076
+ declare function flattenTree<T extends TreeLike<T>>(nodes: readonly T[], expanded: ReadonlySet<string>, level?: number): FlatNode<T>[];
1077
+ /** A decision produced by a key press — any subset may be set. */
1078
+ interface TreeNavAction {
1079
+ /** move focus (roving tabindex) to this node id. */
1080
+ focusId?: string;
1081
+ /** toggle this node's expansion. */
1082
+ toggleId?: string;
1083
+ /** select this node id. */
1084
+ selectId?: string;
1085
+ }
1086
+ /**
1087
+ * Resolve a key press against the flattened, visible node list — the single source of truth for
1088
+ * both render paths. Mirrors the WAI-ARIA tree pattern: Up/Down move by row, Right expands then
1089
+ * descends, Left collapses then ascends to the parent, Home/End jump, Enter/Space select (and
1090
+ * toggle a parent). Returns an empty action for unhandled keys or an unknown current id.
1091
+ */
1092
+ declare function treeNav<T extends TreeLike<T>>(flat: readonly FlatNode<T>[], currentId: string | undefined, key: string): TreeNavAction;
940
1093
 
941
- export { Alert, type AlertProps, type AlertTone, AppShell, type AppShellProps, Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, BulkBar, type BulkBarProps, Button, ButtonGroup, type ButtonGroupProps, type ButtonProps, Card, type CardProps, CellStrong, CellSub, Checkbox, type CheckboxProps, type ColumnFilter, CommandGroup, type CommandGroupProps, CommandItem, type CommandItemProps, CommandPalette, type CommandPaletteProps, Comment, type CommentProps, Comments, type CommentsProps, Composer, ComposerBar, type ComposerBarProps, type ComposerProps, DescriptionItem, type DescriptionItemProps, DescriptionList, type DescriptionListItem, type DescriptionListProps, DetailLayout, type DetailLayoutProps, Drawer, type DrawerProps, EmptyState, type EmptyStateProps, ErrorState, type ErrorStateProps, ExpandToggle, type ExpandToggleProps, Field, type FieldProps, type FilterKind, type FilterOp, Highlighted, type HighlightedProps, Inbox, InboxDetail, type InboxDetailProps, InboxItem, type InboxItemProps, InboxList, type InboxListProps, type InboxProps, Input, InputGroup, type InputGroupProps, type InputProps, Kanban, KanbanCard, type KanbanCardProps, KanbanColumn, type KanbanColumnProps, type KanbanProps, Label, type LabelProps, List, ListItem, type ListItemProps, type ListProps, LoadingState, type LoadingStateProps, Mark, type MarkProps, Meter, type MeterProps, type MeterTone, Modal, type ModalProps, type NavFavouriteItem, NavFavourites, type NavFavouritesLabels, type NavFavouritesProps, NavGroup, type NavGroupProps, NavItem, type NavItemProps, Note, type NoteProps, type NoteTone, PageHeader, type PageHeaderProps, Pagination, type PaginationProps, Peek, type PeekProps, type PillTone, type PillTreatment, Popover, type PopoverProps, type Primitive, Progress, type ProgressProps, Prose, type ProseProps, Radio, RadioGroup, type RadioGroupProps, type RadioProps, type Row, RowAction, type RowActionProps, RowActions, type RowActionsProps, type Segment, Segmented, SegmentedOption, type SegmentedOptionProps, type SegmentedProps, Select, type SelectAllState, type SelectProps, type ShellNav, Sidebar, type SidebarProps, SidebarSection, type SidebarSectionProps, Skeleton, type SkeletonProps, type SortDir, type SortDirection, type SortKey, Spinner, type SpinnerProps, StarButton, type StarButtonProps, Stat, type StatProps, type StatTrend, StatusPill, type StatusPillProps, SubNavItem, type SubNavItemProps, Switch, type SwitchProps, Tab, type TabProps, Table, type TableDensity, type TableProps, TableWrap, type TableWrapProps, Tabs, type TabsProps, Td, type TdProps, Textarea, type TextareaProps, Th, type ThProps, Timeline, TimelineItem, type TimelineItemProps, type TimelineProps, Toast, type ToastProps, type ToastTone, Toaster, type ToasterProps, Tooltip, type TooltipProps, Tr, type TrProps, Tree, type TreeNodeData, type TreeNodeProps, type TreeProps, type UseTableOptions, type UseTableResult, UserChip, type UserChipProps, type ViewState, type VirtualWindow, applyFilters, clampWidth, cx, highlightSegments, matchFilter, mergePinned, multiSort, nextSortDir, parseView, reorder, searchRows, selectAllState, serializeView, shouldVirtualize, toggleId, togglePage, toggleSort, useDialog, useTable, virtualWindow };
1094
+ export { Alert, type AlertProps, type AlertTone, type Align, AppShell, type AppShellProps, Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, BulkBar, type BulkBarProps, Button, ButtonGroup, type ButtonGroupProps, type ButtonProps, Card, type CardProps, CellStrong, CellSub, Checkbox, type CheckboxProps, type ColumnFilter, CommandGroup, type CommandGroupProps, CommandItem, type CommandItemProps, CommandPalette, type CommandPaletteProps, Comment, type CommentProps, Comments, type CommentsProps, Composer, ComposerBar, type ComposerBarProps, type ComposerProps, DescriptionItem, type DescriptionItemProps, DescriptionList, type DescriptionListItem, type DescriptionListProps, DetailLayout, type DetailLayoutProps, Drawer, type DrawerProps, EmptyState, type EmptyStateProps, ErrorState, type ErrorStateProps, ExpandToggle, type ExpandToggleProps, Field, type FieldProps, type FilterKind, type FilterOp, type FlatNode, Highlighted, type HighlightedProps, Inbox, InboxDetail, type InboxDetailProps, InboxItem, type InboxItemProps, InboxList, type InboxListProps, type InboxProps, Input, InputGroup, type InputGroupProps, type InputProps, Kanban, KanbanCard, type KanbanCardProps, KanbanColumn, type KanbanColumnProps, type KanbanProps, Label, type LabelProps, List, ListItem, type ListItemProps, type ListProps, LoadingState, type LoadingStateProps, Mark, type MarkProps, Meter, type MeterProps, type MeterTone, Modal, type ModalProps, type NavFavouriteItem, NavFavourites, type NavFavouritesLabels, type NavFavouritesProps, NavGroup, type NavGroupProps, NavItem, type NavItemProps, Note, type NoteProps, type NoteTone, PageHeader, type PageHeaderProps, Pagination, type PaginationProps, Peek, type PeekProps, type PillTone, type PillTreatment, type Placement, Popover, type PopoverProps, type PositionOptions, type PositionResult, type Primitive, Progress, type ProgressProps, Prose, type ProseProps, Radio, RadioGroup, type RadioGroupProps, type RadioProps, type Rect, type Row, RowAction, type RowActionProps, RowActions, type RowActionsProps, type Segment, Segmented, SegmentedOption, type SegmentedOptionProps, type SegmentedProps, Select, type SelectAllState, type SelectProps, type ShellNav, type Side, Sidebar, type SidebarProps, SidebarSection, type SidebarSectionProps, type Size, Skeleton, type SkeletonProps, type SortDir, type SortDirection, type SortKey, Spinner, type SpinnerProps, StarButton, type StarButtonProps, Stat, type StatProps, type StatTrend, StatusPill, type StatusPillProps, SubNavItem, type SubNavItemProps, Switch, type SwitchProps, Tab, type TabProps, Table, type TableDensity, type TableProps, TableWrap, type TableWrapProps, Tabs, type TabsProps, Td, type TdProps, Textarea, type TextareaProps, Th, type ThProps, Timeline, TimelineItem, type TimelineItemProps, type TimelineProps, Toast, type ToastProps, type ToastTone, Toaster, type ToasterProps, Tooltip, type TooltipProps, Tr, type TrProps, Tree, type TreeLike, type TreeNavAction, type TreeNodeData, type TreeNodeProps, type TreeProps, type UseAnchoredPositionOptions, type UseTableOptions, type UseTableResult, UserChip, type UserChipProps, type ViewState, type VirtualWindow, applyFilters, clampWidth, computePosition, cx, flattenTree, highlightSegments, matchFilter, mergePinned, multiSort, nextSortDir, parseView, reorder, searchRows, selectAllState, serializeView, shouldVirtualize, toggleId, togglePage, toggleSort, treeNav, useAnchoredPosition, useDialog, useTable, virtualWindow };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { cx } from './cx.js';
2
2
  export { useDialog } from './hooks/useDialog.js';
3
3
  export { UseTableOptions, UseTableResult, useTable } from './hooks/useTable.js';
4
+ export { UseAnchoredPositionOptions, useAnchoredPosition } from './hooks/useAnchoredPosition.js';
5
+ export { Align, Placement, PositionOptions, PositionResult, Rect, Side, Size, computePosition } from './overlay-position.js';
4
6
  export { Button, ButtonGroup, ButtonGroupProps, ButtonProps } from './components/Button.js';
5
7
  export { Input, InputGroup, InputGroupProps, InputProps } from './components/Input.js';
6
8
  export { Textarea, TextareaProps } from './components/Textarea.js';
@@ -46,4 +48,5 @@ export { CommandGroup, CommandGroupProps, CommandItem, CommandItemProps, Command
46
48
  export { Inbox, InboxDetail, InboxDetailProps, InboxItem, InboxItemProps, InboxList, InboxListProps, InboxProps } from './components/Inbox.js';
47
49
  export { Kanban, KanbanCard, KanbanCardProps, KanbanColumn, KanbanColumnProps, KanbanProps } from './components/Kanban.js';
48
50
  export { Tree, TreeNodeData, TreeNodeProps, TreeProps } from './components/Tree.js';
51
+ export { FlatNode, TreeLike, TreeNavAction, flattenTree, treeNav } from './tree-model.js';
49
52
  import 'react';
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { cx } from "./cx.js";
2
2
  import { useDialog } from "./hooks/useDialog.js";
3
3
  import { useTable } from "./hooks/useTable.js";
4
+ import { useAnchoredPosition } from "./hooks/useAnchoredPosition.js";
5
+ import { computePosition } from "./overlay-position.js";
4
6
  import { Button, ButtonGroup } from "./components/Button.js";
5
7
  import { Input, InputGroup } from "./components/Input.js";
6
8
  import { Textarea } from "./components/Textarea.js";
@@ -60,6 +62,7 @@ import { CommandPalette, CommandGroup, CommandItem } from "./components/CommandP
60
62
  import { Inbox, InboxList, InboxItem, InboxDetail } from "./components/Inbox.js";
61
63
  import { Kanban, KanbanColumn, KanbanCard } from "./components/Kanban.js";
62
64
  import { Tree } from "./components/Tree.js";
65
+ import { flattenTree, treeNav } from "./tree-model.js";
63
66
  export {
64
67
  Alert,
65
68
  AppShell,
@@ -145,7 +148,11 @@ export {
145
148
  Tr,
146
149
  Tree,
147
150
  UserChip,
151
+ computePosition,
148
152
  cx,
153
+ flattenTree,
154
+ treeNav,
155
+ useAnchoredPosition,
149
156
  useDialog,
150
157
  useTable
151
158
  };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * uix overlay positioning — framework-agnostic, dependency-free anchored placement.
3
+ *
4
+ * Pure geometry (no DOM): given the anchor's rect, the floating element's size, and
5
+ * the viewport, compute where to put the floating element with two behaviours:
6
+ * • flip — if the preferred side doesn't fit, use the opposite side (or whichever
7
+ * side has more room);
8
+ * • shift — slide along the cross axis so the element stays within the viewport.
9
+ *
10
+ * The React hook (useAnchoredPosition) and the vanilla styleguide both feed this
11
+ * getBoundingClientRect() values, so anchored overlays land in the same place in every
12
+ * browser — instead of relying on CSS anchor() positioning, which is Chromium-only and
13
+ * silently detaches the overlay from its trigger everywhere else (UIX-FIX-02).
14
+ *
15
+ * Everything here is deterministic and unit-tested (see overlay-position.test.mjs).
16
+ */
17
+ type Side = 'top' | 'bottom' | 'left' | 'right';
18
+ type Align = 'start' | 'center' | 'end';
19
+ type Placement = Side | `${Side}-${Align}`;
20
+ /** A viewport-space rectangle, as returned by getBoundingClientRect(). */
21
+ interface Rect {
22
+ x: number;
23
+ y: number;
24
+ width: number;
25
+ height: number;
26
+ }
27
+ interface Size {
28
+ width: number;
29
+ height: number;
30
+ }
31
+ interface PositionOptions {
32
+ /** Preferred placement. Default `'bottom-start'`; a bare side (e.g. `'top'`) centers on the cross axis. */
33
+ placement?: Placement;
34
+ /** Gap between the anchor and the floating element, in px. Default 6. */
35
+ offset?: number;
36
+ /** Minimum gap kept from each viewport edge when shifting, in px. Default 8. */
37
+ padding?: number;
38
+ /** Flip to the opposite side when the preferred one doesn't fit. Default true. */
39
+ flip?: boolean;
40
+ /** Slide along the cross axis to stay on-screen. Default true. */
41
+ shift?: boolean;
42
+ }
43
+ interface PositionResult {
44
+ /** Viewport-space left, for `position: fixed`. */
45
+ x: number;
46
+ /** Viewport-space top, for `position: fixed`. */
47
+ y: number;
48
+ /** The placement actually used (after any flip). */
49
+ placement: Placement;
50
+ side: Side;
51
+ align: Align;
52
+ }
53
+ /**
54
+ * Compute the floating element's viewport-space position anchored to `anchor`.
55
+ * Coordinates are for `position: fixed` (same space as getBoundingClientRect).
56
+ */
57
+ declare function computePosition(anchor: Rect, floating: Size, viewport: Size, options?: PositionOptions): PositionResult;
58
+
59
+ export { type Align, type Placement, type PositionOptions, type PositionResult, type Rect, type Side, type Size, computePosition };
@@ -0,0 +1,62 @@
1
+ const OPPOSITE = { top: "bottom", bottom: "top", left: "right", right: "left" };
2
+ function parsePlacement(p) {
3
+ const [side, align] = p.split("-");
4
+ return { side, align: align ?? "center" };
5
+ }
6
+ const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
7
+ function spaceOn(side, a, vp, offset) {
8
+ switch (side) {
9
+ case "top":
10
+ return a.y - offset;
11
+ case "bottom":
12
+ return vp.height - (a.y + a.height) - offset;
13
+ case "left":
14
+ return a.x - offset;
15
+ case "right":
16
+ return vp.width - (a.x + a.width) - offset;
17
+ }
18
+ }
19
+ function fitsOn(side, a, f, vp, offset, padding) {
20
+ const need = side === "top" || side === "bottom" ? f.height : f.width;
21
+ return spaceOn(side, a, vp, offset) >= need + padding;
22
+ }
23
+ function computePosition(anchor, floating, viewport, options = {}) {
24
+ const { placement = "bottom-start", offset = 6, padding = 8, flip = true, shift = true } = options;
25
+ const { side: preferred, align } = parsePlacement(placement);
26
+ let side = preferred;
27
+ if (flip && !fitsOn(side, anchor, floating, viewport, offset, padding)) {
28
+ const opp = OPPOSITE[side];
29
+ if (fitsOn(opp, anchor, floating, viewport, offset, padding) || spaceOn(opp, anchor, viewport, offset) > spaceOn(side, anchor, viewport, offset)) {
30
+ side = opp;
31
+ }
32
+ }
33
+ let x, y;
34
+ const horizontal = side === "top" || side === "bottom";
35
+ if (side === "top") y = anchor.y - floating.height - offset;
36
+ else if (side === "bottom") y = anchor.y + anchor.height + offset;
37
+ else if (side === "left") x = anchor.x - floating.width - offset;
38
+ else x = anchor.x + anchor.width + offset;
39
+ if (horizontal) {
40
+ const start = anchor.x;
41
+ const end = anchor.x + anchor.width - floating.width;
42
+ const center = anchor.x + (anchor.width - floating.width) / 2;
43
+ x = align === "start" ? start : align === "end" ? end : center;
44
+ if (shift) x = clamp(x, padding, Math.max(padding, viewport.width - floating.width - padding));
45
+ } else {
46
+ const start = anchor.y;
47
+ const end = anchor.y + anchor.height - floating.height;
48
+ const center = anchor.y + (anchor.height - floating.height) / 2;
49
+ y = align === "start" ? start : align === "end" ? end : center;
50
+ if (shift) y = clamp(y, padding, Math.max(padding, viewport.height - floating.height - padding));
51
+ }
52
+ return {
53
+ x,
54
+ y,
55
+ placement: align === "center" ? side : `${side}-${align}`,
56
+ side,
57
+ align
58
+ };
59
+ }
60
+ export {
61
+ computePosition
62
+ };
@@ -30,8 +30,36 @@ function toggleSort(keys, field, additive = false) {
30
30
  function asNum(v) {
31
31
  return typeof v === "number" ? v : Number(v);
32
32
  }
33
+ function asTime(v) {
34
+ if (v == null) return NaN;
35
+ if (v instanceof Date) return v.getTime();
36
+ if (typeof v === "number") return v;
37
+ const s = String(v).trim();
38
+ if (s === "") return NaN;
39
+ const parsed = Date.parse(s);
40
+ return Number.isNaN(parsed) ? Number(s) : parsed;
41
+ }
42
+ function matchDate(cell, f) {
43
+ const c = asTime(cell);
44
+ if (Number.isNaN(c)) return false;
45
+ switch (f.op) {
46
+ case "eq":
47
+ return c === asTime(f.value);
48
+ case "lt":
49
+ return c < asTime(f.value);
50
+ case "gt":
51
+ return c > asTime(f.value);
52
+ case "between": {
53
+ const [lo, hi] = f.value.map(asTime);
54
+ return c >= lo && c <= hi;
55
+ }
56
+ default:
57
+ return true;
58
+ }
59
+ }
33
60
  function matchFilter(row, f) {
34
61
  const cell = row[f.field];
62
+ if (f.kind === "date") return matchDate(cell, f);
35
63
  switch (f.op) {
36
64
  case "isAnyOf":
37
65
  return Array.isArray(f.value) && f.value.includes(cell);
@@ -90,13 +118,53 @@ function highlightSegments(text, query) {
90
118
  }
91
119
  return out;
92
120
  }
121
+ const ARRAY_OPS = /* @__PURE__ */ new Set(["isAnyOf", "isNoneOf", "between"]);
122
+ const escapePart = (s) => encodeURIComponent(s).replace(/~/g, "%7E");
123
+ const unescapePart = (s) => decodeURIComponent(s);
124
+ const scalarToStr = (v) => v == null ? "" : String(v);
125
+ function parseScalar(s, kind) {
126
+ switch (kind) {
127
+ case "number":
128
+ return Number(s);
129
+ case "boolean":
130
+ return s === "true";
131
+ case "date":
132
+ return /^-?\d+$/.test(s) ? Number(s) : s;
133
+ // epoch-ms number, else the ISO/date string
134
+ default:
135
+ return s;
136
+ }
137
+ }
138
+ function kindFromOp(op) {
139
+ if (op === "isAnyOf" || op === "isNoneOf") return "enum";
140
+ if (op === "is") return "boolean";
141
+ if (op === "contains" || op === "equals" || op === "startsWith") return "text";
142
+ return "number";
143
+ }
144
+ function serializeFilter(f) {
145
+ const value = Array.isArray(f.value) ? f.value.map((x) => escapePart(scalarToStr(x))).join("|") : escapePart(scalarToStr(f.value));
146
+ return `${escapePart(f.field)}~${f.kind}~${f.op}~${value}`;
147
+ }
148
+ function parseFilter(tok) {
149
+ const parts = tok.split("~");
150
+ let field, kind, op, raw;
151
+ if (parts.length >= 4) {
152
+ [field, kind, op, raw] = [unescapePart(parts[0]), parts[1], parts[2], parts[3] ?? ""];
153
+ } else {
154
+ [field, op, raw] = [unescapePart(parts[0]), parts[1], parts[2] ?? ""];
155
+ kind = kindFromOp(op);
156
+ }
157
+ if (!field || !op) return null;
158
+ const value = ARRAY_OPS.has(op) ? raw.split("|").filter((s) => s !== "").map((s) => parseScalar(unescapePart(s), kind)) : parseScalar(unescapePart(raw), kind);
159
+ return { field, kind, op, value };
160
+ }
93
161
  function serializeView(v) {
94
162
  const p = new URLSearchParams();
95
163
  if (v.sort?.length) p.set("sort", v.sort.map((s) => `${s.field}:${s.dir === "descending" ? "d" : "a"}`).join(","));
96
164
  if (v.columns?.length) p.set("cols", v.columns.join(","));
97
165
  if (v.density && v.density !== "standard") p.set("density", v.density);
98
166
  if (v.q) p.set("q", v.q);
99
- if (v.filters?.length) p.set("filter", v.filters.map((f) => `${f.field}~${f.op}~${Array.isArray(f.value) ? f.value.join("|") : f.value ?? ""}`).join(","));
167
+ if (v.filters?.length) p.set("filter", v.filters.map(serializeFilter).join(","));
100
168
  return p.toString();
101
169
  }
102
170
  function parseView(qs) {
@@ -114,11 +182,9 @@ function parseView(qs) {
114
182
  const q = p.get("q");
115
183
  if (q) v.q = q;
116
184
  const filter = p.get("filter");
117
- if (filter) v.filters = filter.split(",").filter(Boolean).map((tok) => {
118
- const [field, op, raw] = tok.split("~");
119
- const value = raw?.includes("|") ? raw.split("|") : raw;
120
- return { field, kind: "text", op, value };
121
- });
185
+ if (filter) {
186
+ v.filters = filter.split(",").filter(Boolean).map(parseFilter).filter((f) => f !== null);
187
+ }
122
188
  return v;
123
189
  }
124
190
  function virtualWindow(scrollTop, viewportH, rowH, count, overscan = 6) {
@@ -0,0 +1,48 @@
1
+ /**
2
+ * uix tree model — framework-agnostic, dependency-free tree flattening + keyboard nav.
3
+ *
4
+ * Pure functions (no DOM, no React): flatten a nested tree into the linear list of *visible*
5
+ * rows (respecting expansion) so it can be virtualized with the table engine's virtualWindow,
6
+ * and resolve a keyboard key into a navigation decision. Both the plain and the virtualized
7
+ * <Tree> render paths use these, so the two behave identically and keyboard nav works even for
8
+ * rows that are scrolled out of the virtual window (UIX-FIX-05). Unit-tested (tree-model.test.mjs).
9
+ */
10
+ /** Minimal shape a node must have; the concrete node (label, icon, …) rides along as `T`. */
11
+ interface TreeLike<T> {
12
+ id: string;
13
+ children?: T[];
14
+ }
15
+ interface FlatNode<T> {
16
+ node: T;
17
+ /** 1-based depth, for aria-level. */
18
+ level: number;
19
+ /** number of siblings at this level (this node's parent's child count), for aria-setsize. */
20
+ setsize: number;
21
+ /** 1-based index among those siblings, for aria-posinset. */
22
+ posinset: number;
23
+ hasChildren: boolean;
24
+ isExpanded: boolean;
25
+ }
26
+ /**
27
+ * Flatten a nested tree into the visible rows, in DOM/reading order. A node's children follow
28
+ * it immediately and are included only when the node is in `expanded`.
29
+ */
30
+ declare function flattenTree<T extends TreeLike<T>>(nodes: readonly T[], expanded: ReadonlySet<string>, level?: number): FlatNode<T>[];
31
+ /** A decision produced by a key press — any subset may be set. */
32
+ interface TreeNavAction {
33
+ /** move focus (roving tabindex) to this node id. */
34
+ focusId?: string;
35
+ /** toggle this node's expansion. */
36
+ toggleId?: string;
37
+ /** select this node id. */
38
+ selectId?: string;
39
+ }
40
+ /**
41
+ * Resolve a key press against the flattened, visible node list — the single source of truth for
42
+ * both render paths. Mirrors the WAI-ARIA tree pattern: Up/Down move by row, Right expands then
43
+ * descends, Left collapses then ascends to the parent, Home/End jump, Enter/Space select (and
44
+ * toggle a parent). Returns an empty action for unhandled keys or an unknown current id.
45
+ */
46
+ declare function treeNav<T extends TreeLike<T>>(flat: readonly FlatNode<T>[], currentId: string | undefined, key: string): TreeNavAction;
47
+
48
+ export { type FlatNode, type TreeLike, type TreeNavAction, flattenTree, treeNav };
@@ -0,0 +1,49 @@
1
+ function flattenTree(nodes, expanded, level = 1) {
2
+ const out = [];
3
+ const setsize = nodes.length;
4
+ nodes.forEach((node, i) => {
5
+ const hasChildren = Array.isArray(node.children) && node.children.length > 0;
6
+ const isExpanded = hasChildren && expanded.has(node.id);
7
+ out.push({ node, level, setsize, posinset: i + 1, hasChildren, isExpanded });
8
+ if (isExpanded) out.push(...flattenTree(node.children, expanded, level + 1));
9
+ });
10
+ return out;
11
+ }
12
+ function treeNav(flat, currentId, key) {
13
+ const idx = flat.findIndex((f) => f.node.id === currentId);
14
+ if (idx < 0) return {};
15
+ const cur = flat[idx];
16
+ const focusAt = (i) => {
17
+ const clamped = Math.max(0, Math.min(flat.length - 1, i));
18
+ return { focusId: flat[clamped]?.node.id };
19
+ };
20
+ switch (key) {
21
+ case "ArrowDown":
22
+ return focusAt(idx + 1);
23
+ case "ArrowUp":
24
+ return focusAt(idx - 1);
25
+ case "Home":
26
+ return focusAt(0);
27
+ case "End":
28
+ return focusAt(flat.length - 1);
29
+ case "ArrowRight":
30
+ if (cur.hasChildren && !cur.isExpanded) return { toggleId: cur.node.id };
31
+ if (cur.hasChildren && cur.isExpanded) return focusAt(idx + 1);
32
+ return {};
33
+ case "ArrowLeft":
34
+ if (cur.hasChildren && cur.isExpanded) return { toggleId: cur.node.id };
35
+ for (let i = idx - 1; i >= 0; i--) {
36
+ if (flat[i].level === cur.level - 1) return { focusId: flat[i].node.id };
37
+ }
38
+ return {};
39
+ case "Enter":
40
+ case " ":
41
+ return cur.hasChildren ? { toggleId: cur.node.id, selectId: cur.node.id } : { selectId: cur.node.id };
42
+ default:
43
+ return {};
44
+ }
45
+ }
46
+ export {
47
+ flattenTree,
48
+ treeNav
49
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tensor_1/react",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Thin React wrappers over the UIx v2 CSS component library.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",