@tensor_1/react 2.43.0 → 2.44.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,78 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
+ import { DashboardCanvasItem } from '../dashboard-canvas-model.js';
4
+
5
+ /** Every word the canvas renders. `{label}` is the item's name; the others are grid units. */
6
+ interface DashboardCanvasLabels {
7
+ /** The region's accessible name. */
8
+ region: string;
9
+ /** The move handle's name. */
10
+ move: string;
11
+ /** The resize handle's name. */
12
+ resize: string;
13
+ /** How the handles work, read with each handle (`aria-describedby`). */
14
+ instructions: string;
15
+ /** Announced after a move. `{col}` and `{row}` count from 1. */
16
+ moved: string;
17
+ /** Announced after a resize. */
18
+ resized: string;
19
+ /** Announced when a step would leave the canvas or pass the item's size limit. */
20
+ atEdge: string;
21
+ /** Announced when Escape drops a drag. */
22
+ cancelled: string;
23
+ }
24
+ declare const DEFAULT_DASHBOARD_CANVAS_LABELS: DashboardCanvasLabels;
25
+ /** Space between items: `sm` --uix-space-3, `md` --uix-space-4, `lg` --uix-space-6. */
26
+ type DashboardCanvasGap = 'sm' | 'md' | 'lg';
27
+ /** What changed, for the product's save and its journey event. */
28
+ interface DashboardCanvasChange {
29
+ readonly id: string;
30
+ readonly kind: 'move' | 'resize';
31
+ readonly source: 'pointer' | 'keyboard';
32
+ }
33
+ interface DashboardCanvasProps<T extends DashboardCanvasItem> extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'children'> {
34
+ /** The layout. Overlaps and out-of-range values are settled before rendering. */
35
+ items: readonly T[];
36
+ /** One cell's content: a widget, a card. Its last child takes the spare height. */
37
+ renderItem: (item: T) => ReactNode;
38
+ /** The item's name in labels and announcements. Default: its id. */
39
+ itemLabel?: (item: T) => string;
40
+ /**
41
+ * Edit mode: each item gets a move handle and a resize handle, both draggable and both
42
+ * operable with the arrow keys. Off, the canvas is a plain layout and server-renderable.
43
+ */
44
+ editable?: boolean;
45
+ /** The next layout, once per drop or key press. The canvas never keeps its own copy. */
46
+ onLayoutChange?: (items: T[], change: DashboardCanvasChange) => void;
47
+ /** Height of one row, any CSS length. Default `--uix-dashboard-canvas-row` (5rem). */
48
+ rowHeight?: string;
49
+ /** Default `md`. */
50
+ gap?: DashboardCanvasGap;
51
+ labels?: Partial<DashboardCanvasLabels>;
52
+ }
53
+ /**
54
+ * A free dashboard layout (TENSOR HAR-1823): twelve columns, rows of one fixed height, and each
55
+ * item at its own `{ col, row, w, h }` in grid units. In edit mode a person drags an item by its
56
+ * move handle and resizes it by the corner handle; both snap to the grid, push what they land
57
+ * on downwards, and report one new layout per drop. Both handles also take the arrow keys, and
58
+ * the pure functions behind them (`moveCanvasItem`, `resizeCanvasItem`) are exported for the
59
+ * product's own item menu, so dragging is never the only way (WCAG 2.5.7).
60
+ *
61
+ * Below 36rem of its own width the canvas is one column in reading order (row by row, left to
62
+ * right), each item keeping its height, and the handles step aside: the item menu remains.
63
+ *
64
+ * Owns interaction, never persistence: the product stores `items` and passes them back.
65
+ *
66
+ * ```tsx
67
+ * <DashboardCanvas
68
+ * items={layout}
69
+ * editable={editing}
70
+ * itemLabel={(item) => titles[item.id]}
71
+ * renderItem={(item) => <Widget id={item.id} />}
72
+ * onLayoutChange={(next) => save(next)}
73
+ * />
74
+ * ```
75
+ */
76
+ declare function DashboardCanvas<T extends DashboardCanvasItem>({ items, renderItem, itemLabel, editable, onLayoutChange, rowHeight, gap, labels: labelOverrides, className, style, ...props }: DashboardCanvasProps<T>): react.JSX.Element;
77
+
78
+ export { DEFAULT_DASHBOARD_CANVAS_LABELS, DashboardCanvas, type DashboardCanvasChange, type DashboardCanvasGap, type DashboardCanvasLabels, type DashboardCanvasProps };
@@ -0,0 +1,227 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { fillLabel } from "../fill-label.js";
6
+ import { GripVerticalIcon, Maximize2Icon } from "../icons/components.js";
7
+ import {
8
+ canvasMoveTarget,
9
+ canvasReadingOrder,
10
+ canvasResizeTarget,
11
+ DASHBOARD_CANVAS_COLUMNS,
12
+ normalizeCanvas,
13
+ placeCanvasItem
14
+ } from "../dashboard-canvas-model.js";
15
+ const DEFAULT_DASHBOARD_CANVAS_LABELS = {
16
+ region: "Dashboard layout",
17
+ move: "Move {label}",
18
+ resize: "Resize {label}",
19
+ instructions: "Arrow keys move or resize one step. Escape cancels a drag.",
20
+ moved: "{label}: column {col}, row {row}",
21
+ resized: "{label}: {w} columns wide, {h} rows tall",
22
+ atEdge: "{label} cannot go further that way",
23
+ cancelled: "Move cancelled"
24
+ };
25
+ const DRAG_THRESHOLD = 4;
26
+ function measure(grid) {
27
+ const style = getComputedStyle(grid);
28
+ const width = grid.clientWidth - (Number.parseFloat(style.paddingLeft) || 0) - (Number.parseFloat(style.paddingRight) || 0);
29
+ const tracks = style.gridTemplateColumns.split(" ").map((track) => Number.parseFloat(track)).filter(Number.isFinite);
30
+ const trackSum = tracks.reduce((sum, track) => sum + track, 0);
31
+ const columnGap = tracks.length > 1 ? Math.max(0, (width - trackSum) / (tracks.length - 1)) : 0;
32
+ const colPitch = (width + columnGap) / DASHBOARD_CANVAS_COLUMNS;
33
+ const rowGap = Number.parseFloat(style.rowGap) || 0;
34
+ const rowTrack = Number.parseFloat(style.gridAutoRows) || 80;
35
+ return { colPitch, rowPitch: rowTrack + rowGap };
36
+ }
37
+ function DashboardCanvas({
38
+ items,
39
+ renderItem,
40
+ itemLabel = (item) => item.id,
41
+ editable = false,
42
+ onLayoutChange,
43
+ rowHeight,
44
+ gap = "md",
45
+ labels: labelOverrides,
46
+ className,
47
+ style,
48
+ ...props
49
+ }) {
50
+ const labels = { ...DEFAULT_DASHBOARD_CANVAS_LABELS, ...labelOverrides };
51
+ const instructionsId = useId();
52
+ const gridRef = useRef(null);
53
+ const dragRef = useRef(null);
54
+ const [preview, setPreview] = useState(null);
55
+ const [announcement, setAnnouncement] = useState("");
56
+ const layout = useMemo(() => normalizeCanvas(items), [items]);
57
+ const order = useMemo(() => new Map(canvasReadingOrder(layout).map((item, index) => [item.id, index])), [layout]);
58
+ const live = useRef({ layout, onLayoutChange, labels, itemLabel });
59
+ live.current = { layout, onLayoutChange, labels, itemLabel };
60
+ useEffect(() => () => dragRef.current?.stop(), []);
61
+ const describe = (item, kind) => kind === "move" ? fillLabel(labels.moved, { label: itemLabel(item), col: item.col + 1, row: item.row + 1 }) : fillLabel(labels.resized, { label: itemLabel(item), w: item.w, h: item.h });
62
+ const commit = (id, placement, kind, source) => {
63
+ const state = live.current;
64
+ const next = placeCanvasItem(state.layout, id, placement);
65
+ const placed = next.find((item) => item.id === id);
66
+ if (!placed) return;
67
+ state.onLayoutChange?.(next, { id, kind, source });
68
+ setAnnouncement(describe(placed, kind));
69
+ };
70
+ const onHandleKeyDown = (event, item, kind) => {
71
+ const keys = { ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right" };
72
+ const resizes = { ArrowRight: "wider", ArrowLeft: "narrower", ArrowDown: "taller", ArrowUp: "shorter" };
73
+ if (event.key === "Escape" && dragRef.current) {
74
+ event.preventDefault();
75
+ dragRef.current.stop();
76
+ dragRef.current = null;
77
+ setPreview(null);
78
+ setAnnouncement(labels.cancelled);
79
+ return;
80
+ }
81
+ if (!(event.key in keys)) return;
82
+ event.preventDefault();
83
+ const target = kind === "move" ? canvasMoveTarget(item, keys[event.key]) : canvasResizeTarget(item, resizes[event.key]);
84
+ if (!target) {
85
+ setAnnouncement(fillLabel(labels.atEdge, { label: itemLabel(item) }));
86
+ return;
87
+ }
88
+ commit(item.id, target, kind, "keyboard");
89
+ };
90
+ const onHandlePointerDown = (event, item, kind) => {
91
+ if (event.button !== 0 || dragRef.current || !gridRef.current) return;
92
+ const { colPitch, rowPitch } = measure(gridRef.current);
93
+ const origin = { col: item.col, row: item.row, w: item.w, h: item.h };
94
+ let moved = false;
95
+ let last = origin;
96
+ const targetFor = (move) => {
97
+ const dc = Math.round((move.clientX - drag.startX) / colPitch);
98
+ const dr = Math.round((move.clientY - drag.startY) / rowPitch);
99
+ return kind === "move" ? { ...origin, col: origin.col + dc, row: origin.row + dr } : { ...origin, w: origin.w + dc, h: origin.h + dr };
100
+ };
101
+ const onMove = (move) => {
102
+ if (move.pointerId !== drag.pointerId) return;
103
+ if (!moved && Math.hypot(move.clientX - drag.startX, move.clientY - drag.startY) < DRAG_THRESHOLD) return;
104
+ moved = true;
105
+ const placed = placeCanvasItem(live.current.layout, item.id, targetFor(move)).find((candidate) => candidate.id === item.id);
106
+ if (!placed) return;
107
+ last = { col: placed.col, row: placed.row, w: placed.w, h: placed.h };
108
+ setPreview({ id: item.id, placement: last });
109
+ };
110
+ const finish = (drop) => {
111
+ drag.stop();
112
+ dragRef.current = null;
113
+ setPreview(null);
114
+ const changed = last.col !== origin.col || last.row !== origin.row || last.w !== origin.w || last.h !== origin.h;
115
+ if (drop && moved && changed) commit(item.id, last, kind, "pointer");
116
+ };
117
+ const onUp = (up) => {
118
+ if (up.pointerId === drag.pointerId) finish(true);
119
+ };
120
+ const onCancel = (cancel) => {
121
+ if (cancel.pointerId === drag.pointerId) finish(false);
122
+ };
123
+ window.addEventListener("pointermove", onMove);
124
+ window.addEventListener("pointerup", onUp);
125
+ window.addEventListener("pointercancel", onCancel);
126
+ const drag = {
127
+ id: item.id,
128
+ kind,
129
+ pointerId: event.pointerId,
130
+ startX: event.clientX,
131
+ startY: event.clientY,
132
+ origin,
133
+ colPitch,
134
+ rowPitch,
135
+ stop: () => {
136
+ window.removeEventListener("pointermove", onMove);
137
+ window.removeEventListener("pointerup", onUp);
138
+ window.removeEventListener("pointercancel", onCancel);
139
+ }
140
+ };
141
+ dragRef.current = drag;
142
+ };
143
+ const placementStyle = (placement, readingIndex) => ({
144
+ "--uix-dashboard-canvas-col": placement.col + 1,
145
+ "--uix-dashboard-canvas-row-start": placement.row + 1,
146
+ "--uix-dashboard-canvas-w": placement.w,
147
+ "--uix-dashboard-canvas-h": placement.h,
148
+ "--uix-dashboard-canvas-order": readingIndex
149
+ });
150
+ const rootStyle = rowHeight ? { ...style, "--uix-dashboard-canvas-row": rowHeight } : style;
151
+ return /* @__PURE__ */ jsxs(
152
+ "div",
153
+ {
154
+ ...props,
155
+ ref: gridRef,
156
+ role: "region",
157
+ "aria-label": labels.region,
158
+ className: cx(
159
+ "uix-dashboard-canvas",
160
+ gap === "sm" && "uix-dashboard-canvas--gap-sm",
161
+ gap === "lg" && "uix-dashboard-canvas--gap-lg",
162
+ editable && "uix-dashboard-canvas--editing",
163
+ preview && "uix-dashboard-canvas--dragging",
164
+ className
165
+ ),
166
+ style: rootStyle,
167
+ children: [
168
+ layout.map((item) => {
169
+ const name = itemLabel(item);
170
+ const dragging = preview?.id === item.id;
171
+ return /* @__PURE__ */ jsxs(
172
+ "div",
173
+ {
174
+ className: cx("uix-dashboard-canvas__item", dragging && "uix-dashboard-canvas__item--dragging"),
175
+ style: placementStyle(item, order.get(item.id) ?? 0),
176
+ "data-canvas-id": item.id,
177
+ children: [
178
+ renderItem(item),
179
+ editable ? /* @__PURE__ */ jsxs(Fragment, { children: [
180
+ /* @__PURE__ */ jsx(
181
+ "button",
182
+ {
183
+ type: "button",
184
+ className: "uix-dashboard-canvas__handle uix-dashboard-canvas__move",
185
+ "aria-label": fillLabel(labels.move, { label: name }),
186
+ "aria-describedby": instructionsId,
187
+ onKeyDown: (event) => onHandleKeyDown(event, item, "move"),
188
+ onPointerDown: (event) => onHandlePointerDown(event, item, "move"),
189
+ children: /* @__PURE__ */ jsx(GripVerticalIcon, { "aria-hidden": "true" })
190
+ }
191
+ ),
192
+ /* @__PURE__ */ jsx(
193
+ "button",
194
+ {
195
+ type: "button",
196
+ className: "uix-dashboard-canvas__handle uix-dashboard-canvas__resize",
197
+ "aria-label": fillLabel(labels.resize, { label: name }),
198
+ "aria-describedby": instructionsId,
199
+ onKeyDown: (event) => onHandleKeyDown(event, item, "resize"),
200
+ onPointerDown: (event) => onHandlePointerDown(event, item, "resize"),
201
+ children: /* @__PURE__ */ jsx(Maximize2Icon, { "aria-hidden": "true" })
202
+ }
203
+ )
204
+ ] }) : null
205
+ ]
206
+ },
207
+ item.id
208
+ );
209
+ }),
210
+ preview ? /* @__PURE__ */ jsx(
211
+ "div",
212
+ {
213
+ className: "uix-dashboard-canvas__preview",
214
+ style: placementStyle(preview.placement, order.get(preview.id) ?? 0),
215
+ "aria-hidden": "true"
216
+ }
217
+ ) : null,
218
+ editable ? /* @__PURE__ */ jsx("span", { id: instructionsId, className: "uix-visually-hidden", children: labels.instructions }) : null,
219
+ /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: announcement })
220
+ ]
221
+ }
222
+ );
223
+ }
224
+ export {
225
+ DEFAULT_DASHBOARD_CANVAS_LABELS,
226
+ DashboardCanvas
227
+ };
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { RefObject, KeyboardEvent, ReactNode } from 'react';
2
+ import { SyntheticEvent, RefObject, KeyboardEvent, ReactNode } from 'react';
3
3
  import { ButtonProps } from './Button.js';
