@danxbot/ui 20.10.0 → 20.12.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.
@@ -138,19 +138,6 @@ export interface BoardColumnProps<T> {
138
138
  * never colour or glyph alone (WCAG 1.4.1).
139
139
  */
140
140
  icon?: ReactNode;
141
- /**
142
- * DX-4540: the lane's real length when `column.items` holds only the cards
143
- * loaded so far (a windowed lane, see the file header). The count badge, the
144
- * empty-state decision and the WIP limit all read it instead of the loaded
145
- * count, so a lane of 500 shows 500 while rendering 40. Omitted, they read
146
- * `column.count` exactly as before.
147
- *
148
- * `null` is "the length is still being read" (a lane opening, or starting a
149
- * new sort): the count shows a dash, and the lane is NOT empty — its cards
150
- * are on their way, so the empty state stays away and the children (a
151
- * `PagedWindowList` holding a loading placeholder) stay mounted.
152
- */
153
- total?: number | null;
154
141
  /**
155
142
  * DX-4540: controls drawn at the header's end, after the count — a
156
143
  * `SortMenu`, say. A slot of content, not a boolean: supplying it is what
@@ -163,7 +150,7 @@ export interface BoardColumnProps<T> {
163
150
  * lane puts a `PagedWindowList` here — see the file header. */
164
151
  children?: ReactNode;
165
152
  }
166
- export declare function BoardColumn<T>({ column, emptyState, className, listClassName, accent, icon, total, actions, children, }: BoardColumnProps<T>): import("react").JSX.Element;
153
+ export declare function BoardColumn<T>({ column, emptyState, className, listClassName, accent, icon, actions, children, }: BoardColumnProps<T>): import("react").JSX.Element;
167
154
  export interface BoardCardProps {
168
155
  id: BoardItemId;
169
156
  /** Position within its OWN column right now — same contract as
@@ -37,10 +37,19 @@ export interface DragDropContainerProps {
37
37
  * @default "y"
38
38
  */
39
39
  axis?: DragAxis;
40
+ /**
41
+ * DX-4540: for a container that renders only a window of a longer list — the
42
+ * position in the whole list of its first item. Every announcement then
43
+ * speaks the real place ("position 221 of 500"), not the place among what
44
+ * happens to be rendered. Give both or neither.
45
+ */
46
+ offset?: number;
47
+ /** The whole list's length, counting the carried item when it lives here. See `offset`. */
48
+ total?: number;
40
49
  className?: string;
41
50
  children?: ReactNode;
42
51
  }
43
- export declare function DragDropContainer({ id, label, axis, className, children, }: DragDropContainerProps): import("react").JSX.Element;
52
+ export declare function DragDropContainer({ id, label, axis, offset, total, className, children, }: DragDropContainerProps): import("react").JSX.Element;
44
53
  export interface DragDropItemProps {
45
54
  id: DragId;
46
55
  /**
@@ -7,14 +7,14 @@ export interface PagedWindowStrings {
7
7
  retry: string;
8
8
  }
9
9
  export interface PagedWindowItemPosition {
10
- /** Position among the items currently held, across every held page. */
10
+ /** Position among the items currently shown, across every shown page. */
11
11
  local: number;
12
- /** Position in the whole list: `page * pageSize + index on the page`. */
12
+ /** Position in the whole list: `source.offset + local`. */
13
13
  global: number;
14
14
  }
