@danxbot/ui 3.6.1 → 3.8.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.
@@ -55,3 +55,36 @@ export declare function MenuGroup({ label, children }: MenuGroupProps): import("
55
55
  export declare function MenuSeparator({ className }: {
56
56
  className?: string;
57
57
  }): import("react").JSX.Element;
58
+ export interface MenuSubProps {
59
+ /** The row that opens it — reads like a MenuItem and carries a trailing caret. */
60
+ label: ReactNode;
61
+ /** Optional leading icon, matching `MenuItem`'s own slot. */
62
+ icon?: ReactNode;
63
+ disabled?: boolean;
64
+ className?: string;
65
+ children: ReactNode;
66
+ }
67
+ /**
68
+ * A nested menu that flies out beside its parent.
69
+ *
70
+ * WHY THIS IS A COMPONENT AND NOT A FLAT GROUP. `MenuGroup` renders its items
71
+ * inline under a label, which is right for two or three related actions and
72
+ * wrong the moment the group is a SET YOU PICK FROM — teams, workspaces,
73
+ * projects. Inline, that set pushes every other item down the menu, grows
74
+ * without bound as the account gains members, and puts a one-of-many choice at
75
+ * the same visual weight as "Sign out". A submenu keeps the parent menu a fixed
76
+ * size and says, correctly, that the thing behind this row is a list.
77
+ *
78
+ * OPENING IS HOVER *AND* KEYBOARD, which Base UI gives us: pointer-over opens
79
+ * after its own small delay, ArrowRight/Enter opens from the keyboard, and
80
+ * ArrowLeft/Escape closes back to the parent. Building this from a nested
81
+ * `Menu` would lose all of it — an inner Root gets its own trigger semantics
82
+ * and its own focus scope, so keyboard users end up in a popup the parent does
83
+ * not know it owns.
84
+ *
85
+ * It is positioned `side="right"` with a small negative `alignOffset` so the
86
+ * first child row sits level with the row that opened it; Base UI flips it to
87
+ * the left edge on its own when there is no room, which is the case a hard
88
+ * `left` would get wrong on a narrow window.
89
+ */
90
+ export declare function MenuSub({ label, icon, disabled, className, children }: MenuSubProps): import("react").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { DragSessionCallbacks } from "../../lib/dnd";
3
+ import { type DragDropProps } from "../DragDrop";
3
4
  import type { KanbanLane, KanbanMove, KanbanRefusal } from "./types";
4
5
  export type { KanbanLane, KanbanMove, KanbanRefusal };
5
6
  export interface KanbanBoardProps<T> extends Omit<DragSessionCallbacks, "onDragEnd"> {
@@ -40,6 +41,51 @@ export interface KanbanBoardProps<T> extends Omit<DragSessionCallbacks, "onDragE
40
41
  onRefuse?: (refusal: KanbanRefusal) => void;
41
42
  /** Shown in a lane with no cards. */
42
43
  emptyLane?: ReactNode;
44
+ /**
45
+ * Override any part of the announcement grammar — `DragDrop.strings`, reached
46
+ * through the board.
47
+ *
48
+ * IT WAS UNREACHABLE, and the omission had a symptom. The default sentence is
49
+ * `… Position 1 of 4 in ${containerLabel}`, which reads correctly right up
50
+ * until a lane is called "In review" and a screen reader says "in In review".
51
+ * That is a property of the lane's NAME rather than of the template, so there
52
+ * is no template this component could ship that is right for every board —
53
+ * which is precisely why the engine exposes the grammar, and precisely why a
54
+ * board that swallowed the prop left its consumer with no way out of a
55
+ * sentence they could hear was wrong. Rename the lane, or write the sentence:
56
+ *
57
+ * ```tsx
58
+ * strings={{ picked: (c) => `Picked up ${c.label}, ${c.containerLabel} column.` }}
59
+ * ```
60
+ */
61
+ strings?: DragDropProps["strings"];
62
+ /**
63
+ * Fill the space between the board's own top and the bottom of whatever
64
+ * scrolls it, so the lanes are the only thing that scrolls down.
65
+ *
66
+ * `true` — the default, and what a board IS. It was not the default until
67
+ * 3.7.0, and the reason is worth keeping: the height was expressed purely as
68
+ * a `flex-1 min-h-0` chain, which is correct and is correct only while every
69
+ * ancestor cooperates. One ordinary page wrapper above the board — a bare
70
+ * `<div className="flex flex-col gap-10">`, which is this package's own demo
71
+ * layout — makes `flex-1` inert, the board sizes to its tallest lane, and the
72
+ * PAGE takes the scrollbar that was supposed to belong to a column. Reported
73
+ * from a running board: "the columns should fill to the full remaining height
74
+ * of the page. No scroll bar vertically. Only scroll vertically per column."
75
+ *
76
+ * The height is MEASURED (`lib/use-fill-height`), not computed from a token,
77
+ * and re-measured whenever anything above the board changes size. It is
78
+ * paired with `max-h-full`, so an ancestor that genuinely does bound the board
79
+ * still wins and a board in a short box does not overflow it.
80
+ *
81
+ * `false` — the board is as tall as the box it is given, or as its tallest
82
+ * lane if nothing gives it one. For a board deliberately sized by its
83
+ * container (`h-[60dvh]`, a fixed-height card) or embedded in a page that is
84
+ * meant to scroll as a document.
85
+ *
86
+ * @default true
87
+ */
88
+ fill?: boolean;
43
89
  className?: string;
44
90
  }
45
- export declare function KanbanBoard<T>({ label, lanes, getItemId, itemLabel, renderItem, renderDragging, onMove, onRefuse, emptyLane, className, ...callbacks }: KanbanBoardProps<T>): import("react").JSX.Element;
91
+ export declare function KanbanBoard<T>({ label, lanes, getItemId, itemLabel, renderItem, renderDragging, onMove, onRefuse, emptyLane, strings, fill, className, ...callbacks }: KanbanBoardProps<T>): import("react").JSX.Element;
@@ -5,6 +5,14 @@ export declare const kanban: import("tailwind-variants").TVReturnType<{
5
5
  };
6
6
  false: {};
7
7
  };