4
4
  import '../link.js';
5
5
  import './Kbd.js';
@@ -15,6 +15,12 @@ interface DiscardPromptLabels {
15
15
  /** Close and throw the input away. */
16
16
  discard: string;
17
17
  }
18
+ /**
19
+ * What asked a `Drawer` or `Modal` to close, handed to its `onClose`: the click on the close
20
+ * button, a `DialogClose`, the drawer's backdrop or "Discard". Absent when the browser closed the
21
+ * dialog itself (Escape on a clean dialog, a `<form method="dialog">`), as before 2.42.
22
+ */
23
+ type DialogCloseEvent = SyntheticEvent | Event;
18
24
  type DialogCloseProps = ButtonProps;
19
25
  /**
20
26
  * A button that closes the surrounding `Drawer` or `Modal` the way its × does (HAR-2029): at once
@@ -25,7 +31,7 @@ type DialogCloseProps = ButtonProps;
25
31
  declare const DialogClose: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLButtonElement>>;
26
32
  interface GuardedDialogOptions {
27
33
  open: boolean;
28
- onClose?: () => void;
34
+ onClose?: (event?: DialogCloseEvent) => void;
29
35
  dirty?: boolean;
30
36
  pending?: boolean;
31
37
  labels?: Partial<DiscardPromptLabels>;
@@ -45,10 +51,10 @@ interface GuardedDialogOptions {
45
51
  */