15
15
  export interface PagedWindowListProps<T> {
16
16
  /** The window to render — what `usePagedWindow` returned. */
17
- window: PagedWindow<T>;
17
+ source: PagedWindow<T>;
18
18
  /** Stable across renders and pages. Never an array index. */
19
19
  getKey: (item: T) => string | number;
20
20
  renderItem: (item: T, position: PagedWindowItemPosition) => ReactNode;
@@ -28,4 +28,4 @@ export interface PagedWindowListProps<T> {
28
28
  strings?: Partial<PagedWindowStrings>;
29
29
  className?: string;
30
30
  }
31
- export declare function PagedWindowList<T>({ window: win, getKey, renderItem, estimatedItemHeight, strings, className, }: PagedWindowListProps<T>): import("react").JSX.Element;
31
+ export declare function PagedWindowList<T>({ source, getKey, renderItem, estimatedItemHeight, strings, className, }: PagedWindowListProps<T>): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ import type { BlockStatus } from "../../lib/paged-window/layout";
3
+ export interface PageBlockProps {
4
+ page: number;
5
+ status: BlockStatus;
6
+ /** The cards of a `ready` page. Ignored for every other status: they have none to show. */
7
+ children?: ReactNode;
8
+ /** How many cards a `ready` page shows (`data-count`, which the pitch measurement reads). */
9
+ count: number;
10
+ /** The height the page holds when it has no cards: items on it x the measured item height. */
11
+ height: number;
12
+ /** One item's height, for the loading rows. */
13
+ pitch: number;
14
+ /** Ref callback that returns its cleanup: how the list's observers find this block. */
15
+ blockRef: Ref<HTMLDivElement>;
16
+ errorText: string;
17
+ retryText: string;
18
+ onRetry: () => void;
19
+ }
20
+ /**
21
+ * THE ONE ELEMENT A PAGE IS, in every state. A page turns from placeholder to
22
+ * cards (a load), to placeholder again (a drop) and to an error row and back
23
+ * without this element being replaced: it is the same component type at the same
24
+ * key, so React keeps the same DOM node. That is what keeps the browser's scroll
25
+ * anchoring on the element under the viewport and keeps the observers attached
26
+ * (visibility and pitch) instead of detaching and re-attaching on every change.
27
+ * What changes is only what it holds and how it is drawn: the cards of a ready
28
+ * page, otherwise the page's height (and, while loading, faint rows; on failure,
29
+ * a Retry row).
30
+ */
31
+ export declare function PageBlock({ page, status, children, count, height, pitch, blockRef, errorText, retryText, onRetry, }: PageBlockProps): import("react").JSX.Element;
@@ -113,10 +113,10 @@ export { DragDrop, DragDropContainer, DragDropItem, DragDropHandle, DragDropSurf
113
113
  export { KanbanBoard, type KanbanBoardProps, type KanbanLane, type KanbanMove, type KanbanRefusal, } from "./components/kanban/KanbanBoard";
114
114
  export { DEFAULT_DRAG_STRINGS, DRAG_INSTRUCTIONS_ID, DragInstructions, DragLiveRegion, DragSession, DragSessionProvider, applyMove, attachAutoScroll, attachKeyboardSensor, attachPointerSensor, attachSessionWindowListeners, isNoOp, resolveTarget, sameTarget, scrollVelocity, useCreateDragSession, useDragAnnouncements, useDragSession, useDragState, useDragLive, useDraggable, useDropContainer, type ContainerId, type DragAxis, type DragCancelEvent, type DragEndEvent, type DragId, type DragMode, type DragPhase, type DragSessionCallbacks, type DragStartEvent, type DragState, type AutoScrollOptions, type DragAnnouncementContext, type DragAnnouncementOptions, type DragAnnouncements, type DragInstructionsProps, type DragLiveRegionProps, type DragStrings, type DropTarget, type KeyboardSensorOptions, type PointerActivation, } from "./lib/dnd";
115
115
  export { Board, BoardColumn, BoardCard, BoardCardSurface, type BoardProps, type BoardColumnProps, type BoardCardProps, type BoardCardSurfaceProps, } from "./components/Board";
116
- export { buildBoardModel, resolveMove, applyBoardMoves, useBoard, type BoardModel, type UseBoardResult, } from "./lib/board";
117
- export type { BoardAccessors, BoardColumnDef, BoardColumnId, BoardColumnModel, BoardItemId, BoardMove, BoardMoveRequest, BoardMoveSource, BoardMoveVerdict, UseBoardOptions, } from "./lib/board";
116
+ export { buildBoardModel, resolveMove, applyBoardMoves, useBoard, moveAnchor, columnLength, positionInColumn, type BoardModel, type MoveAnchor, type UseBoardResult, } from "./lib/board";
117
+ export type { BoardAccessors, BoardColumnDef, BoardEdge, BoardColumnId, BoardColumnModel, BoardItemId, BoardMove, BoardMoveRequest, BoardMoveSource, BoardMoveVerdict, UseBoardOptions, } from "./lib/board";
118
118
  export { PagedWindowList, type PagedWindowItemPosition, type PagedWindowListProps, type PagedWindowStrings, } from "./components/PagedWindowList";
119
- export { PagedWindowStore, usePagedWindow, windowPageNumbers, type LoadPage, type LoadPageContext, type PageResponse, type PageSnapshot, type PageStatus, type PagedWindow, type PagedWindowOptions, type PagedWindowSnapshot, type UsePagedWindowOptions, } from "./lib/paged-window";
119
+ export { PagedWindowStore, usePagedWindow, windowPageNumbers, type LoadCount, type LoadPage, type LoadPageContext, type PageResponse, type PageSnapshot, type PageStatus, type PagedWindow, type PagedWindowMode, type PagedWindowOptions, type PagedWindowSnapshot, type UsePagedWindowOptions, } from "./lib/paged-window";
120
120
  export type { MessageRole, MessagePart, TextPart, CodePart, ReasoningPart, ToolPart, ToolStatus, FilePart, ComponentPart, ChatFile, ChatMessage, RenderContext, PartRenderer, PartRenderers, ChatComponentRenderer, ChatComponents, } from "./components/chat/types";
121
121
  export { Chat, ChatPopover, type ChatProps, type ChatPopoverProps, type ChatSurface, } from "./components/chat/Chat";
122
122
  export { Message, MessagePartView, ToolCall, type MessageProps, type MessageAction, type MessagePartViewProps, type ToolCallProps, } from "./components/chat/Message";
@@ -0,0 +1,53 @@
1
+ /**
2
+ * ==== MOVE ANCHOR ====
3
+ *
4
+ * What a server needs to place a card the board moved, when the board holds
5
+ * only a window of the column (DX-4540). A drop knows exactly one thing about
6
+ * where it landed: which LOADED neighbours it is between. It never knows the
7
+ * card's position in the whole column, and a client that tries to compute one
8
+ * from `to.index` is wrong the moment the window does not start at the top.
9
+ * So the request names the neighbours, and the server finds the place.
10
+ *
11
+ * `to.index` counts the destination's LOADED cards with the moved card already
12
+ * removed (the board's one index rule), so the card lands before
13
+ * `rest[to.index]`, where `rest` is the destination's loaded cards without the
14
+ * moved one. ONE neighbour is named, because a server places a card relative to
15
+ * one: the card it lands directly above (`beforeId`) whenever there is one, and
16
+ * only at the end of the loaded run, where nothing is below, the card it lands
17
+ * directly below (`afterId`). An empty destination names nothing.
18
+ *
19
+ * AN EDGE INTENT TRUMPS THE NEIGHBOURS. "Move to top" in a window that starts
20
+ * at card 221 means the top of the column, which no loaded neighbour can say,
21
+ * so the anchor carries the edge and no ids.
22
+ */
23
+ import type { BoardEdge, BoardItemId, BoardMove } from "./types";
24
+ /**
25
+ * EXACTLY ONE placement field is set — a server's lane-move takes one — or none
26
+ * for an empty destination:
27
+ * beforeId the moved card lands directly above this loaded card
28
+ * afterId it lands directly below this one: only when it lands after the last
29
+ * loaded card, where nothing is below to name
30
+ * edge it goes to the very top or bottom of the whole column
31
+ */
32
+ export type MoveAnchor = {
33
+ beforeId: BoardItemId;
34
+ afterId?: never;
35
+ edge?: never;
36
+ } | {
37
+ afterId: BoardItemId;
38
+ beforeId?: never;
39
+ edge?: never;
40
+ } | {
41
+ edge: BoardEdge;
42
+ beforeId?: never;
43
+ afterId?: never;
44
+ } | {
45
+ beforeId?: never;
46
+ afterId?: never;
47
+ edge?: never;
48
+ };
49
+ /**
50
+ * @param destinationItems the destination column's LOADED cards (the moved card
51
+ * may be among them, for a same-column move).
52
+ */
53
+ export declare function moveAnchor<T>(destinationItems: readonly T[], move: BoardMove, getId: (item: T) => BoardItemId): MoveAnchor;
@@ -8,7 +8,9 @@
8
8
  * NOT wired into the package's top-level `src/index.ts` — that barrel is
9
9
  * owned by the contract's author, not this agent (see this round's brief).
10
10
  */
11
- export type { BoardAccessors, BoardColumnDef, BoardColumnId, BoardColumnModel, BoardItemId, BoardMove, BoardMoveRequest, BoardMoveSource, BoardMoveVerdict, UseBoardOptions, } from "./types";
11
+ export type { BoardAccessors, BoardColumnDef, BoardEdge, BoardColumnId, BoardColumnModel, BoardItemId, BoardMove, BoardMoveRequest, BoardMoveSource, BoardMoveVerdict, UseBoardOptions, } from "./types";
12
12
  export { buildBoardModel, type BoardModel } from "./model";
13
+ export { moveAnchor, type MoveAnchor } from "./anchor";
14
+ export { columnLength, positionInColumn } from "./position";
13
15
  export { applyBoardMoves, resolveMove, type ResolveMoveContext, type ResolvedMove, } from "./move";
14
16
  export { useBoard, type UseBoardResult } from "./use-board";
@@ -146,6 +146,15 @@ export declare function resolveMove<T>(context: ResolveMoveContext<T>, request:
146
146
  * column membership on every subsequent `buildBoardModel` call, and
147
147
  * `buildBoardModel` only ever reads WITHIN-column order.
148
148
  *
149
+ * `to.edge` IS NOT APPLIED HERE. This arranges the cards it was given, and in a
150
+ * windowed column an edge move lands OUTSIDE the cards that are loaded — at the
151
+ * top or bottom of a list this function cannot see — so the index it would
152
+ * splice at is only the nearest loaded position, not the place the card is
153
+ * going. A consumer of a windowed column sends the move to its server (see
154
+ * `moveAnchor`) and re-reads the affected pages (`invalidateAll`); it does not
155
+ * apply an edge move locally. In a column with every card loaded the edge IS
156
+ * the index, and this function is exactly right.
157
+ *
149
158
  * NEVER WRITES A FIELD ON ANY ITEM — ONLY REPOSITIONS. `BoardAccessors` has
150
159
  * no setter, on purpose (the file header on `types.ts` — items are opaque),
151
160
  * so this function CANNOT change what `getColumnId` reports for a moved
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ==== COLUMN POSITION ====
3
+ *
4
+ * Two facts about a column that three callers each need and must not each work
5
+ * out for themselves (DX-4540): how long the column really is, and where in the
6
+ * whole column a loaded card sits. A windowed column holds only a few pages of
7
+ * a longer list, so the cards loaded are not the column.
8
+ *
9
+ * `buildBoardModel` decides the length once (the model's `count`); `resolveMove`
10
+ * works from the column DEFINITIONS before any model exists and must decide
11
+ * "is this card already at that edge" with the same rule; and the move menu
12
+ * decides what to offer from a card's place in the column. All three read these.
13
+ */
14
+ import type { BoardColumnDef } from "./types";
15
+ /**
16
+ * The column's length: its `total` when the definition gave a number, otherwise
17
+ * the cards loaded (`total` absent means every card is loaded; `null` means the
18
+ * length is still being read, so the loaded cards are all that is known).
19
+ */
20
+ export declare function columnLength(def: Pick<BoardColumnDef, "total"> | undefined, loaded: number): number;
21
+ /** The place in the whole column of the loaded card at `index`: the window's offset plus the index. */
22
+ export declare function positionInColumn(def: Pick<BoardColumnDef, "offset"> | undefined, index: number): number;
@@ -74,11 +74,42 @@ export interface BoardColumnDef {
74
74
  * Soft cap on item count. Exceeding it is a VISUAL event, not a blocked move
75
75
  * — a WIP limit exists to interrupt a human, and a board that refuses the
76
76
  * drop instead just teaches people to work around the board. The core reports
77
- * `overWip`; the view layer is responsible for making it loud.
77
+ * `overWip`; the view layer is responsible for making it loud. The limit is
78
+ * judged against the column's real length (`total`), not what is loaded.
78
79
  */
79
80
  wipLimit?: number;
81
+ /**
82
+ * DX-4540: the column's real length when `items` holds only the cards loaded so
83
+ * far (a windowed lane). The model's `count` is this number, and `overWip`,
84
+ * the move menu's "is there anything above/below" and the drag announcements
85
+ * all read it. `null` means the length is still being read: the column is
86
+ * neither empty nor over its limit, and `count` falls back to the loaded
87
+ * cards. Omitted, every card of the column is loaded and the count is theirs.
88
+ */
89
+ total?: number | null;
90
+ /**
91
+ * DX-4540: the position in the whole column of the first loaded card, so a
92
+ * card at index 0 of `items` is spoken as "position 221", not "position 1".
93
+ * Only meaningful with `total`; the loaded cards must be one contiguous run
94
+ * of the column. @default 0
95
+ */
96
+ offset?: number;
80
97
  }
81
- /** One item's move. `to.index` counts the destination list without this item. */
98
+ /**
99
+ * Where a move lands beyond what is loaded. In a windowed column `to.index` can
100
+ * only name a position among the LOADED cards, so a move to "the very top" or
101
+ * "the very bottom" of the column says so here and the consumer sends that
102
+ * intent to its server (see `moveAnchor`). In a column with every card loaded
103
+ * the edge is the same place as `index` 0 / the end, so a consumer that ignores
104
+ * it is still right.
105
+ */
106
+ export type BoardEdge = "top" | "bottom";
107
+ /**
108
+ * One item's move. `to.index` counts the destination list without this item —
109
+ * the LOADED list, in a windowed column. `to.edge`, when present, is the
110
+ * intent the index cannot carry; the index is then the nearest loaded position
111
+ * (0 for `"top"`, one past the last loaded card for `"bottom"`).
112
+ */
82
113
  export interface BoardMove {
83
114
  itemId: BoardItemId;
84
115
  from: {
@@ -88,6 +119,7 @@ export interface BoardMove {
88
119
  to: {
89
120
  columnId: BoardColumnId;
90
121
  index: number;
122
+ edge?: BoardEdge;
91
123
  };
92
124
  }
93
125
  /**
@@ -125,11 +157,22 @@ export type BoardMoveVerdict = {
125
157
  export interface BoardColumnModel<T> {
126
158
  id: BoardColumnId;
127
159
  label: string;
160
+ /** The cards loaded. All of the column's cards unless the column is windowed. */
128
161
  items: readonly T[];
129
- /** Count is on the model because the view needs it before it renders rows. */
162
+ /**
163
+ * The column's length: its `total` when the definition gave one, else the
164
+ * loaded cards. Count is on the model because the view needs it before it
165
+ * renders rows — and because it is computed ONCE here: the badge, the empty
166
+ * state, the WIP verdict, the move menu and the announcements all read this
167
+ * number, never their own.
168
+ */
130
169
  count: number;
170
+ /** `true` while a windowed column's length is still being read (`total: null`). `count` is then the loaded cards. */
171
+ counting: boolean;
172
+ /** Position in the whole column of `items[0]`; 0 unless the column is windowed. */
173
+ offset: number;
131
174
  wipLimit: number | undefined;
132
- /** `true` when `count` exceeds `wipLimit`. Always false with no limit set. */
175
+ /** `true` when `count` exceeds `wipLimit`. Always false with no limit set, and while `counting`. */
133
176
  overWip: boolean;
134
177
  }
135
178
  export interface UseBoardOptions<T> {
@@ -7,7 +7,7 @@ export interface DragAnnouncementContext {
7
7
  label: string;
8
8
  container: ContainerId;
9
9
  containerLabel: string;
10
- /** 1-based, as spoken. */
10
+ /** 1-based, as spoken, in the WHOLE container — a windowed container reports its real offset and length (DX-4540). */
11
11
  position: number;
12
12
  /**
13
13
  * The list length WITH the carried item counted in it, so `position` is never
@@ -94,6 +94,18 @@ export declare class DragSession {
94
94
  cancel(): void;
95
95
  /** The container the pointer is over right now, or null. */
96
96
  containerAtPoint(point: Point): string | null;
97
+ /**
98
+ * Where a drop target sits, as a person is told it: 1-based, in the WHOLE
99
+ * container, with the whole container's length counted with the carried item
100
+ * in it. A container that renders every item is its own whole (the items
101
+ * registered under it); a windowed one says so at registration (`offset`,
102
+ * `total`) and the answer follows. `index` counts the container with the
103
+ * carried item already removed — the engine's one index rule.
104
+ */
105
+ spokenPosition(container: string, index: number, carried: DragId): {
106
+ position: number;
107
+ total: number;
108
+ };
97
109
  /** How many items a container holds once the carried one is taken out. */
98
110
  slotCount(container: string): number;
99
111
  }
@@ -95,6 +95,15 @@ export interface ContainerRegistration {
95
95
  id: ContainerId;
96
96
  element: HTMLElement;
97
97
  axis: DragAxis;
98
+ /**
99
+ * DX-4540: for a container that renders only a window of a longer list. The
100
+ * position in the whole list of its first registered item, so a drop spoken
101
+ * as "position 221 of 500" is true. Absent for a container that holds every
102
+ * item.
103
+ */
104
+ offset?: number;
105
+ /** The whole list's length, the carried item counted in it when it lives here. See `offset`. */
106
+ total?: number;
98
107
  }
99
108
  /** One item's measured box, taken at snapshot time. */
100
109
  export interface ItemRect {
@@ -55,6 +55,10 @@ export declare function useDraggable(session: DragSession, options: UseDraggable
55
55
  export interface UseDropContainerOptions {
56
56
  id: ContainerId;
57
57
  axis?: DragAxis;
58
+ /** See `ContainerRegistration.offset` — a windowed container's first item's place in the whole list. */
59
+ offset?: number;
60
+ /** See `ContainerRegistration.total` — a windowed container's whole length. */
61
+ total?: number;
58
62
  }
59
63
  export interface UseDropContainerResult {
60
64
  setContainerRef: (el: HTMLElement | null) => void;
@@ -0,0 +1,15 @@
1
+ export type ScrollAxis = "x" | "y";
2
+ export interface ScrollBoxOptions {
3
+ /** The axes that count; a box scrolling along any of them qualifies. @default ["y"] */
4
+ axes?: readonly ScrollAxis[];
5
+ /** Also require the content to overflow the box on that axis, not merely allow scrolling. @default false */
6
+ mustOverflow?: boolean;
7
+ }
8
+ export interface ScrollAncestorOptions extends ScrollBoxOptions {
9
+ /** Whether `from` itself may be the answer. Required: it is a statement about the caller. */
10
+ includeSelf: boolean;
11
+ }
12
+ /** Does `el` scroll along any of the given axes? */
13
+ export declare function isScrollBox(el: HTMLElement, { axes, mustOverflow }?: ScrollBoxOptions): boolean;
14
+ /** The nearest box that scrolls, walking up from `from`; null when none does. */
15
+ export declare function nearestScrollAncestor(from: Element | null, { includeSelf, ...box }: ScrollAncestorOptions): HTMLElement | null;
@@ -1,2 +1,2 @@
1
- export { PagedWindowStore, windowPageNumbers, type LoadPage, type LoadPageContext, type PageResponse, type PageSnapshot, type PageStatus, type PagedWindowOptions, type PagedWindowSnapshot, } from "./store";
1
+ export { PagedWindowStore, windowPageNumbers, type LoadCount, type LoadPage, type LoadPageContext, type PageResponse, type PageSnapshot, type PageStatus, type PagedWindowMode, type PagedWindowOptions, type PagedWindowSnapshot, } from "./store";
2
2
  export { usePagedWindow, type PagedWindow, type UsePagedWindowOptions } from "./use-paged-window";
@@ -0,0 +1,21 @@
1
+ import type { PageSnapshot } from "./store";
2
+ /**
3
+ * What a page's block is showing. One name for every case, so the list renders
4
+ * ONE kind of block and only this changes:
5
+ * ready the page's cards are shown
6
+ * loading held, its request is in flight
7
+ * error held, its request failed
8
+ * waiting held and ready, but past a hole in the loaded run — not shown until
9
+ * the page that closes the hole arrives (see the store header)
10
+ * unheld outside the window: empty space of the right height
11
+ */
12
+ export type BlockStatus = "ready" | "loading" | "error" | "waiting" | "unheld";
13
+ export declare function blockStatus<T>(held: PageSnapshot<T> | undefined): BlockStatus;
14
+ /**
15
+ * How many page blocks to render. One per page of the list; before the first
16
+ * response the length is unknown but page 0 is being asked for, and one block
17
+ * stands in for it, so a new query does not flash an empty list.
18
+ */
19
+ export declare function blockCountFor(pageCount: number, heldPageCount: number): number;
20
+ /** The height of a page's placeholder: the items it holds (the last page is short) times one item's height. */
21
+ export declare function placeholderHeight(page: number, pageSize: number, total: number, pitch: number): number;
@@ -9,6 +9,9 @@ export interface LoadPageContext {
9
9
  signal: AbortSignal;
10
10
  }
11
11
  export type LoadPage<T> = (page: number, context: LoadPageContext) => Promise<PageResponse<T>>;
12
+ /** Answers the list's length alone — no items. What a hidden (`count` mode) lane asks. */
13
+ export type LoadCount = (context: LoadPageContext) => Promise<number>;
14
+ export type PagedWindowMode = "pages" | "count";
12
15
  export type PageStatus = "loading" | "ready" | "error";
13
16
  export interface PageSnapshot<T> {
14
17
  page: number;
@@ -19,8 +22,11 @@ export interface PageSnapshot<T> {
19
22
  refreshing: boolean;
20
23
  /** What the load rejected with. Only on `error`. */
21
24
  error: unknown;
25
+ /** Part of the loaded run — its items are in `items`. A ready page past a hole is not, until the hole closes. */
26
+ shown: boolean;
22
27
  }
23
28
  export interface PagedWindowSnapshot<T> {
29
+ mode: PagedWindowMode;
24
30
  /** The list's length as the latest response reported it; null before the first. */
25
31
  total: number | null;
26
32
  pageSize: number;
@@ -32,14 +38,22 @@ export interface PagedWindowSnapshot<T> {
32
38
  epoch: number;
33
39
  /** The held pages, ascending. At most `windowPages` of them. */
34
40
  pages: readonly PageSnapshot<T>[];
35
- /** Every item of every `ready` held page, in list order. At most `windowPages * pageSize`. */
41
+ /** The loaded run: the items of the shown pages, in list order. At most `windowPages * pageSize`. */
36
42
  items: readonly T[];
43
+ /** The position in the whole list of `items[0]`; 0 when nothing is loaded. */
44
+ offset: number;
45
+ /** What the last `loadCount` rejected with; undefined otherwise. */
46
+ countError: unknown;
37
47
  }
38
48
  export interface PagedWindowOptions<T> {
39
49
  pageSize: number;
40
50
  /** How many pages are held at once. @default 3 */
41
51
  windowPages?: number;
42
52
  loadPage: LoadPage<T>;
53
+ /** Required to use `count` mode. */
54
+ loadCount?: LoadCount;
55
+ /** @default "pages" */
56
+ mode?: PagedWindowMode;
43
57
  }
44
58
  /**
45
59
  * The pages to hold for an anchor: `windowPages` of them centred on it (the
@@ -51,61 +65,84 @@ export declare class PagedWindowStore<T> {
51
65
  private readonly pageSize;
52
66
  private readonly windowPages;
53
67
  private loadPage;
68
+ private loadCount;
54
69
  private held;
55
70
  private total;
56
71
  private anchor;
57
72
  /** The anchor the held pages were last synced to; differs from `anchor` only while frozen. */
58
73
  private appliedAnchor;
59
- private enabled;
74
+ private mode;
60
75
  private frozen;
61
76
  private epoch;
62
77
  private nextToken;
78
+ private countRequest;
79
+ /** Answers that arrived while frozen, applied in arrival order on thaw. */
80
+ private heldAnswers;
81
+ /** Pages whose refresh was asked for while frozen. */
82
+ private queuedPages;
83
+ /** The length's refresh (count mode) was asked for while frozen. */
84
+ private queuedCount;
85
+ private countError;
63
86
  private snapshot;
64
87
  private listeners;
65
- constructor({ pageSize, windowPages, loadPage }: PagedWindowOptions<T>);
88
+ constructor({ pageSize, windowPages, loadPage, loadCount, mode }: PagedWindowOptions<T>);
66
89
  /**
67
- * Replaced on every render by the React binding, so a consumer's loader can
68
- * close over fresh state without the store being rebuilt (rebuilding drops
69
- * every held page). A change in WHAT is loaded is a `reset`, never this.
90
+ * Replace the loaders. The React binding calls this after every commit so a
91
+ * consumer's loaders can close over fresh state without the store being
92
+ * rebuilt (rebuilding drops every held page). A change in WHAT is loaded is a
93
+ * `reset`, never this.
70
94
  */
71
- setLoader(loadPage: LoadPage<T>): void;
95
+ setLoaders(loadPage: LoadPage<T>, loadCount: LoadCount | undefined): void;
72
96
  getSnapshot: () => PagedWindowSnapshot<T>;
73
97
  subscribe: (listener: () => void) => (() => void);
74
98
  /** The viewer moved to another page. Ignored while frozen, applied on thaw. */
75
99
  setAnchorPage(page: number): void;
76
100
  /**
77
- * While true the window neither loads nor drops pages, whatever the anchor
78
- * does. A drag needs it: the carried card is mounted in its page, and
79
- * dropping that page mid-drag would unmount the card and cancel the drag.
101
+ * While true the window neither loads nor drops pages, whatever the anchor or
102
+ * the total does. A drag needs it: the carried card is mounted in its page,
103
+ * and dropping that page mid-drag would unmount the card and cancel the drag.
80
104
  */
81
105
  setFrozen(frozen: boolean): void;
82
- /** Off, nothing is held or requested. Back on, the window loads again. */
83
- setEnabled(enabled: boolean): void;
106
+ /** `count` holds and requests no cards, only the length (see the file header). */
107
+ setMode(mode: PagedWindowMode): void;
84
108
  /** The query changed: forget everything, return to the first page. */
85
109
  reset(): void;
86
110
  /** Re-read one held page in place. A page that is not held loads fresh when it is next wanted. */
87
111
  invalidatePage(page: number): void;
88
- /** Re-read every held page in place. */
112
+ /** Re-read every held page in place; in `count` mode, re-ask the length. */
89
113
  invalidateAll(): void;
90
- /** Load the first window. The binding calls it on mount, and again after a `stop`. */
114
+ /** Load the first window (or the length). The binding calls it on mount, and again after a `stop`. */
91
115
  start(): void;
92
- /** Abort every request and release every held page. The binding calls it on unmount. */
116
+ /** Abort every request and release every page. The binding calls it on unmount. */
93
117
  stop(): void;
118
+ private requireCountLoader;
94
119
  private pageCount;
95
120
  /**
96
- * Pages to hold now. Page 0 alone before the first response (the count is
97
- * unknown) and for an empty list: an empty page is still held, because it is
98
- * what `invalidateAll` re-reads when the list gets its first item — an empty
99
- * lane that holds nothing could never find out it was no longer empty.
121
+ * Pages to hold now. None in `count` mode. Page 0 alone before the first
122
+ * response (the count is unknown) and for an empty list: an empty page is
123
+ * still held, because it is what `invalidateAll` re-reads when the list gets
124
+ * its first item — an empty lane that holds nothing could never find out it
125
+ * was no longer empty.
100
126
  */
101
127
  private wanted;
128
+ /** Bring the held pages to the wanted ones — unless frozen, when that waits for the thaw. */
102
129
  private sync;
130
+ private reconcile;
131
+ /** Everything the frozen window put off, in the order the header promises. */
132
+ private thaw;
103
133
  /** Start (or restart) a page's request; whatever was in flight is abandoned. */
104
134
  private request;
105
135
  private settle;
106
- /** The list shrank: a held page past its end has nothing to show. */
107
- private dropBeyondCount;
136
+ private requestCount;
137
+ private settleCount;
138
+ private abortCount;
108
139
  private dropAll;
140
+ /**
141
+ * The longest unbroken stretch of ready pages inside the list, lowest on a
142
+ * tie. Pages past the list's end (it shrank while the window was frozen) are
143
+ * not part of any run.
144
+ */
145
+ private loadedRun;
109
146
  private build;
110
147
  private publish;
111
148
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * How tall one item really is — measured, not guessed. DX-4540.
3
+ *
4
+ * A page that is not held is drawn as a placeholder of `items x pitch`, so the
5
+ * scrollbar is only as true as the pitch. `estimated` stands in until pages
6
+ * have been seen; after that the pitch is the MEAN over every item measured,
7
+ * which is a ResizeObserver over the page blocks.
8
+ *
9
+ * EVERY block is observed, because a block turns from placeholder to cards (and
10
+ * back) without its element being replaced: only a block that currently holds
11
+ * cards (`data-count`) is a measurement of what a card costs; a placeholder's
12
+ * own size is the estimate drawn back at us. A block that stops holding cards,
13
+ * or is removed, leaves its LAST reading in the mean (`departed`), so dropping
14
+ * a page does not make the estimate lurch and every placeholder above the
15
+ * viewer resize with it.
16
+ *
17
+ * A change under half a pixel is not published: re-laying-out every placeholder
18
+ * in a long list for sub-pixel drift is cost with no picture.
19
+ *
20
+ * NOTHING IS EVER RESET, AND THAT IS DELIBERATE. A new query (another sort, a
21
+ * different time range) lists the same kind of card, so what was learned about
22
+ * its height still holds; clearing it would snap every placeholder back to the
23
+ * estimate and make the first screen of the new query jump. The measurement is
24
+ * only a sizing aid for space no card occupies, and the mean keeps correcting
25
+ * itself from every page that is shown.
26
+ *
27
+ * A block whose size does not change when it turns from placeholder to cards
28
+ * (or back) fires no resize, so its reading is picked up at the next size change;
29
+ * a placeholder is sized from the estimate in the first place.
30
+ */
31
+ export declare function useMeasuredPitch(estimated: number): {
32
+ pitch: number;
33
+ observe: (el: HTMLElement) => () => void;
34
+ };
@@ -0,0 +1,20 @@
1
+ import type { ScrollRoot } from "./use-scroll-root";
2
+ /**
3
+ * The page the window should centre on: the middle one (lower middle when
4
+ * even) of the pages whose blocks intersect the root. A viewport shorter than
5
+ * a page sees one or two; the window of three covers either. Null when nothing
6
+ * is in view (a hidden scroller), so the window keeps where it was.
7
+ */
8
+ export declare function centrePage(visible: ReadonlySet<Element>): number | null;
9
+ /**
10
+ * Which pages are in view, from one IntersectionObserver over every page block,
11
+ * rooted at the scroller — no scroll listener, no polling. DX-4540.
12
+ *
13
+ * `observe(el)` is what a block's ref calls; it returns the cleanup. It is
14
+ * stable, and blocks that mount before the observer exists (it needs the
15
+ * resolved scroller) are picked up when it does. Whenever the set in view
16
+ * changes, `onCentre` is told the page to centre on.
17
+ */
18
+ export declare function usePageVisibility(scroller: ScrollRoot | null, onCentre: (page: number) => void): {
19
+ observe: (el: HTMLElement) => () => void;
20
+ };