8
+ fill: {
9
+ true: {
10
+ root: string;
11
+ };
12
+ false: {
13
+ root: string;
14
+ };
15
+ };
8
16
  }, {
9
17
  root: string;
10
18
  lane: string;
@@ -22,6 +30,14 @@ export declare const kanban: import("tailwind-variants").TVReturnType<{
22
30
  };
23
31
  false: {};
24
32
  };
33
+ fill: {
34
+ true: {
35
+ root: string;
36
+ };
37
+ false: {
38
+ root: string;
39
+ };
40
+ };
25
41
  }, {
26
42
  root: string;
27
43
  lane: string;
@@ -39,6 +55,14 @@ export declare const kanban: import("tailwind-variants").TVReturnType<{
39
55
  };
40
56
  false: {};
41
57
  };
58
+ fill: {
59
+ true: {
60
+ root: string;
61
+ };
62
+ false: {
63
+ root: string;
64
+ };
65
+ };
42
66
  }, {
43
67
  root: string;
44
68
  lane: string;
@@ -28,7 +28,7 @@ export { Checkbox, Switch, Radio, RadioGroup, CheckboxField, SwitchField, RadioF
28
28
  export { Dialog, DialogClose, AlertDialog, type DialogProps, type AlertDialogProps, } from "./components/Dialog";
29
29
  export { Popover, PopoverClose, Tooltip, TooltipProvider, type PopoverProps, type TooltipProps, } from "./components/Popover";
30
30
  export { Toaster, ToastProvider, useToast, type ToasterProps, type ToastProviderProps, type ToastOptions, type ToastTone, } from "./components/Toast";
31
- export { Menu, MenuItem, MenuGroup, MenuRadioGroup, MenuRadioItem, MenuSeparator, type MenuProps, type MenuItemProps, type MenuGroupProps, type MenuRadioGroupProps, type MenuRadioItemProps, } from "./components/Menu";
31
+ export { Menu, MenuItem, MenuGroup, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSub, type MenuProps, type MenuItemProps, type MenuGroupProps, type MenuRadioGroupProps, type MenuRadioItemProps, type MenuSubProps, } from "./components/Menu";
32
32
  export { ThemeMenu, type ThemeMenuProps } from "./components/ThemeMenu";
33
33
  export { AppShell, type AppShellProps, type NavItem, type NavSection, } from "./components/AppShell";
34
34
  export { FileUpload, type FileUploadProps, type UploadFile } from "./components/FileUpload";
@@ -90,6 +90,7 @@ export { ModelSelector, type ModelSelectorProps, type ChatModel, } from "./compo
90
90
  export { EffortControl, DEFAULT_EFFORT_LEVELS, type EffortControlProps, type EffortLevel, } from "./components/chat/EffortControl";
91
91
  export { ContextMeter, ChatStatus, formatTokens, type ContextMeterProps, type ContextSegment, type UsageLimit, type ChatStatusProps, } from "./components/chat/ContextMeter";
92
92
  export { cn } from "./lib/cn";
93
+ export { useFillHeight } from "./lib/use-fill-height";
93
94
  export { useTheme, useUiScale, useContrast, useMotionPreference, type Theme, type UiScale, type ContrastPreference, type MotionPreference, } from "./lib/preferences";
94
95
  export { usePersistedState, readStored, writeStored, clearStored, storageKey, type PersistedStateOptions, } from "./lib/persist";
95
96
  export { CalendarMonth, type CalendarMonthProps } from "./components/calendar/CalendarMonth";
@@ -109,3 +110,6 @@ export { UseCaseCard, RoadmapItemCard, RoadmapSwimlaneBoard, UseCaseLedgerPage,
109
110
  export { ProvenanceDisclosure, ProvenanceBadge, ConfidenceMeter, provenanceStateLabel, confidenceBand, type ProvenanceRecord, type ProvenanceState, type ProvenanceSource, type ProvenanceCheck, type ProvenanceCall, type ProvenanceSignoff, type ConfidenceBand, type ProvenanceDisclosureProps, type ProvenanceTriggerBadge, type ProvenanceBadgeProps, type ProvenanceBadgeAppearance, type ConfidenceMeterProps, } from "./components/provenance";
110
111
  export { createSimStore, resetSimStores, useSimSettings, setSimSettings, getNetworkSpeed, getDataSource, simulateLatency, SimNetworkError, type SimStore, type SimStoreOptions, type SimSettings, type NetworkSpeed, type DataSource, type SimNamespace, } from "./lib/sim";
111
112
  export { NodeCanvas, LAYOUT_DEFAULTS, layoutTree, pathToNode, type NodeCanvasProps, type CanvasEdge, type CanvasLayout, type CanvasNode, type CanvasOrientation, type LayoutOptions, type NodeRenderContext, type NodeRenderer, type PlacedNode, } from "./components/node-canvas";
113
+ export { defineRecordType, resetRecordTypes, useRecord, useRecords, useStaleness, write, writeAll, peekRecord, readState, readStaleness, subscribeToKey, subscribeToStaleness, retainKeys, sweepRecords, setRecordStoreTenant, getRecordStoreTenant, flushRecordNotifications, resetRecordStore, recordStoreStats, UNKNOWN_STATE, normalizeTypeName, normalizeTimestamp, recordKey, isRecordRef, type RecordRef, type ReadOptions, type RecordPayload, type RecordState, type RecordTypeOptions, type RecordTypeToken, type StalenessEntry, type WriteAllResult, type WriteOptions, type WriteResult, type WriteSource, type WriteVerdict, } from "./lib/records";
114
+ export { RealtimeProvider, useRealtime, useRealtimeEpoch, useRealtimeHealth, useRealtimeRegistry, SubscriptionRegistry, normalizeScope, scopeKey, readRealtimeEpoch, subscribeToRealtimeEpoch, subscribeToRealtimeEvents, resetRealtimeSignals, type RealtimeChannel, type RealtimeConnectionState, type RealtimeDiagnostic, type RealtimeEvent, type RealtimeHealth, type RealtimeProviderProps, type RealtimeScope, type RealtimeScopeInput, type RealtimeStatus, type RealtimeTransport, type RegistryOptions, type SubscriptionHealth, type SubscriptionLease, type SubscriptionRequest, } from "./lib/realtime";
115
+ export { useRecordRead, useRecordListRead, useRecordStoreSweep, applyMutationResult, acquireSlot, appendCreatedKey, emptySlotSnapshot, readSlot, resetSlots, slotKeys, subscribeToSlot, writeSlot, RefetchRunner, registerActiveRead, resetActiveReads, type ActiveRead, type ListFetch, type ListFetchResult, type ListReadOptions, type ListReadResult, type ReadError, type RecordFetch, type RecordReadOptions, type RecordReadResult, type RunnerOptions, type SlotSnapshot, } from "./lib/reads";
@@ -21,6 +21,37 @@ export declare function measure(items: Iterable<ItemRegistration>, containers: I
21
21
  * the innermost one.
22
22
  */
23
23
  export declare function containerAt(snapshot: LayoutSnapshot, point: Point): ContainerRect | null;
24
+ /**
25
+ * The container CLOSEST to the pointer, for a pointer that is inside none of
26
+ * them.
27
+ *
28
+ * THE DEAD ZONE IS THE BUG THIS EXISTS FOR, AND IT IS MOST OF A BOARD. The
29
+ * registered container is the SCROLLER — it has to be, so auto-scroll can find
30
+ * its edges — and on a kanban lane the scroller is only the card list. The lane
31
+ * header above it, the lane's own border, the gutter between two lanes and the
32
+ * board's bottom padding are all outside every registered rect, so a pointer
33
+ * there is "over no container" while being unmistakably over a lane to the
34
+ * person holding the card.
35
+ *
36
+ * What used to happen there was `resolveTarget` returning null and the session
37
+ * keeping the PREVIOUS target — so a card dragged from lane 1 and released on
38
+ * lane 3's header landed in lane 2, the lane the pointer had last crossed on
39
+ * its way. Measured in the demo before this change: released at (1022, 419),
40
+ * visually the middle of "Review" whose lane box is 878–1166 × 407–939, the
41
+ * card landed in "Building". Not an edge case — the header is 60px of every
42
+ * lane's top, and a person aiming at an empty lane aims at its label.
43
+ *
44
+ * SCOPED, so "nearest" cannot mean "somewhere else entirely": the snapshot
45
+ * holds one `DragDrop` root's own containers and nothing else — every root
46
+ * builds its own `DragSession` (`use-drag.ts`) with its own registry. The
47
+ * nearest container to a point over the sidebar is still a lane of the board
48
+ * being dragged, which is the only answer a release can sensibly have.
49
+ *
50
+ * TIES GO TO THE SMALLER BOX, matching `containerAt` — a point in the gutter
51
+ * between two lanes is equidistant from both, and a nested container is the
52
+ * more specific answer for the same reason it is there.
53
+ */
54
+ export declare function nearestContainer(snapshot: LayoutSnapshot, point: Point): ContainerRect | null;
24
55
  /** A container's items in reading order, excluding the one being carried. */
25
56
  export declare function remainingItems(snapshot: LayoutSnapshot, container: string, dragged: DragId | null): ItemRect[];
26
57
  /**
@@ -41,12 +72,29 @@ export declare function indexAt(snapshot: LayoutSnapshot, container: ContainerRe
41
72
  /**
42
73
  * Where the dragged item would land if released at `point`.
43
74
  *
44
- * Returns null when the pointer is over no container at all. The session keeps
45
- * the PREVIOUS target in that case rather than clearing it — dragging a card
46
- * briefly outside the board should not make the insertion marker vanish, and a
47
- * release outside every container should still land where the marker last
48
- * showed. That is the behaviour every board people already use has, and the
49
- * alternative reads as the drag having silently failed.
75
+ * THE POINTER IS OVER THE CONTAINER IT IS NEAREST TO, not only the one whose
76
+ * rectangle happens to hold it. Both halves of that matter and the second one
77
+ * is the fix:
78
+ *
79
+ * The goal has always been that dragging a card briefly outside the board must
80
+ * not make the insertion marker vanish, and that a release outside every
81
+ * container still lands somewhere rather than reading as a silently failed
82
+ * drag. That was served by returning null here and having the session keep the
83
+ * PREVIOUS target — which achieves "the marker does not blink out" and gets
84
+ * "lands where you are pointing" exactly backwards. The gap between two lanes,
85
+ * a lane's header, a lane's border and the board's own bottom padding are all
86
+ * outside every registered rect; a release in any of them landed the card in
87
+ * the last lane the pointer had crossed, which for a drag from lane 1 to lane 3
88
+ * is lane 2. See `nearestContainer` for the measurement.
89
+ *
90
+ * Nearest keeps the marker alive for the same reason last-target did, and puts
91
+ * it where the person is aiming rather than where they have been.
92
+ *
93
+ * STILL NULL FOR A BOARD WITH NO CONTAINERS AT ALL — there is no nearest one,
94
+ * and the session's `?? s.over` remains the guard for that.
95
+ *
96
+ * The index then falls out of `indexAt` without a special case: a point above a
97
+ * lane is before every card in it, a point below is after all of them.
50
98
  */
51
99
  export declare function resolveTarget(snapshot: LayoutSnapshot, point: Point, dragged: DragId | null): DropTarget | null;
52
100
  /** Value equality — targets are recreated on every move, so `===` is useless. */
@@ -7,7 +7,7 @@
7
7
  * empty icon registry (CAV-11).
8
8
  */
9
9
  export type { ContainerId, ContainerRect, ContainerRegistration, DragAxis, DragCancelEvent, DragEndEvent, DragId, DragMode, DragPhase, DragSessionCallbacks, DragStartEvent, DragState, DropTarget, ItemRect, ItemRegistration, LayoutSnapshot, Point, } from "./types";
10
- export { applyMove, containerAt, indexAt, isNoOp, measure, remainingItems, resolveTarget, sameTarget, } from "./geometry";
10
+ export { applyMove, containerAt, indexAt, isNoOp, measure, nearestContainer, remainingItems, resolveTarget, sameTarget, } from "./geometry";
11
11
  export { DragSession, type StartArgs } from "./session";
12
12
  export { attachPointerSensor, attachSessionWindowListeners, type PointerActivation, type PointerSensorOptions, } from "./sensors/pointer";
13
13
  export { attachKeyboardSensor, type KeyboardSensorOptions, } from "./sensors/keyboard";
@@ -0,0 +1,49 @@
1
+ import { type RecordPayload, type RecordTypeToken, type WriteResult } from "../records";
2
+ import type { ListReadOptions, ListReadResult, RecordReadOptions, RecordReadResult } from "./types";
3
+ /**
4
+ * Read one record: fetch it, write it into the store tagged with the field set
5
+ * it asked for, and return the store's answer.
6
+ *
7
+ * `id` may be null for a conditional read — the hook then fetches nothing and
8
+ * reports `unknown`, rather than forcing a caller around a rule of hooks.
9
+ */
10
+ export declare function useRecordRead<T>(token: RecordTypeToken<T>, id: string | number | null | undefined, options: RecordReadOptions): RecordReadResult<T>;
11
+ /**
12
+ * Read a page: fetch it, write every row into the store, and record which keys
13
+ * answered the query in a LIST SLOT.
14
+ *
15
+ * The slot is what makes a `created` broadcast visible and a tombstone
16
+ * removable without a fetch — and it is where `meta` lives, because pagination
17
+ * has no identity and can never be a record.
18
+ */
19
+ export declare function useRecordListRead<T, TMeta = unknown>(token: RecordTypeToken<T>, options: ListReadOptions<TMeta>): ListReadResult<T, TMeta>;
20
+ /**
21
+ * Write a mutation's returned item into the store.
22
+ *
23
+ * A MUTATION RESPONSE IS A FETCH, and this exists because that is a question an
24
+ * implementer will otherwise answer by coin flip. It is a full server answer to
25
+ * a request this client made: it may satisfy a field-set tag, it clears
26
+ * staleness on every field it carries, and it must NEVER be dropped by the
27
+ * self-echo filter — that filter applies to broadcasts, and dropping a
28
+ * mutation's own response would discard the server-computed fields the caller
29
+ * asked for. Structurally it cannot be dropped, because the filter lives on the
30
+ * broadcast path and this does not go through it.
31
+ */
32
+ export declare function applyMutationResult(payload: RecordPayload, options: {
33
+ tag: string;
34
+ tenant?: string | number;
35
+ }): WriteResult;
36
+ /**
37
+ * Sweep unreachable records on a bounded schedule, for as long as this is
38
+ * mounted.
39
+ *
40
+ * IT IS A HOOK, NOT AN INTERVAL STARTED ON IMPORT. A design-system module that
41
+ * schedules work because somebody imported it is a worse defect than the leak
42
+ * it prevents — it runs in every consumer, including ones with no store at all.
43
+ * So the library exports the sweep and the CONSUMER decides when: this hook at
44
+ * an app root, a call at a navigation boundary, or both. A navigation boundary
45
+ * alone is not enough, because a broadcast can create a key no root reaches and
46
+ * a screen nobody navigates away from accumulates those for as long as the tab
47
+ * is open.
48
+ */
49
+ export declare function useRecordStoreSweep(intervalMs?: number): void;
@@ -0,0 +1,4 @@
1
+ export { useRecordRead, useRecordListRead, applyMutationResult, useRecordStoreSweep } from "./hooks";
2
+ export { acquireSlot, appendCreatedKey, emptySlotSnapshot, readSlot, resetSlots, slotKeys, subscribeToSlot, writeSlot, type SlotSnapshot, } from "./slots";
3
+ export { RefetchRunner, registerActiveRead, resetActiveReads, type ActiveRead, type RunnerOptions } from "./refetch";
4
+ export type { ListFetch, ListFetchResult, ListReadOptions, ListReadResult, ReadError, RecordFetch, RecordReadOptions, RecordReadResult, } from "./types";
@@ -0,0 +1,52 @@
1
+ export interface RunnerOptions {
2
+ readonly minIntervalMs?: number;
3
+ /** Test seam. Defaults to `Date.now`. */
4
+ readonly now?: () => number;
5
+ }
6
+ /**
7
+ * One read's refetch discipline: a floor between runs, and one request in
8
+ * flight at a time.
9
+ *
10
+ * A SECOND SIGNAL ARRIVING MID-FLIGHT SETS A RE-RUN FLAG rather than issuing a
11
+ * second request. Two overlapping refetches of the same target are not twice as
12
+ * fresh; they are two answers racing to write, and the loser can be the newer
13
+ * one.
14
+ */
15
+ export declare class RefetchRunner {
16
+ private readonly run;
17
+ private inFlight;
18
+ private rerun;
19
+ private lastFinishedAt;
20
+ private timer;
21
+ private disposed;
22
+ private readonly minIntervalMs;
23
+ private readonly now;
24
+ constructor(run: () => Promise<void>, options?: RunnerOptions);
25
+ /** Ask for a refetch. Respects the floor and the in-flight request. */
26
+ request(): void;
27
+ /**
28
+ * Run now, floor and all.
29
+ *
30
+ * For the FIRST fetch and for a key change — neither is a refetch, and making
31
+ * a fresh screen wait out an interval that exists to damp a broadcast stream
32
+ * would be the floor working against its own purpose.
33
+ */
34
+ runNow(): void;
35
+ private execute;
36
+ dispose(): void;
37
+ }
38
+ /** One mounted read, as the driver sees it. */
39
+ export interface ActiveRead {
40
+ /** The keys this read is responsible for. Re-read per signal: a list's move. */
41
+ roots(): readonly string[];
42
+ /** Ask this read to re-issue the request it already made. */
43
+ request(): void;
44
+ }
45
+ /**
46
+ * Register a read so staleness and liveness recovery can reach it.
47
+ *
48
+ * @returns unregister
49
+ */
50
+ export declare function registerActiveRead(read: ActiveRead): () => void;
51
+ /** Test seam. Nothing in the library calls this. */
52
+ export declare function resetActiveReads(): void;
@@ -0,0 +1,52 @@
1
+ export interface SlotSnapshot<T = unknown, TMeta = unknown> {
2
+ readonly items: readonly T[];
3
+ readonly pending: readonly T[];
4
+ readonly pendingCount: number;
5
+ readonly meta: TMeta | undefined;
6
+ /** Every key the slot holds, fetched and pending. The refetch roots. */
7
+ readonly keys: readonly string[];
8
+ }
9
+ /** `getServerSnapshot`, and the answer for a slot nothing has written yet. */
10
+ export declare function emptySlotSnapshot(): SlotSnapshot;
11
+ /**
12
+ * Claim a slot for the duration of the returned release.
13
+ *
14
+ * REFCOUNTED AND IDEMPOTENT, which is what makes StrictMode's mount / unmount /
15
+ * mount leave the slot — and its retained keys — exactly as it found them.
16
+ */
17
+ export declare function acquireSlot(id: string, options: {
18
+ type: string;
19
+ tag: string;
20
+ acceptsCreated: boolean;
21
+ }): () => void;
22
+ /**
23
+ * Replace a slot's membership with a fetch's answer.
24
+ *
25
+ * A key that arrived through a `created` event and is now in the fetched page
26
+ * stops being pending — the fetch has placed it, which is the only thing that
27
+ * ever could.
28
+ */
29
+ export declare function writeSlot(id: string, keys: readonly string[], meta: unknown): void;
30
+ /**
31
+ * Append a record that entered the set elsewhere.
32
+ *
33
+ * WITH NO POSITION. The server owns sort order and page boundary; a client that
34
+ * inserts at a guessed index renders a list that is subtly, confidently wrong.
35
+ */
36
+ export declare function appendCreatedKey(type: string, key: string): void;
37
+ export declare function subscribeToSlot(id: string, listener: () => void): () => void;
38
+ /** The keys a slot holds right now — the roots a targeted refetch matches on. */
39
+ export declare function slotKeys(id: string): readonly string[];
40
+ /**
41
+ * The slot's projection, memoized to a stable reference.
42
+ *
43
+ * IT RECOMPUTES AND THEN COMPARES rather than trusting its dirty flag, because
44
+ * a member notification does not always change what this slot renders — a
45
+ * record whose field-set tag is still unsatisfied moves nothing here — and
46
+ * handing back a fresh array for an unchanged list re-renders every consumer
47
+ * for nothing. The elements themselves are the store's own reference-stable
48
+ * snapshots, so comparing them is one identity check each.
49
+ */
50
+ export declare function readSlot(id: string): SlotSnapshot;
51
+ /** Test seam. Nothing in the library calls this. */
52
+ export declare function resetSlots(): void;
@@ -0,0 +1,139 @@
1
+ import type { RecordPayload, RecordState } from "../records";
2
+ /**
3
+ * A refusal.
4
+ *
5
+ * IT LIVES HERE AND NOT IN THE STORE, deliberately. A 404 is not one of the
6
+ * store's record states, because a refusal carries a message and a remedy the
7
+ * store has no way to build — so a `404` member there would be permanently
8
+ * unreachable. It belongs to the layer that performed the request, which is
9
+ * this one.
10
+ */
11
+ export interface ReadError {
12
+ readonly message: string;
13
+ /** Whatever was thrown. The app's own error wrapper reads it. */
14
+ readonly cause: unknown;
15
+ }
16
+ /** What a fetch of one record must resolve to: the payload itself. */
17
+ export type RecordFetch = (signal?: AbortSignal) => Promise<RecordPayload>;
18
+ /** What a fetch of a page must resolve to. */
19
+ export interface ListFetchResult<TMeta = unknown> {
20
+ readonly items: readonly RecordPayload[];
21
+ /**
22
+ * Pagination, totals, anything the page carries that is NOT a record.
23
+ *
24
+ * IT LIVES IN THE SLOT BECAUSE IT CANNOT LIVE ANYWHERE ELSE. `meta.total` has
25
+ * no `__type` and no `id`, so it can never be a record — and a normalized
26
+ * store without a place for it is the half of this design that gets omitted
27
+ * and then discovered from a screen that cannot render its own page count.
28
+ */
29
+ readonly meta?: TMeta;
30
+ }
31
+ export type ListFetch<TMeta = unknown> = (signal?: AbortSignal) => Promise<ListFetchResult<TMeta>>;
32
+ /** Options common to both reads. */
33
+ interface CommonReadOptions {
34
+ /**
35
+ * The opaque field-set name this request asks for — `"card"`, `"detail"`,
36
+ * whatever the app's own api layer already calls its selector.
37
+ *
38
+ * The library never interprets it. It is compared for equality and nothing
39
+ * else, which is what lets `needs` gate a read on a completion the library
40
+ * cannot compute.
41
+ */
42
+ readonly tag: string;
43
+ /**
44
+ * The tenant to stamp writes with. Defaults to the store's current tenant,
45
+ * READ AT REQUEST TIME.
46
+ *
47
+ * Request time is the whole point: stamping on ARRIVAL reads the tenant as it
48
+ * is now, which defeats the check entirely — a team-A response that resolves
49
+ * after a switch to team B would be stamped B and merged into B's store.
50
+ */
51
+ readonly tenant?: string | number;
52
+ /** Skip the fetch entirely. The read reports `loading: false`. */
53
+ readonly enabled?: boolean;
54
+ /**
55
+ * The floor between two refetches of THIS read. Default 1000ms.
56
+ *
57
+ * A TIME FLOOR IS NOT OPTIONAL, and breadth-coalescing alone is not enough:
58
+ * at least one real broadcast source has no change guard at all and fires on
59
+ * every related save, so during a long job it is a continuous STREAM rather
60
+ * than a burst. Without a floor that is one refetch per frame, of whatever
61
+ * the heaviest read on the screen happens to be.
62
+ */
63
+ readonly minRefetchIntervalMs?: number;
64
+ }
65
+ export interface RecordReadOptions extends CommonReadOptions {
66
+ readonly fetch: RecordFetch;
67
+ }
68
+ export interface ListReadOptions<TMeta = unknown> extends CommonReadOptions {
69
+ /**
70
+ * The serialized query — the slot's identity.
71
+ *
72
+ * Two components asking the same question share one slot; two different
73
+ * questions never collide. It is the caller's serialization because only the
74
+ * caller knows which of its parameters are part of the question.
75
+ */
76
+ readonly key: string;
77
+ readonly fetch: ListFetch<TMeta>;
78
+ /**
79
+ * Whether a `created` broadcast for this type may append to this slot.
80
+ * Default `true`.
81
+ *
82
+ * SET IT FALSE FOR A SLOT WHOSE MEMBERSHIP THIS CLIENT CANNOT VERIFY. The
83
+ * backend evaluates a filter subscription server-side per save, so a
84
+ * `created` event arriving on such a subscription IS authoritative that the
85
+ * record matches — but the library cannot tell WHICH of several slots of the
86
+ * same type it matches when a screen holds more than one differently-filtered
87
+ * list. Appending to all of them would be a guess; this flag is where the app
88
+ * says which one owns the event.
89
+ */
90
+ readonly acceptsCreated?: boolean;
91
+ }
92
+ /** The loading / refreshing / error triple, shared by both reads. */
93
+ interface ReadStatus {
94
+ /** No answer yet AND a fetch is in flight. This is the skeleton. */
95
+ readonly loading: boolean;
96
+ /**
97
+ * An answer is on screen AND a fetch is in flight.
98
+ *
99
+ * SEPARATE FROM `loading` ON PURPOSE. Collapsing the two blanks a populated
100
+ * screen every time anything invalidates — which is exactly what recovering
101
+ * from staleness by blanking the screen would do, and it is a worse
102
+ * experience than the staleness.
103
+ */
104
+ readonly refreshing: boolean;
105
+ readonly error: ReadError | undefined;
106
+ /** Re-issue the request now, subject to the refetch floor. */
107
+ readonly refetch: () => void;
108
+ }
109
+ export interface RecordReadResult<T> extends ReadStatus {
110
+ /** The store's answer: `unknown`, `present` or `deleted`. Never a maybe. */
111
+ readonly state: RecordState<T>;
112
+ /** `state.record` when present, for the common case. */
113
+ readonly data: T | undefined;
114
+ }
115
+ export interface ListReadResult<T, TMeta = unknown> extends ReadStatus {
116
+ /** The fetched page, in the server's order, tombstoned rows removed. */
117
+ readonly items: readonly T[];
118
+ /**
119
+ * Rows that entered the set through a `created` broadcast, in arrival order.
120
+ *
121
+ * THEY ARE NOT SPLICED INTO `items`, and that is the honest form of the rule
122
+ * rather than a limitation: sort position and page boundary are decided by
123
+ * the server, so guessing where a new row goes is precisely the lie the rule
124
+ * exists to prevent. A screen renders "N new" and the next fetch places them.
125
+ */
126
+ readonly pending: readonly T[];
127
+ /**
128
+ * How many rows are waiting, INCLUDING ones not yet renderable.
129
+ *
130
+ * THIS IS NOT `pending.length`, and the difference is the whole reason it
131
+ * exists. A `created` broadcast is partial by whitelist, so it can never
132
+ * satisfy the field-set tag — meaning the appended record is countable but
133
+ * not renderable until a fetch fills it in. Reading "N new" off `pending`
134
+ * would print zero for exactly the case the marker was added to cover.
135
+ */
136
+ readonly pendingCount: number;
137
+ readonly meta: TMeta | undefined;
138
+ }
139
+ export {};
@@ -0,0 +1,38 @@
1
+ import type { RecordTypeToken } from "../records";
2
+ import type { RealtimeHealth, RealtimeScopeInput } from "./types";
3
+ /**
4
+ * Declare that this component is looking at these records and wants these
5
+ * events.
6
+ *
7
+ * COALESCED. Twenty-five components asking for the same scope produce ONE
8
+ * server-side registration, and the union of everyone's event names is bound —
9
+ * never the first caller's only, which is the live defect this module was
10
+ * written against.
11
+ *
12
+ * NO-OP WITHOUT A PROVIDER. An application with realtime switched off keeps
13
+ * fetching and keeps rendering; nothing here throws.
14
+ *
15
+ * @param token the record type, from `defineRecordType`
16
+ * @param scope `"all"`, an id list, or `{filter}` the server evaluates
17
+ * @param events event names to bind, e.g. `["created", "updated", "deleted"]`
18
+ */
19
+ export declare function useRealtime<T>(token: RecordTypeToken<T>, scope: RealtimeScopeInput, events: readonly string[]): void;
20
+ /**
21
+ * The renderable liveness state.
22
+ *
23
+ * IT IS RENDERABLE ON PURPOSE. A dead subscription is indistinguishable from a
24
+ * quiet one, and the prior art's answer — a console panel behind a keyboard
25
+ * shortcut — is why two real bugs in it went unnoticed for years. This is the
26
+ * equivalent of `refreshing`: a word the reader can see, not a log line.
27
+ */
28
+ export declare function useRealtimeHealth(): RealtimeHealth;
29
+ /**
30
+ * The "you may have missed events" counter, for a consumer that wants to fold
31
+ * it into its own invalidation rather than use the read binding.
32
+ *
33
+ * IT IS A REVISION, NEVER A READ KEY. A key change blanks the screen to a
34
+ * skeleton; a revision keeps the answer on screen and marks it refreshing. This
35
+ * signal exists because a subscription went dead and came back — recovering
36
+ * from that by blanking the screen is a worse experience than the staleness.
37
+ */
38
+ export declare function useRealtimeEpoch(): number;
@@ -0,0 +1,4 @@
1
+ export { RealtimeProvider, useRealtimeRegistry, type RealtimeProviderProps } from "./provider";
2
+ export { useRealtime, useRealtimeEpoch, useRealtimeHealth } from "./hooks";
3
+ export { SubscriptionRegistry, normalizeScope, scopeKey, readRealtimeEpoch, subscribeToRealtimeEpoch, subscribeToRealtimeEvents, resetRealtimeSignals, type RegistryOptions, } from "./registry";
4
+ export type { RealtimeChannel, RealtimeConnectionState, RealtimeDiagnostic, RealtimeEvent, RealtimeHealth, RealtimeScope, RealtimeScopeInput, RealtimeStatus, RealtimeTransport, SubscriptionHealth, SubscriptionLease, SubscriptionRequest, } from "./types";
@@ -0,0 +1,38 @@
1
+ import { type ReactNode } from "react";
2
+ import { SubscriptionRegistry, type RegistryOptions } from "./registry";
3
+ export interface RealtimeProviderProps extends Omit<RegistryOptions, "transport" | "tenant" | "now"> {
4
+ /**
5
+ * Everything environment-specific in one object: socket, endpoints,
6
+ * connection observer. INJECTED, never imported — see `types.ts` for why, and
7
+ * for the one constraint on its implementation that a type cannot express.
8
+ */
9
+ readonly transport: RegistryOptions["transport"];
10
+ /**
11
+ * The team, org or account these records belong to.
12
+ *
13
+ * CHANGING IT IS A REAL EVENT, not a re-render: every server-side
14
+ * registration is dropped, every channel is left, the record store is
15
+ * cleared, and everything still mounted re-subscribes under the new tenant.
16
+ */
17
+ readonly tenant?: string | number;
18
+ /**
19
+ * Whether this provider owns the record store's tenant. Default `true`.
20
+ *
21
+ * Set it false only when something else already calls
22
+ * `setRecordStoreTenant` — two writers of one value is how they disagree.
23
+ */
24
+ readonly ownsStoreTenant?: boolean;
25
+ readonly children: ReactNode;
26
+ }
27
+ export declare function RealtimeProvider({ transport, tenant, ownsStoreTenant, children, ...options }: RealtimeProviderProps): import("react").JSX.Element;
28
+ /**
29
+ * The registry, or `null` when no provider is mounted.
30
+ *
31
+ * THIS PACKAGE'S USUAL CONVENTION IS INVERTED HERE, DELIBERATELY. Every other
32
+ * provider in this library throws when its hook is used outside it, and that is
33
+ * right for a capability a component cannot work without. Realtime is not one:
34
+ * an application with no transport configured must degrade to plain fetching,
35
+ * never crash. A design system that hard-fails because an OPTIONAL capability
36
+ * is absent is not offering an optional capability.
37
+ */
38
+ export declare function useRealtimeRegistry(): SubscriptionRegistry | null;