46
52
  declare function useGuardedDialog({ open, onClose, dirty, pending, labels: labelsProp, closeRef }: GuardedDialogOptions): {
47
53
  ref: RefObject<HTMLDialogElement>;
48
- requestClose: (anchor?: HTMLElement | null) => void;
54
+ requestClose: (anchor?: HTMLElement | null, event?: DialogCloseEvent) => void;
49
55
  onKeyDown: (event: KeyboardEvent<HTMLDialogElement>) => void;
50
56
  prompt: react.JSX.Element | null;
51
57
  provide: (children: ReactNode) => react.JSX.Element;
52
58
  };
53
59
 
54
- export { DialogClose, type DialogCloseProps, type DiscardPromptLabels, type GuardedDialogOptions, useGuardedDialog };
60
+ export { DialogClose, type DialogCloseEvent, type DialogCloseProps, type DiscardPromptLabels, type GuardedDialogOptions, useGuardedDialog };
@@ -37,7 +37,7 @@ const DialogClose = forwardRef(({ onClick, type = "button", ...props }, ref) =>
37
37
  }
38
38
  onClick?.(event);
39
39
  if (event.defaultPrevented) return;
40
- dialog?.requestClose(event.currentTarget);
40
+ dialog?.requestClose(event.currentTarget, event);
41
41
  }
