@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/components/Field.js +10 -4
- package/dist/components/Popover.d.ts +18 -4
- package/dist/components/Popover.js +31 -2
- package/dist/components/Toast.js +5 -4
- package/dist/components/Tooltip.d.ts +9 -5
- package/dist/components/Tooltip.js +61 -3
- package/dist/components/Tree.d.ts +15 -1
- package/dist/components/Tree.js +247 -35
- package/dist/hooks/useAnchoredPosition.d.ts +22 -0
- package/dist/hooks/useAnchoredPosition.js +44 -0
- package/dist/hooks/useDialog.js +36 -5
- package/dist/index.cjs +657 -100
- package/dist/index.d.cts +164 -11
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/overlay-position.d.ts +59 -0
- package/dist/overlay-position.js +62 -0
- package/dist/table-engine.js +72 -6
- package/dist/tree-model.d.ts +48 -0
- package/dist/tree-model.js +49 -0
- package/package.json +1 -1
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.
|
|
733
|
+
/** Tooltip text. */
|
|
658
734
|
label: string;
|
|
735
|
+
/** Preferred placement. Default `'top'`. */
|
|
736
|
+
placement?: Placement;
|
|
659
737
|
children?: ReactNode;
|
|
660
738
|
}
|
|
661
739
|
/**
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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
|
+
};
|
package/dist/table-engine.js
CHANGED
|
@@ -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(
|
|
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)
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
};
|