42
42
  }
43
43
  );
@@ -57,11 +57,11 @@ function useGuardedDialog({ open, onClose, dirty = false, pending = false, label
57
57
  setPromptState(next);
58
58
  }, []);
59
59
  const guarded = () => !!latest.current.onClose && (latest.current.dirty || latest.current.pending);
60
- const requestClose = useCallback((anchor) => {
60
+ const requestClose = useCallback((anchor, event) => {
61
61
  const { open: isOpen, onClose: close, dirty: isDirty, pending: isPending, prompt: asking } = latest.current;
62
62
  if (!isOpen || !close || isPending || asking) return;
63
63
  if (!isDirty) {
64
- close();
64
+ close(event);
65
65
  return;
66
66
  }
67
67
  const active = document.activeElement;
@@ -89,9 +89,9 @@ function useGuardedDialog({ open, onClose, dirty = false, pending = false, label
89
89
  return true;
90
90
  });
91
91
  dialogRef.current = ref.current;
92
- const discard = useCallback(() => {
92
+ const discard = useCallback((event) => {
93
93
  setPrompt(null);
94
- latest.current.onClose?.();
94
+ latest.current.onClose?.(event);
95
95
  }, [setPrompt]);
96
96
  useEffect(() => {
97
97
  if (open && dirty && !pending) return;
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
- import { DiscardPromptLabels } from './DiscardPrompt.js';
3
+ import { DialogCloseEvent, DiscardPromptLabels } from './DiscardPrompt.js';
4
4
  import './Button.js';
5
5
  import '../link.js';
6
6
  import './Kbd.js';
@@ -9,7 +9,11 @@ interface DrawerProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
9
9
  /** TENSOR RX-125 (UIX-04): translatable; English default. */
10
10
  closeLabel?: string;
11
11
  open: boolean;
12
- onClose?: () => void;
12
+ /**
13
+ * Close the drawer. Gets the click that asked (the close button, the backdrop, a `DialogClose`,
14
+ * "Discard"); nothing when the browser closed it (Escape, `<form method="dialog">`).
15
+ */
16
+ onClose?: (event?: DialogCloseEvent) => void;
13
17
  /**
14
18
  * The drawer holds input that is not saved (HAR-2029), e.g. react-hook-form's
15
19
  * `formState.isDirty`. Escape, the close button, a backdrop click and a `DialogClose` then open
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useId, useRef } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { useUixLabels } from "../labels-context.js";
6
- import { backdropDismiss } from "../hooks/backdropDismiss.js";
6
+ import { isBackdropClick } from "../hooks/backdropDismiss.js";
7
7
  import { useGuardedDialog } from "./DiscardPrompt.js";
8
8
  const CloseIcon = () => /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
9
9
  function Drawer({
@@ -35,7 +35,10 @@ function Drawer({
35
35
  className: cx("uix-drawer", side !== "end" && `uix-drawer--${side}`, className),
36
36
  "aria-labelledby": title ? titleId : void 0,
37
37
  ...rest,
38
- onClick: backdropDismiss(onClose && (() => guard.requestClose()), onClick, dismissOnBackdrop),
38
+ onClick: (event) => {
39
+ onClick?.(event);
40
+ if (dismissOnBackdrop && onClose && isBackdropClick(event)) guard.requestClose(null, event);
41
+ },
39
42
  onKeyDown: (event) => {
40
43
  onKeyDown?.(event);
41
44
  guard.onKeyDown(event);
@@ -51,7 +54,7 @@ function Drawer({
51
54
  ref: closeRef,
52
55
  type: "button",
53
56
  className: "uix-drawer__close",
54
- onClick: () => guard.requestClose(),
57
+ onClick: (event) => guard.requestClose(event.currentTarget, event),
55
58
  "aria-label": closeLabel,
56
59
  "aria-disabled": pending || void 0,
57
60
  children: /* @__PURE__ */ jsx(CloseIcon, {})
@@ -1,13 +1,17 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
- import { DiscardPromptLabels } from './DiscardPrompt.js';
3
+ import { DialogCloseEvent, DiscardPromptLabels } from './DiscardPrompt.js';
4
4
  import './Button.js';
5
5
  import '../link.js';
6
6
  import './Kbd.js';
7
7
 
8
8
  interface ModalProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
9
9
  open: boolean;
10
- onClose?: () => void;
10
+ /**
11
+ * Close the dialog. Gets the click that asked (the close button, a `DialogClose`, "Discard");
12
+ * nothing when the browser closed it (Escape, `<form method="dialog">`).
13
+ */
14
+ onClose?: (event?: DialogCloseEvent) => void;
11
15
  /**
12
16
  * The dialog holds input that is not saved (HAR-2029). Escape, the close button and a
13
17
  * `DialogClose` then open an anchored "Discard changes?" step with focus on "Keep editing"
@@ -48,7 +48,7 @@ function Modal({
48
48
  ref: closeRef,
49
49
  type: "button",
50
50
  className: "uix-dialog__close",
51
- onClick: () => guard.requestClose(),
51
+ onClick: (event) => guard.requestClose(event.currentTarget, event),
52
52
  "aria-label": closeLabel,
53
53
  "aria-disabled": pending || void 0,
54
54
  children: /* @__PURE__ */ jsx(CloseIcon, {})
@@ -97,7 +97,12 @@ interface OrgChartProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelect'
97
97
  }
98
98
  /** The next zoom step above (`dir` 1) or below (`dir` -1) `zoom`. */
99
99
  declare const orgChartZoomStep: (zoom: number, dir: 1 | -1) => number;
100
- /** "Mira Kovač" → "MK"; one word → its first letter. */
100
+ /**
101
+ * "Mira Kovač" → "MK"; one word → its first letter. Bracketed parts ("(extern)", "[TST]") and
102
+ * a title ending in a full stop ("Dr.", "Prof.") are not part of the initials, so
103
+ * "Dr. Anna-Maria Koenig" → "AK" and "Tenant Admin (dev)" → "TA". The first letter of a word
104
+ * is its first letter in any script, never a quote or a hyphen in front of it.
105
+ */
101
106
  declare const orgChartInitials: (name: string) => string;
102
107
  /**
103
108
  * An org chart over `.uix-org-chart` (HAR-1807): managers above their direct reports, joined by
@@ -31,11 +31,14 @@ const orgChartZoomStep = (zoom, dir) => {
31
31
  return [...ZOOM_STEPS].reverse().find((s) => s < zoom - 1e-3) ?? MIN_ZOOM;
32
32
  };
33
33
  const orgChartInitials = (name) => {
34
- const words = name.trim().split(/\s+/).filter(Boolean);
35
- if (words.length === 0) return "";
36
- const first = words[0];
37
- const last = words.length > 1 ? words[words.length - 1] : "";
38
- return (Array.from(first)[0] ?? "").concat(last ? Array.from(last)[0] ?? "" : "").toUpperCase();
34
+ const letterOf = (word) => /\p{L}/u.exec(word)?.[0] ?? "";
35
+ const words = name.replace(/[([{][^)\]}]*[)\]}]/g, " ").trim().split(/\s+/).filter((word) => letterOf(word) !== "");
36
+ const named = words.length > 1 ? words.filter((word, i) => !(word.endsWith(".") && i < words.length - 1)) : words;
37
+ const parts = named.length > 0 ? named : words;
38
+ if (parts.length === 0) return "";
39
+ const first = letterOf(parts[0]);
40
+ const last = parts.length > 1 ? letterOf(parts[parts.length - 1]) : "";
41
+ return `${first}${last}`.toLocaleUpperCase();
39
42
  };
40
43
  const LOADING = "::uix-loading";
41
44
  const FAILED = "::uix-error";
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Pure model for `DashboardCanvas` (TENSOR HAR-1823): a free dashboard layout on a 12-column
3
+ * grid. Every item has a place and a size in grid units — `col` and `row` (0-based), `w`
4
+ * columns wide and `h` rows tall — and may declare the smallest and largest size it accepts.
5
+ *
6
+ * The canvas is free: an item stays where it is put, and nothing floats up into a gap. When a
7
+ * move or a resize lands on other items, they are pushed down below it, in reading order, and
8
+ * whatever they then land on is pushed in turn. `compactCanvas` is the separate, explicit
9
+ * "close the gaps" step for a product that wants one (after removing a widget, say).
10
+ *
11
+ * The same functions back the pointer drag in the component and the keyboard alternative a
12
+ * product puts in each item's menu (Move up / down / left / right, Wider / Narrower, Taller /
13
+ * Shorter), so dragging is never the only way to arrange the canvas (WCAG 2.5.7).
14
+ */
15
+ /** The canvas has twelve columns: 12 divides into halves, thirds, quarters and sixths. */
16
+ declare const DASHBOARD_CANVAS_COLUMNS = 12;
17
+ interface DashboardCanvasItem {
18
+ /** Stable id: the key of the cell and of every change reported for it. */
19
+ readonly id: string;
20
+ /** First column, 0-based (0 – 11). */
21
+ readonly col: number;
22
+ /** First row, 0-based. */
23
+ readonly row: number;
24
+ /** Width in columns. */
25
+ readonly w: number;
26
+ /** Height in rows. */
27
+ readonly h: number;
28
+ /** Narrowest the item may be, in columns. Default 1. */
29
+ readonly minW?: number;
30
+ /** Widest the item may be, in columns. Default 12. */
31
+ readonly maxW?: number;
32
+ /** Shortest the item may be, in rows. Default 1. */
33
+ readonly minH?: number;
34
+ /** Tallest the item may be, in rows. Default: no limit. */
35
+ readonly maxH?: number;
36
+ }
37
+ /** A keyboard move: one column or one row at a time. */
38
+ type DashboardCanvasDirection = 'up' | 'down' | 'left' | 'right';
39
+ /** A keyboard resize: one column or one row at a time. */
40
+ type DashboardCanvasResize = 'wider' | 'narrower' | 'taller' | 'shorter';
41
+ /** Where and how big an item is, in grid units. */
42
+ interface DashboardCanvasPlacement {
43
+ readonly col: number;
44
+ readonly row: number;
45
+ readonly w: number;
46
+ readonly h: number;
47
+ }
48
+ /** An item's size limits, made sane: whole numbers, inside the canvas, minimum never above maximum. */
49
+ declare function canvasItemLimits(item: DashboardCanvasItem): {
50
+ readonly minW: number;
51
+ readonly maxW: number;
52
+ readonly minH: number;
53
+ readonly maxH: number;
54
+ };
55
+ /**
56
+ * The item made valid: whole numbers, its size inside its own limits and the canvas, and its
57
+ * place inside the twelve columns (a too-wide item at the right edge moves left). Limits from
58
+ * an untyped caller are made sane too: a maximum below the minimum is raised to it.
59
+ */
60
+ declare function clampCanvasItem<T extends DashboardCanvasItem>(item: T): T;
61
+ /** True when two different items share at least one cell. */
62
+ declare function canvasItemsOverlap(a: DashboardCanvasPlacement & {
63
+ readonly id: string;
64
+ }, b: DashboardCanvasPlacement & {
65
+ readonly id: string;
66
+ }): boolean;
67
+ /**
68
+ * Reading order: row by row from the top, left to right in a row. Ties keep the order they
69
+ * came in. This is the order the canvas collapses to on a narrow screen and the order a
70
+ * screen reader meets the items in.
71
+ */
72
+ declare function canvasReadingOrder<T extends DashboardCanvasItem>(items: readonly T[]): T[];
73
+ /** The number of rows the canvas uses: one past the lowest item's last row. */
74
+ declare function canvasRowCount(items: readonly DashboardCanvasItem[]): number;
75
+ /**
76
+ * A layout with every item valid and no two sharing a cell. Use it on a stored layout before
77
+ * showing it: an overlap someone saved elsewhere is settled the same way a move settles one.
78
+ */
79
+ declare function normalizeCanvas<T extends DashboardCanvasItem>(items: readonly T[]): T[];
80
+ /**
81
+ * Puts one item at a new place and size (clamped to its limits and the canvas) and pushes
82
+ * what it lands on down. Every other item keeps its place unless it had to move. Returns the
83
+ * items in the order they came in; an unknown id returns them unchanged.
84
+ */
85
+ declare function placeCanvasItem<T extends DashboardCanvasItem>(items: readonly T[], id: string, placement: Partial<DashboardCanvasPlacement>): T[];
86
+ /** One step of a keyboard move, as a new place. Null when the item is already at that edge. */
87
+ declare function canvasMoveTarget(item: DashboardCanvasItem, direction: DashboardCanvasDirection): DashboardCanvasPlacement | null;
88
+ /**
89
+ * One step of a keyboard resize, as a new place and size. `wider` grows to the right, or to the
90
+ * left when the item already touches the right edge. Null when the item is at its limit.
91
+ */
92
+ declare function canvasResizeTarget(item: DashboardCanvasItem, change: DashboardCanvasResize): DashboardCanvasPlacement | null;
93
+ /** Moves an item one column or row. At an edge the items come back unchanged. */
94
+ declare function moveCanvasItem<T extends DashboardCanvasItem>(items: readonly T[], id: string, direction: DashboardCanvasDirection): T[];
95
+ /** Resizes an item by one column or row. At a limit the items come back unchanged. */
96
+ declare function resizeCanvasItem<T extends DashboardCanvasItem>(items: readonly T[], id: string, change: DashboardCanvasResize): T[];
97
+ /**
98
+ * Closes the gaps: in reading order, each item rises as far as it can without landing on one
99
+ * above it. Columns and sizes never change. Not called by the canvas itself — a free canvas
100
+ * keeps the gaps a person leaves — but by a product that wants a tidy layout.
101
+ */
102
+ declare function compactCanvas<T extends DashboardCanvasItem>(items: readonly T[]): T[];
103
+
104
+ export { DASHBOARD_CANVAS_COLUMNS, type DashboardCanvasDirection, type DashboardCanvasItem, type DashboardCanvasPlacement, type DashboardCanvasResize, canvasItemLimits, canvasItemsOverlap, canvasMoveTarget, canvasReadingOrder, canvasResizeTarget, canvasRowCount, clampCanvasItem, compactCanvas, moveCanvasItem, normalizeCanvas, placeCanvasItem, resizeCanvasItem };