@nivaro/react 0.3.41 → 0.3.43

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.
Files changed (97) hide show
  1. package/dist/components/LayoutForm.js +100 -98
  2. package/dist/full.css +1 -1
  3. package/dist/hooks/useSessionRecorder.js +1 -1
  4. package/dist/index.d.ts +733 -15
  5. package/dist/index.js +820 -775
  6. package/dist/shared/components/ActivityEntry.js +154 -0
  7. package/dist/shared/components/AudienceConfirmPanel.js +98 -0
  8. package/dist/shared/components/BackgroundRunOffer.js +43 -31
  9. package/dist/shared/components/BroadcastView.js +225 -204
  10. package/dist/shared/components/CollectionBrowserView.js +3924 -3584
  11. package/dist/shared/components/ConfigHistory.js +153 -0
  12. package/dist/shared/components/ConfigReadBack.js +65 -0
  13. package/dist/shared/components/DataTable.js +379 -346
  14. package/dist/shared/components/ForceReloadBanner.js +47 -40
  15. package/dist/shared/components/IdleReloadWatcher.js +69 -0
  16. package/dist/shared/components/ItemEditForm.js +4041 -3853
  17. package/dist/shared/components/MyWorkView.js +278 -213
  18. package/dist/shared/components/NotificationBell.js +130 -121
  19. package/dist/shared/components/ProfileView.js +776 -1003
  20. package/dist/shared/components/RecentRecordsRail.js +34 -16
  21. package/dist/shared/components/RecordReadView.js +500 -477
  22. package/dist/shared/components/Skeletons.js +199 -0
  23. package/dist/shared/components/SlowNotice.js +63 -0
  24. package/dist/shared/components/Term.js +23 -0
  25. package/dist/shared/components/WidgetSlot.js +350 -353
  26. package/dist/shared/components/access/PermissionSummary.js +106 -0
  27. package/dist/shared/components/bulk/BulkActionButtons.js +76 -73
  28. package/dist/shared/components/chat/ChatPanel.js +902 -886
  29. package/dist/shared/components/chat/MemberPaging.js +70 -0
  30. package/dist/shared/components/chat/RoomParts.js +147 -117
  31. package/dist/shared/components/chat/ScreenClip.js +73 -69
  32. package/dist/shared/components/chat/chat-core.js +449 -396
  33. package/dist/shared/components/chat/huddle/huddle-store.js +48 -43
  34. package/dist/shared/components/help-videos/api.js +2 -5
  35. package/dist/shared/components/help-videos/viewer/HelpVideoMatchChip.js +79 -0
  36. package/dist/shared/components/help-videos/viewer/matchChip.js +50 -0
  37. package/dist/shared/components/imports/CollectionImportPanel.js +139 -133
  38. package/dist/shared/components/imports/RunDetailSheet.js +222 -227
  39. package/dist/shared/components/integrations/console/ErrorExplanation.js +121 -0
  40. package/dist/shared/components/integrations/console/PartnerDetail.js +175 -174
  41. package/dist/shared/components/integrations/console/SubmissionRow.js +36 -34
  42. package/dist/shared/components/integrations/console/drill/SubmissionDrill.js +50 -48
  43. package/dist/shared/components/item-edit/FieldComments.js +212 -0
  44. package/dist/shared/components/item-edit/FieldRow.js +340 -336
  45. package/dist/shared/components/item-edit/RecordHeaderActions.js +23 -22
  46. package/dist/shared/components/item-edit/RelationCombobox.js +231 -222
  47. package/dist/shared/components/item-edit/RuleHiddenNotes.js +55 -0
  48. package/dist/shared/components/item-edit/field-comments-context.js +6 -0
  49. package/dist/shared/components/item-edit/field-sources.js +33 -0
  50. package/dist/shared/components/item-edit/rule-hidden.js +30 -0
  51. package/dist/shared/components/layout-editor/HistoryPanel.js +113 -96
  52. package/dist/shared/components/layout-editor/editors/cascade-filters.js +170 -145
  53. package/dist/shared/components/layout-editor/inspector/Breadcrumb.js +22 -18
  54. package/dist/shared/components/layout-editor/inspector/GroupInspector.js +177 -159
  55. package/dist/shared/components/layout-editor/inspector/PickerTab.js +35 -34
  56. package/dist/shared/components/layout-editor/inspector/RulesTab.js +124 -111
  57. package/dist/shared/components/layout-editor/inspector/kit.js +292 -277
  58. package/dist/shared/components/layout-editor/parity-keys.json.js +1 -1
  59. package/dist/shared/components/list/CopyListLinkButton.js +41 -0
  60. package/dist/shared/components/list/InlineEdit.js +363 -0
  61. package/dist/shared/components/notifications/NotificationCenterView.js +213 -181
  62. package/dist/shared/components/notifications/NotificationDetailBits.js +205 -82
  63. package/dist/shared/components/notifications/NotificationRulesEditor.js +515 -0
  64. package/dist/shared/components/notifications/TestMyRules.js +137 -0
  65. package/dist/shared/components/notifications/changes-lines.js +24 -0
  66. package/dist/shared/components/notifications/why-lines.js +64 -0
  67. package/dist/shared/components/panels/CommentPanel.js +316 -281
  68. package/dist/shared/components/panels/LegalHoldBanner.js +38 -0
  69. package/dist/shared/components/panels/PipelinePanel.js +201 -196
  70. package/dist/shared/components/panels/RevisionsPanel.js +496 -479
  71. package/dist/shared/components/pipeline/PipelineEditorView.js +1587 -1560
  72. package/dist/shared/components/pipeline/PipelineStateOwners.js +116 -103
  73. package/dist/shared/components/profile/PersonDidPanel.js +188 -0
  74. package/dist/shared/components/profile/PersonProfile.js +57 -52
  75. package/dist/shared/components/queue/QueueWorklist.js +1526 -1365
  76. package/dist/shared/hooks/useSlowReason.js +39 -0
  77. package/dist/shared/lib/activity-sentence.js +670 -0
  78. package/dist/shared/lib/config-sentences.js +418 -0
  79. package/dist/shared/lib/core-glossary.js +229 -0
  80. package/dist/shared/lib/error-hints.js +159 -0
  81. package/dist/shared/lib/error-message.js +105 -0
  82. package/dist/shared/lib/error-toast.js +22 -0
  83. package/dist/shared/lib/field-comments.js +25 -0
  84. package/dist/shared/lib/filter-sentence.js +261 -0
  85. package/dist/shared/lib/glossary.js +55 -48
  86. package/dist/shared/lib/idle-reload.js +138 -0
  87. package/dist/shared/lib/layout-resolve.js +42 -29
  88. package/dist/shared/lib/list-inline-edit.js +190 -0
  89. package/dist/shared/lib/list-link.js +147 -0
  90. package/dist/shared/lib/nav-trail.js +107 -0
  91. package/dist/shared/lib/prefetch.js +259 -106
  92. package/dist/shared/lib/record-ref.js +72 -0
  93. package/dist/shared/lib/reload-holds.js +25 -0
  94. package/dist/shared/lib/safe-error.js +52 -24
  95. package/dist/shared/lib/skeleton-shapes.js +72 -0
  96. package/dist/shared/lib/slow-reason.js +63 -0
  97. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -120,6 +120,164 @@ declare interface ActiveLayoutData {
120
120
  ungrouped_sort?: number | null;
121
121
  }
122
122
 
123
+ /** Who really did it (nivaro_activity.actor, migration 516; derived for older rows). */
124
+ declare interface ActivityActor {
125
+ kind: ActivityActorKind;
126
+ /** view_as: the admin; api_key: the key id; mcp: the connection; import: the definition
127
+ * key; flow: the flow id; cron: the job id. */
128
+ id?: string | null;
129
+ name?: string | null;
130
+ /** The person the actor acted for / as (view_as: the person being viewed as). */
131
+ on_behalf_of?: string | null;
132
+ }
133
+
134
+ /**
135
+ * Activity sentences — one readable line per audit-trail entry.
136
+ *
137
+ * "Pat moved PO 4471 from Draft to Approved"
138
+ * "Sam deleted 3 invoices"
139
+ * "Rob as Pat changed Vendor on Workflow 85607"
140
+ * "Pat updated 40 workflows: Status → On Hold"
141
+ *
142
+ * Pure (no imports, no I/O): the Activity page, the activity detail page, the
143
+ * record history panels and the email digest all render through it. Labels
144
+ * come from POST /api/activity/labels (records read AS THE READER — a record
145
+ * the reader cannot see is "a record you can't see"; people by name;
146
+ * collection / field / choice labels); until they arrive the sentence falls
147
+ * back to the collection name and the stored id.
148
+ *
149
+ * TWIN FILE: packages/shared/src/lib/activity-sentence.ts and
150
+ * api/src/services/activity-sentence.ts must stay byte-identical (the API
151
+ * cannot import packages/shared; an API test compares the two files).
152
+ */
153
+ declare type ActivityActorKind = 'person' | 'view_as' | 'api_key' | 'mcp' | 'import' | 'flow' | 'cron' | 'system';
154
+
155
+ /** A linked approval decision the read API joined onto the entry. */
156
+ declare interface ActivityApproval {
157
+ decision: string;
158
+ comment?: string | null;
159
+ chain?: string | null;
160
+ step?: string | number | null;
161
+ collection?: string | null;
162
+ item?: string | null;
163
+ }
164
+
165
+ /** A field change the read API summarised from the revision delta. */
166
+ declare interface ActivityChange {
167
+ field: string;
168
+ value?: unknown;
169
+ }
170
+
171
+ export declare function ActivityEntry({ entry, labels, group, onOpen, actions, showTime, className }: ActivityEntryProps): JSX.Element;
172
+
173
+ export declare interface ActivityEntryInput {
174
+ id?: number | string | null;
175
+ action: string | null;
176
+ /** The person (nivaro_activity.user); revisions call it user_id. */
177
+ user?: string | null;
178
+ user_id?: string | null;
179
+ first_name?: string | null;
180
+ last_name?: string | null;
181
+ user_email?: string | null;
182
+ collection?: string | null;
183
+ item?: string | number | null;
184
+ comment?: string | null;
185
+ timestamp?: string | Date | null;
186
+ origin?: string | null;
187
+ chain_parent?: string | null;
188
+ auth_method?: string | null;
189
+ api_key_id?: number | string | null;
190
+ api_key_name?: string | null;
191
+ mcp_conn?: string | null;
192
+ via_app?: string | null;
193
+ via_agent?: string | null;
194
+ group_key?: string | null;
195
+ /** Stored JSON (string) or the parsed object. */
196
+ actor?: ActivityActor | string | null;
197
+ changes?: ActivityChange[] | null;
198
+ changed_fields?: string[] | null;
199
+ delta?: Record<string, unknown> | null;
200
+ /** A pipeline event folded into a record's history (services/revisions.ts). */
201
+ event?: {
202
+ kind: 'transition' | 'start';
203
+ from_label?: string | null;
204
+ to_label?: string | null;
205
+ transition_label?: string | null;
206
+ source?: string | null;
207
+ on_behalf_of?: {
208
+ id: string;
209
+ name: string | null;
210
+ } | null;
211
+ addendum?: {
212
+ id: string;
213
+ title: string | null;
214
+ } | null;
215
+ } | null;
216
+ why?: ActivityWhy | null;
217
+ }
218
+
219
+ /**
220
+ * One audit-trail entry as a sentence ("Pat moved PO 4471 from Draft to Approved"), who
221
+ * really did it (View-as, API key, assistant, import, flow, scheduled job), why (change
222
+ * reason, transition note, linked approval) and when, in the reader's time zone. A burst
223
+ * (`group` with more than one entry: one bulk action, import run, flow run, MCP request…)
224
+ * reads as one line that expands to its entries.
225
+ *
226
+ * `actions` is a slot rendered at the row's end (Undo, Open…). Labels come from
227
+ * `useActivityLabels` (POST /api/activity/labels, read as the viewer).
228
+ */
229
+ export declare interface ActivityEntryProps {
230
+ entry: ActivityEntryInput;
231
+ labels?: ActivityLabels | null;
232
+ /** The entry's whole burst, newest first (the entry itself included). */
233
+ group?: ActivityEntryInput[];
234
+ onOpen?: (entry: ActivityEntryInput) => void;
235
+ actions?: ReactNode;
236
+ /** Show the time at the row's end (default true). */
237
+ showTime?: boolean;
238
+ className?: string;
239
+ }
240
+
241
+ /** A list of entries with bursts folded (groupActivity) — each group one ActivityEntry. */
242
+ export declare function ActivityFeed({ entries, labels, onOpen, actionsFor, empty, className }: {
243
+ entries: ActivityEntryInput[];
244
+ labels?: ActivityLabels | null;
245
+ onOpen?: (entry: ActivityEntryInput) => void;
246
+ /** The `actions` slot for a group's head entry. */
247
+ actionsFor?: (group: ActivityGroup) => ReactNode;
248
+ empty?: ReactNode;
249
+ className?: string;
250
+ }): JSX.Element;
251
+
252
+ declare interface ActivityGroup<T extends ActivityEntryInput = ActivityEntryInput> {
253
+ /** Stable React key. */
254
+ key: string;
255
+ /** Newest first, as given. */
256
+ entries: T[];
257
+ /** 'group_key' = written by one bulk action / import / flow / cron / batch / MCP request;
258
+ * 'time' = same actor + collection + action within the window (rows before 516). */
259
+ by: 'group_key' | 'time' | null;
260
+ }
261
+
262
+ /** What POST /api/activity/labels answers (plus the reader's time zone, if any). */
263
+ export declare interface ActivityLabels {
264
+ /** Person id (any case) → display name. */
265
+ users?: Record<string, string>;
266
+ /** `collection:id` → label; null = the reader cannot see that record. */
267
+ records?: Record<string, string | null>;
268
+ /** Collection → names (or a bare display name). */
269
+ collections?: Record<string, CollectionNames | string>;
270
+ /** `collection.field` → field label. */
271
+ fields?: Record<string, string>;
272
+ /** `collection.field` → stored value → choice label. */
273
+ choices?: Record<string, Record<string, string>>;
274
+ /** `collection.field` → the collection a link field points at. Link values are ids, so a
275
+ * sentence names the field, never the raw value. */
276
+ links?: Record<string, string>;
277
+ /** IANA zone dates in values render in; unset = the runtime's own zone. */
278
+ timeZone?: string | null;
279
+ }
280
+
123
281
  /** A stretch of the recording where the author was typing in a text field,
124
282
  * or touched nothing at all (#1518). Never what was typed. */
125
283
  declare type ActivitySpan = {
@@ -128,6 +286,25 @@ declare type ActivitySpan = {
128
286
  end_ms: number;
129
287
  };
130
288
 
289
+ /** The "why" the read API joins onto an entry (#5): reason, note, approval. */
290
+ declare interface ActivityWhy {
291
+ /** The change reason a person gave for a create / update / delete. */
292
+ reason?: string | null;
293
+ /** What a person wrote on a transition. */
294
+ note?: string | null;
295
+ approval?: ActivityApproval | null;
296
+ }
297
+
298
+ /** The entry's "why" as short lines: change reason, transition note, approval. */
299
+ export declare function activityWhy(entry: ActivityEntryInput): {
300
+ reason: string | null;
301
+ note: string | null;
302
+ approval: string | null;
303
+ };
304
+
305
+ /** "Pat", "Rob as Pat", "API key “Partner sync”", "Claude for Pat", "Import “Vendors”". */
306
+ export declare function actorName(entry: ActivityEntryInput, labels?: ActivityLabels | null): string;
307
+
131
308
  export declare function AddendumPanel({ collection, item, addendumLayoutId, canCreate, onActiveCountChange, onApplied, onCreated, defaultExpanded }: {
132
309
  collection: string;
133
310
  item: string;
@@ -786,6 +963,9 @@ export declare interface BackgroundRequest {
786
963
  * then offers to hand the read to the server. While the server runs it, the strip shows elapsed
787
964
  * time against the usual time; when the result is ready it fills the page's own query and the
788
965
  * strip goes away. Every state is inline — no dialog.
966
+ *
967
+ * #3231 — from ~3 s it also says what the read is waiting on ("Waiting for a free slot — busy
968
+ * right now", "Running PUB health — not cached yet"), before and beside the offer.
789
969
  */
790
970
  export declare function BackgroundRunOffer({ className, ...opts }: UseBackgroundRunOptions & {
791
971
  className?: string;
@@ -1052,6 +1232,20 @@ export declare function captureMaskedScreenshot(opts?: {
1052
1232
 
1053
1233
  export declare function captureReplayClip(kind?: ReplayClipKind, timeoutMs?: number): Promise<ReplayLink | null>;
1054
1234
 
1235
+ /**
1236
+ * A card placeholder: optional title row, a KPI figure, a chart block or text lines — the shape
1237
+ * of the card that is coming.
1238
+ */
1239
+ export declare function CardSkeleton({ title, kpi, chart, lines, className, framed }: {
1240
+ title?: boolean;
1241
+ kpi?: boolean;
1242
+ chart?: boolean;
1243
+ lines?: number;
1244
+ className?: string;
1245
+ /** false = no border/background (the host already draws the card). */
1246
+ framed?: boolean;
1247
+ }): JSX.Element;
1248
+
1055
1249
  export { CascadeFilterRule }
1056
1250
 
1057
1251
  declare type CascadeRule = {
@@ -1111,6 +1305,17 @@ declare interface CellFilterLite {
1111
1305
  id_value?: number | null;
1112
1306
  }
1113
1307
 
1308
+ /**
1309
+ * Shape-matched loading placeholders (#3229) — the pure half.
1310
+ *
1311
+ * A placeholder takes the shape of what is coming: a list's real columns (a date column gets a
1312
+ * date-sized bar, a status column a pill, a number is right-aligned), a record form's real
1313
+ * sections and field widths when its layout is known before the record, a card's header and
1314
+ * lines. Widths are deterministic per (row, column) so a skeleton never flickers between renders.
1315
+ * The components live in components/Skeletons.tsx.
1316
+ */
1317
+ declare type CellShape = 'text' | 'long' | 'short' | 'number' | 'date' | 'badge' | 'avatar' | 'bool';
1318
+
1114
1319
  /** Shape of the 422 CHANGE_REASON_REQUIRED challenge from the items service. */
1115
1320
  export declare interface ChangeReasonChallenge {
1116
1321
  fields_changed: string[];
@@ -1890,6 +2095,11 @@ export declare function CollectionImportPanel({ initialJobId, onJobOpen }: {
1890
2095
  onJobOpen?: (id: string | null) => void;
1891
2096
  }): JSX.Element;
1892
2097
 
2098
+ declare interface CollectionNames {
2099
+ singular?: string | null;
2100
+ plural?: string | null;
2101
+ }
2102
+
1893
2103
  /**
1894
2104
  * Realtime sprint client contract. A HOST (admin, a portal app) owns the actual
1895
2105
  * Socket.io connection and provides this to shared components; components
@@ -2044,6 +2254,9 @@ export declare function ConformanceView({ className }: {
2044
2254
  className?: string;
2045
2255
  }): JSX.Element;
2046
2256
 
2257
+ /** Pure: may this connection prefetch? Not when it asks to save data or is 2g-slow. */
2258
+ export declare function connectionAllowsPrefetch(c: NetInfo | null | undefined): boolean;
2259
+
2047
2260
  /**
2048
2261
  * "Copy as …" (#654): the request behind a list or a record, as a curl
2049
2262
  * command, an SDK call and a GraphQL document, each ready to paste. The
@@ -2480,6 +2693,20 @@ declare interface DisplayPrefs {
2480
2693
 
2481
2694
  export declare function DisplayPrefsCard(): JSX.Element;
2482
2695
 
2696
+ /**
2697
+ * The crumbs to show: walk back from the current page, newest first, skipping
2698
+ * the current page itself and repeats (the NEWEST visit of a page wins — the
2699
+ * fewest steps back), and stop after the first "root" page (a list or hub the
2700
+ * person started from) or `max` crumbs. Oldest first.
2701
+ */
2702
+ export declare function displayTrail(chain: TrailEntry[], current: {
2703
+ idx: number;
2704
+ path: string;
2705
+ }, opts?: {
2706
+ isRoot?: (pathname: string) => boolean;
2707
+ max?: number;
2708
+ }): TrailCrumb[];
2709
+
2483
2710
  declare type DmOpener = (userId: string, displayName?: string) => void;
2484
2711
 
2485
2712
  export declare function dmPeer(room: string, self: string): string | null;
@@ -2782,6 +3009,8 @@ export declare interface ErpSubmission {
2782
3009
  * A record page hosting its OWN External requests dialog already IS that
2783
3010
  * record, so it never needs this badge and doesn't send it. */
2784
3011
  record_label?: string;
3012
+ /** #18 — a failed push in plain words + Fix it target (null when it did not fail). */
3013
+ explanation?: IntegrationErrorExplanation | null;
2785
3014
  }
2786
3015
 
2787
3016
  export declare interface ErrorReplayLink {
@@ -3248,8 +3477,14 @@ export declare const FORCE_RELOAD_EVENT = "nvr:force-refresh";
3248
3477
  * Unsaved work (lib/unsaved-work.ts) holds the reload: past the deadline the
3249
3478
  * banner says a reload is waiting and reloads the moment the edits are saved
3250
3479
  * or discarded — or when the person clicks Reload now.
3480
+ *
3481
+ * Reload old tabs when idle (#2937): the banner also mounts IdleReloadWatcher, so a tab left on an
3482
+ * older build after a release reloads by itself once idle (no countdown, no banner). Pass
3483
+ * `idleReload={false}` to leave that out.
3251
3484
  */
3252
- export declare function ForceReloadBanner(): JSX.Element | null;
3485
+ export declare function ForceReloadBanner({ idleReload }?: {
3486
+ idleReload?: boolean;
3487
+ }): JSX.Element;
3253
3488
 
3254
3489
  /**
3255
3490
  * Sticky force refresh — the client half of api/src/services/sticky-refresh.ts.
@@ -3386,6 +3621,45 @@ export declare type FormSchema = {
3386
3621
  slotAssignments: FormSlotAssignment[];
3387
3622
  };
3388
3623
 
3624
+ /**
3625
+ * A record form placeholder. With a plan (formSkeletonFromLayout) it paints the form's own tabs,
3626
+ * section titles and field grid; without one, two generic sections.
3627
+ */
3628
+ export declare function FormSkeleton({ plan, className }: {
3629
+ plan: FormSkeletonPlan | null;
3630
+ className?: string;
3631
+ }): JSX.Element;
3632
+
3633
+ declare interface FormSkeletonField {
3634
+ key: string;
3635
+ /** Grid span out of 12. */
3636
+ span: number;
3637
+ /** A grid, widget or slot block (taller than an input). */
3638
+ tall?: boolean;
3639
+ }
3640
+
3641
+ /**
3642
+ * The resolved layout (GET /collection-layouts/active) → the sections and field widths the form
3643
+ * will paint. Null when there is no layout (the caller shows a generic form placeholder).
3644
+ */
3645
+ export declare function formSkeletonFromLayout(layout: LayoutLike | null | undefined, opts?: {
3646
+ isNew?: boolean;
3647
+ maxSections?: number;
3648
+ maxFields?: number;
3649
+ }): FormSkeletonPlan | null;
3650
+
3651
+ export declare interface FormSkeletonPlan {
3652
+ /** Tab labels, when the form opens on tabs (the first is the one that shows). */
3653
+ tabs: string[];
3654
+ sections: FormSkeletonSection[];
3655
+ }
3656
+
3657
+ declare interface FormSkeletonSection {
3658
+ key: string;
3659
+ label: string | null;
3660
+ fields: FormSkeletonField[];
3661
+ }
3662
+
3389
3663
  export declare type FormSlotAssignment = {
3390
3664
  slot: string;
3391
3665
  groupKey: string | null;
@@ -3591,6 +3865,16 @@ declare interface GridSumCapConfig {
3591
3865
  message?: string;
3592
3866
  }
3593
3867
 
3868
+ /**
3869
+ * Folds consecutive entries of one burst into one group. Entries sharing a
3870
+ * group_key always fold; entries without one fold when the same actor did the
3871
+ * same action on the same collection, each within `windowMs` of the previous
3872
+ * (rows from before migration 516). Order is kept; singles are groups of one.
3873
+ */
3874
+ export declare function groupActivity<T extends ActivityEntryInput>(entries: T[], opts?: {
3875
+ windowMs?: number;
3876
+ }): ActivityGroup<T>[];
3877
+
3594
3878
  /**
3595
3879
  * Run one identity call through the gate: `blocked()` answers without a request while the tab
3596
3880
  * is signed out (outside its probe window); a success opens the gate, a signed-out 401 closes it.
@@ -3891,6 +4175,13 @@ export declare interface HelpVideoDto {
3891
4175
  * the page. Null when nothing was found or an author dismissed it;
3892
4176
  * publishing clears it. Optional: older servers. */
3893
4177
  stale?: HelpVideoStale | null;
4178
+ /** #18, GET /help-videos/for only: how the video matched the screen — tier
4179
+ * 0 step, 1 screen, 2 screens like it, 3 record type, 4 area, 5 a term on
4180
+ * the screen. Optional: older servers. */
4181
+ match?: {
4182
+ tier: number;
4183
+ reason: string;
4184
+ };
3894
4185
  }
3895
4186
 
3896
4187
  export declare function HelpVideoEditor({ videoId, onClose }: {
@@ -3935,6 +4226,28 @@ export declare function HelpVideoLibrary({ watchId, editId, onWatch, onEdit, hea
3935
4226
  headerExtra?: React.ReactNode;
3936
4227
  }): JSX.Element;
3937
4228
 
4229
+ /**
4230
+ * #18 — "Watch: How to submit an IR (2:10)": the screen's best help video,
4231
+ * when there is a strong match the person has not finished. Renders nothing
4232
+ * otherwise (no wrapper either), so a host can mount it in its header area
4233
+ * unconditionally. × hides that video's chip for good (this browser).
4234
+ *
4235
+ * Hosts pass the route (`path`) and, when they have one, their page key /
4236
+ * record context; the server ranks screen → screens like it → record type →
4237
+ * area → glossary terms (GET /help-videos/for).
4238
+ */
4239
+ export declare function HelpVideoMatchChip({ path, app, page, collection, item, terms, className }: {
4240
+ path: string;
4241
+ app?: string;
4242
+ page?: string;
4243
+ collection?: string;
4244
+ item?: string | number;
4245
+ /** Glossary terms the screen shows. */
4246
+ terms?: string[];
4247
+ /** Classes for the wrapper, which only renders when a chip shows. */
4248
+ className?: string;
4249
+ }): JSX.Element;
4250
+
3938
4251
  /** Keyed by video id: switching to another video starts a fresh player, so
3939
4252
  * progress, the resume point and the one-retry flag never carry over. */
3940
4253
  export declare function HelpVideoPlayer(props: HelpVideoPlayerProps): JSX.Element;
@@ -4147,6 +4460,16 @@ export declare function huddleHolds(prefs: Record<string, unknown> | null | unde
4147
4460
  /** Time without real input before someone counts as idle. */
4148
4461
  export declare const IDLE_AFTER_MS: number;
4149
4462
 
4463
+ /**
4464
+ * Reload old tabs when idle (#2937) — renders nothing. Mounted by ForceReloadBanner, so every
4465
+ * host that shows pushed reloads also gets this. The decision is lib/idle-reload.ts
4466
+ * `decideIdleReload` (pure, tested); this only feeds it and acts on it.
4467
+ *
4468
+ * Asks GET /realtime/build-status through the host's NivaroClient when there is one, else the
4469
+ * same-origin /api (admin mounts the banner outside its NivaroProvider).
4470
+ */
4471
+ export declare function IdleReloadWatcher(): null;
4472
+
4150
4473
  /** The fields a presence heartbeat should carry. */
4151
4474
  export declare function idleState(): {
4152
4475
  is_idle: boolean;
@@ -4634,8 +4957,9 @@ declare interface InputBinding {
4634
4957
  export declare function installClientChaosFetch(): void;
4635
4958
 
4636
4959
  /**
4637
- * Document-level intent listener: resting on (or keyboard-focusing) a same-origin link
4638
- * preloads its screen's code, and a record link also warms the record. Returns the uninstaller.
4960
+ * Document-level intent listener: resting on (or keyboard-focusing) a same-origin link preloads
4961
+ * its screen's code; a record link also warms the record, a list link the list's metadata.
4962
+ * Leaving the link before pressing it cancels what it started. Returns the uninstaller.
4639
4963
  */
4640
4964
  export declare function installIntentPrefetch(opts: IntentPrefetchOptions): () => void;
4641
4965
 
@@ -4661,6 +4985,17 @@ export declare interface IntegrationDotsProps {
4661
4985
  className?: string;
4662
4986
  }
4663
4987
 
4988
+ declare interface IntegrationErrorExplanation {
4989
+ sentence: string;
4990
+ cause: string;
4991
+ fix_label: string | null;
4992
+ fix: IntegrationFixTarget | null;
4993
+ fix_link: string | null;
4994
+ field: string | null;
4995
+ detail: string | null;
4996
+ rule: string;
4997
+ }
4998
+
4664
4999
  /**
4665
5000
  * One entry of the integration events feed. The cross-record feed carries
4666
5001
  * `provider`; the per-record feed (GET /integration-events/record/...) carries
@@ -4718,6 +5053,25 @@ export declare type IntegrationEventPathTarget = {
4718
5053
  chainId: string;
4719
5054
  };
4720
5055
 
5056
+ /** GET answers' `explanation` (api services/integration-error-explain.ts). */
5057
+ declare type IntegrationFixTarget = {
5058
+ kind: 'record';
5059
+ collection: string;
5060
+ item: string;
5061
+ field?: string | null;
5062
+ } | {
5063
+ kind: 'external_api';
5064
+ id: number;
5065
+ section?: 'auth' | 'endpoints' | 'connection' | 'response' | null;
5066
+ } | {
5067
+ kind: 'partner';
5068
+ id: number;
5069
+ } | {
5070
+ kind: 'payload_template';
5071
+ api_id: number;
5072
+ key: string;
5073
+ };
5074
+
4721
5075
  export declare function IntegrationObligationsView({ api, className }: IntegrationObligationsViewProps): JSX.Element;
4722
5076
 
4723
5077
  export declare interface IntegrationObligationsViewProps {
@@ -4915,10 +5269,6 @@ export declare interface IntegrationsConsoleTab {
4915
5269
  render: () => ReactNode;
4916
5270
  }
4917
5271
 
4918
- /**
4919
- * HTTP shapes served by GET /integration-signals and /integration-partners —
4920
- * copied from the core routes so the shared package never imports api code.
4921
- */
4922
5272
  export declare interface IntegrationSignalAction {
4923
5273
  kind: 'retry_submission' | 'resend' | 'open' | 'explain' | 'extension';
4924
5274
  label: string;
@@ -4984,12 +5334,29 @@ declare interface IntentPrefetchOptions {
4984
5334
  client: NivaroClient | null;
4985
5335
  /** A link's same-origin path → the record it opens, or null. */
4986
5336
  matchRecord?: (path: string) => RecordTarget | null;
5337
+ /** A link's same-origin path → the collection list it opens, or null (#3230). */
5338
+ matchList?: (path: string) => string | null;
4987
5339
  /** Load the code for the screen a same-origin path opens. */
4988
5340
  preloadRoute?: (path: string) => void;
4989
5341
  /** Only links inside this element (default: the whole document). */
4990
5342
  root?: HTMLElement | Document;
4991
5343
  }
4992
5344
 
5345
+ export declare interface IntentProps {
5346
+ onPointerEnter: () => void;
5347
+ onPointerLeave: () => void;
5348
+ onPointerDown: () => void;
5349
+ onFocus: () => void;
5350
+ onBlur: () => void;
5351
+ }
5352
+
5353
+ /**
5354
+ * Handlers that turn resting on (or focusing) an element into `act()` after INTENT_DWELL_MS,
5355
+ * and leaving it into a cancel of what `act` started (unless it was pressed). `group` names the
5356
+ * intent (e.g. `record:projects:7`); `act` must start its prefetches under that group.
5357
+ */
5358
+ export declare function intentProps(group: string, act: () => number | boolean | undefined): IntentProps;
5359
+
4993
5360
  /** A title card before the recording (real extra edited time). Blank
4994
5361
  * `title` / `subtitle` mean the video's title and the first line of its
4995
5362
  * description. */
@@ -5283,6 +5650,12 @@ declare interface JsonMapEditorConfig {
5283
5650
  };
5284
5651
  }
5285
5652
 
5653
+ /**
5654
+ * A total the person has already seen for this list, from any cached page of it (search, sort
5655
+ * and filters aside) — so "Reading 48,000 invoices" never counts rows they cannot see.
5656
+ */
5657
+ export declare function knownTotalFrom(entries: ReadonlyArray<readonly [unknown, unknown]>): number | null;
5658
+
5286
5659
  declare interface LadderTier {
5287
5660
  after_hours: number;
5288
5661
  notify: string;
@@ -5311,6 +5684,15 @@ declare interface LayoutAssignment {
5311
5684
  default_expanded?: boolean | number | null;
5312
5685
  }
5313
5686
 
5687
+ declare interface LayoutAssignmentLike {
5688
+ field: string;
5689
+ sort?: number | null;
5690
+ group_key?: string | null;
5691
+ col_span?: number | null;
5692
+ is_visible?: boolean | number | null;
5693
+ widget_id?: number | null;
5694
+ }
5695
+
5314
5696
  declare interface LayoutDesign {
5315
5697
  selected: DesignTarget | null;
5316
5698
  onSelect(t: DesignTarget, opts?: SelectOptions): void;
@@ -5382,10 +5764,15 @@ declare interface LayoutDesignProp {
5382
5764
  *
5383
5765
  * Zero CSS shipped — style via className props or data-* attributes.
5384
5766
  */
5385
- export declare function LayoutForm({ form, components, className, style, sectionClassName, tabStripClassName, activeTabClassName, inactiveTabClassName, gridClassName, slotRenderers, itemId, stepperClassName, stepNavClassName, prevButtonClassName, nextButtonClassName, lastStepActions, layoutSlug: _layoutSlug }: LayoutFormProps): default_2.JSX.Element | null;
5767
+ export declare function LayoutForm({ form, components, className, style, sectionClassName, tabStripClassName, activeTabClassName, inactiveTabClassName, gridClassName, slotRenderers, itemId, stepperClassName, stepNavClassName, prevButtonClassName, nextButtonClassName, lastStepActions, layoutSlug: _layoutSlug, loadingFallback }: LayoutFormProps): string | number | bigint | boolean | default_2.JSX.Element | Iterable<default_2.ReactNode> | Promise<string | number | bigint | boolean | default_2.ReactPortal | default_2.ReactElement<unknown, string | default_2.JSXElementConstructor<any>> | Iterable<default_2.ReactNode> | null | undefined> | null;
5386
5768
 
5387
5769
  declare type LayoutFormProps = {
5388
5770
  form: UseNivaroFormReturn;
5771
+ /**
5772
+ * #3229 — shown while the schema or the record is still loading (default: nothing, the form
5773
+ * stays headless). Pass a shape-matched placeholder, e.g. `<FormSkeleton plan={…} />`.
5774
+ */
5775
+ loadingFallback?: default_2.ReactNode;
5389
5776
  components?: ComponentOverrides;
5390
5777
  /** CSS class applied to the <form> element */
5391
5778
  className?: string;
@@ -5453,8 +5840,24 @@ declare interface LayoutGroup {
5453
5840
  summary_hide_empty?: boolean | number | null;
5454
5841
  }
5455
5842
 
5843
+ declare interface LayoutGroupLike {
5844
+ id?: number | null;
5845
+ key: string;
5846
+ label?: string | null;
5847
+ type?: string | null;
5848
+ sort?: number | null;
5849
+ container_id?: number | null;
5850
+ visibility_mode?: string | null;
5851
+ }
5852
+
5456
5853
  export declare type LayoutItem = FormGroupDescriptor | '__ungrouped__';
5457
5854
 
5855
+ declare interface LayoutLike {
5856
+ groups?: LayoutGroupLike[] | null;
5857
+ assignments?: LayoutAssignmentLike[] | null;
5858
+ ungrouped_sort?: number | null;
5859
+ }
5860
+
5458
5861
  declare interface LayoutMeta {
5459
5862
  id: number;
5460
5863
  name: string;
@@ -5588,6 +5991,11 @@ export declare function ListLoadError({ error, onRetry, className }: {
5588
5991
  */
5589
5992
  export declare function listLoadErrorText(err: unknown): string;
5590
5993
 
5994
+ /** The query key a collection list (CollectionBrowserView) reads its metadata under. */
5995
+ export declare function listQueryKeys(collection: string): {
5996
+ meta: QueryKey;
5997
+ };
5998
+
5591
5999
  /**
5592
6000
  * "Copy as …" (#654): the request a list or record view is making, rendered
5593
6001
  * as a curl command, an SDK call and a GraphQL document. Pure — no client,
@@ -5606,6 +6014,22 @@ declare interface ListRequest {
5606
6014
  page?: number;
5607
6015
  }
5608
6016
 
6017
+ /**
6018
+ * A whole list placeholder for surfaces without a real header yet: a header strip (real labels
6019
+ * when given) and rows at the real row height, one bar per column.
6020
+ */
6021
+ export declare function ListSkeleton({ columns, rows, header, rowClassName, className }: {
6022
+ columns: readonly (SkeletonColumn & {
6023
+ label?: ReactNode;
6024
+ grow?: number;
6025
+ })[];
6026
+ rows?: number;
6027
+ /** false = no header strip. */
6028
+ header?: boolean;
6029
+ rowClassName?: string;
6030
+ className?: string;
6031
+ }): JSX.Element;
6032
+
5609
6033
  /**
5610
6034
  * Query-key heads of surfaces that keep themselves live with their own socket handlers (record
5611
6035
  * pages — RECORD_QUERY_HEADS in item-edit/RecordLiveSync — the collection browser's row reads,
@@ -5977,6 +6401,11 @@ declare interface MatrixEditorConfig {
5977
6401
  };
5978
6402
  }
5979
6403
 
6404
+ /** Prefetch requests one tab runs at once; the rest wait their turn. */
6405
+ export declare const MAX_PREFETCH_IN_FLIGHT = 2;
6406
+
6407
+ declare type MaybeIntent = IntentProps | Record<string, never>;
6408
+
5980
6409
  /**
5981
6410
  * The offset from a few samples: the server stamped its answer somewhere inside the round
5982
6411
  * trip, so assume the midpoint, and trust the sample with the shortest round trip (the least
@@ -6137,6 +6566,11 @@ export declare type NavigationContextValue = {
6137
6566
  helpVideosPath?: string;
6138
6567
  };
6139
6568
 
6569
+ declare interface NetInfo {
6570
+ saveData?: boolean;
6571
+ effectiveType?: string;
6572
+ }
6573
+
6140
6574
  export declare function networkdaysBetween(a: Date, b: Date): number;
6141
6575
 
6142
6576
  export declare function NewImportDialog({ open, onOpenChange, definitions, runCounts, onQueued }: {
@@ -6284,7 +6718,7 @@ export declare function NotificationAnalyticsView({ className, compactHeader }:
6284
6718
  * click-through routing via the shared notification-target resolver. Needs
6285
6719
  * `<NivaroProvider>`.
6286
6720
  */
6287
- export declare function NotificationBell({ routes, onNavigate, app, subscribe, buttonClassName, panelClassName, beforeList, extraBadge, mailLogUrl, renderTrigger, allPath, subscriptionsPath, onActionError, panelPlacement, tabBadge }: NotificationBellProps): JSX.Element;
6721
+ export declare function NotificationBell({ routes, onNavigate, app, subscribe, buttonClassName, panelClassName, beforeList, extraBadge, mailLogUrl, renderTrigger, allPath, subscriptionsPath, rulesPath, onActionError, panelPlacement, tabBadge }: NotificationBellProps): JSX.Element;
6288
6722
 
6289
6723
  export declare interface NotificationBellProps {
6290
6724
  /** Where each notification kind lands in the host app. */
@@ -6322,6 +6756,8 @@ export declare interface NotificationBellProps {
6322
6756
  allPath?: string | null;
6323
6757
  /** Path of the subscriptions page — a watch's "Why me?" offers a Manage link. */
6324
6758
  subscriptionsPath?: string | null;
6759
+ /** Path of Profile → Notification rules — "Why?" offers a change link there (#3078). */
6760
+ rulesPath?: string | null;
6325
6761
  /** Called with an error message when an inline action fails. */
6326
6762
  onActionError?: (message: string) => void;
6327
6763
  /** Where the panel opens relative to the trigger. 'below' (default) keeps
@@ -6360,7 +6796,7 @@ export declare interface NotificationBundle<R = unknown> {
6360
6796
  rows: R[];
6361
6797
  }
6362
6798
 
6363
- export declare function NotificationCenterView({ routes, onNavigate, app, mailLogUrl, subscriptionsPath, onNotice, onError, className }: NotificationCenterViewProps): JSX.Element;
6799
+ export declare function NotificationCenterView({ routes, onNavigate, app, mailLogUrl, subscriptionsPath, rulesPath, changesId, onNotice, onError, className }: NotificationCenterViewProps): JSX.Element;
6364
6800
 
6365
6801
  export declare interface NotificationCenterViewProps {
6366
6802
  routes: NotificationRouteMap;
@@ -6369,6 +6805,11 @@ export declare interface NotificationCenterViewProps {
6369
6805
  mailLogUrl?: (mailLogId: number) => string | null;
6370
6806
  /** Host route of the subscriptions page (a watch's "Why me?" links there). */
6371
6807
  subscriptionsPath?: string | null;
6808
+ /** Host route of Profile → Notification rules — "Why?" offers a change link there (#3078). */
6809
+ rulesPath?: string | null;
6810
+ /** #3079 — open on one notification's changes (the email's "See what changed" link lands
6811
+ * with ?changes=<id>). Undefined = read ?changes= from the address bar. */
6812
+ changesId?: number | null;
6372
6813
  /** Toast hooks — hosts wire their own toaster. */
6373
6814
  onNotice?: (message: string) => void;
6374
6815
  onError?: (message: string) => void;
@@ -6837,6 +7278,8 @@ declare interface PartnerCall {
6837
7278
  triggered_by: string | null;
6838
7279
  has_body: boolean;
6839
7280
  user: PartnerCallUser | null;
7281
+ /** #18 — a failed call in plain words + Fix it target (null when it did not fail). */
7282
+ explanation?: IntegrationErrorExplanation | null;
6840
7283
  }
6841
7284
 
6842
7285
  /** One user recorded on a call — via nivaro_users, batch-resolved server-side
@@ -7062,7 +7505,7 @@ export declare interface PersonProfileData {
7062
7505
  } | null;
7063
7506
  }
7064
7507
 
7065
- export declare type PersonProfileTab = 'overview' | 'access' | 'activity' | 'tools';
7508
+ export declare type PersonProfileTab = 'overview' | 'access' | 'activity' | 'did' | 'tools';
7066
7509
 
7067
7510
  /** Mirror of the server's PersonProfile (services/user-profile.ts). */
7068
7511
  declare interface PersonRef {
@@ -7112,7 +7555,7 @@ declare type PipelineBinding = {
7112
7555
  dimensions?: PipelineOwnerDimension[];
7113
7556
  };
7114
7557
 
7115
- export declare type PipelineEditorSection = 'states' | 'transitions' | 'branches' | 'bindings' | 'matrix' | 'simulator' | 'canvas' | 'flow-map' | 'replay' | 'cumulative-flow' | 'migration' | 'coverage' | 'lint' | 'skip-report' | 'ai-review' | 'versions' | 'matrix-versions';
7558
+ export declare type PipelineEditorSection = 'states' | 'transitions' | 'branches' | 'bindings' | 'matrix' | 'simulator' | 'canvas' | 'flow-map' | 'replay' | 'cumulative-flow' | 'migration' | 'coverage' | 'lint' | 'skip-report' | 'history' | 'ai-review' | 'versions' | 'matrix-versions';
7116
7559
 
7117
7560
  export declare function PipelineEditorView({ templateId, onBack, onExport, hidden }: {
7118
7561
  templateId: string;
@@ -7282,12 +7725,22 @@ export declare function precisionOf(options: unknown, fallback?: number): number
7282
7725
  /** API prefetches one tab may make in any rolling minute. */
7283
7726
  export declare const PREFETCH_PER_MINUTE = 30;
7284
7727
 
7728
+ /**
7729
+ * Warm a collection list: its metadata (fields, relations, browse config) — what the list waits
7730
+ * on before it can ask for rows, and what shapes its loading skeleton. The rows themselves
7731
+ * depend on the person's saved view, sort and filters, so they are not guessed.
7732
+ */
7733
+ export declare function prefetchCollectionList(qc: QueryClient, client: NivaroClient, collection: string, opts?: {
7734
+ group?: string | null;
7735
+ }): number;
7736
+
7285
7737
  /**
7286
7738
  * Warm a record page: its readOne and the layout it will open with. Returns how many requests
7287
- * went out (0 when both were fresh or the budget was spent).
7739
+ * were queued (0 when both were fresh or the budget was spent).
7288
7740
  */
7289
7741
  export declare function prefetchRecord(qc: QueryClient, client: NivaroClient, collection: string, id: string | number, opts?: {
7290
7742
  layout?: boolean;
7743
+ group?: string | null;
7291
7744
  }): number;
7292
7745
 
7293
7746
  export declare interface PreparedReplayClip {
@@ -7396,7 +7849,7 @@ export declare function ProfileView({ userId, className, initialTab, extra, push
7396
7849
  userId?: string | null;
7397
7850
  className?: string;
7398
7851
  /** Which tab an admin lands on for someone else's page. */
7399
- initialTab?: 'overview' | 'access' | 'activity' | 'tools';
7852
+ initialTab?: 'overview' | 'access' | 'activity' | 'did' | 'tools';
7400
7853
  /** Host-only cards for the OWN profile (a browser-push card, say), rendered in the right column. */
7401
7854
  extra?: React.ReactNode;
7402
7855
  /** Service worker the Browser notifications card registers (same origin); false hides the card. */
@@ -8321,6 +8774,9 @@ declare type RecordingHandoff = {
8321
8774
  };
8322
8775
  };
8323
8776
 
8777
+ /** Feed one location change. Returns the entry now current. */
8778
+ export declare function recordNavigation(ev: TrailEvent): TrailEntry | undefined;
8779
+
8324
8780
  /**
8325
8781
  * Mounted ONCE by a host (admin's AppLayout), inside a NivaroProvider and a
8326
8782
  * QueryClientProvider: binds the outbox to the signed-in person so saves left
@@ -8514,6 +8970,18 @@ export declare function registerHelpVideoPage(key: string): () => void;
8514
8970
 
8515
8971
  export declare function registerLayoutSlot(plugin: LayoutSlotPlugin): void;
8516
8972
 
8973
+ /**
8974
+ * "Is this tab in the middle of something live that a reload would cut off?" — a live huddle, a
8975
+ * screen clip being recorded. Not unsaved work (that is lib/unsaved-work.ts, the only source of
8976
+ * "unsaved changes"): nothing here is lost to a reload, it is interrupted. Read by the automatic
8977
+ * idle reload (lib/idle-reload.ts), which never reloads while any hold is on.
8978
+ *
8979
+ * Leaf module: the huddle store and the screen clip button report here, so the reload code never
8980
+ * imports the chat code.
8981
+ */
8982
+ /** Report something live; the returned function removes it. `active` is read at decision time. */
8983
+ export declare function registerReloadHold(label: string, active?: () => boolean): () => void;
8984
+
8517
8985
  /** Called by a host's recorder module. Returns an undo for tests / HMR. */
8518
8986
  export declare function registerReplayCapture(fn: Capture, snap?: Snapshot): () => void;
8519
8987
 
@@ -8533,8 +9001,12 @@ export declare function RelatedRecordsPanel({ collection, itemId, title, default
8533
9001
  defaultExpanded?: boolean;
8534
9002
  }): JSX.Element;
8535
9003
 
8536
- export declare function RelationCombobox({ collection, value, onChange, disabled, placeholder, extraFilter, autoSelectSingle, optionSort, requiredParent, facets, fieldKey, narrowedBy, pinned, missPlacement }: {
9004
+ export declare function RelationCombobox({ collection, value, onChange, disabled, placeholder, extraFilter, autoSelectSingle, optionSort, requiredParent, facets, fieldKey, narrowedBy, pinned, missPlacement, defaultOpen, onClose }: {
8537
9005
  collection: string;
9006
+ /** Open the options panel on mount (a list cell's inline editor, #3224). */
9007
+ defaultOpen?: boolean;
9008
+ /** Called when the options panel closes (picked, clicked away or Escape). */
9009
+ onClose?: () => void;
8538
9010
  /** The record-form placement this picker sits on: searches that find
8539
9011
  * nothing are counted there (#2726). Absent = not reported. */
8540
9012
  missPlacement?: PickerMissPlacement | null;
@@ -8622,6 +9094,12 @@ export declare function releaseGatedReadsOnSharedFetch(cache: QueryCacheLike): (
8622
9094
 
8623
9095
  export declare function releaseHeld(): HeldItem[];
8624
9096
 
9097
+ /** "Pat updated 40 workflows: Status → On Hold"; a group of one reads as its entry. */
9098
+ export declare function renderActivityGroupSentence(entries: ActivityEntryInput[], labels?: ActivityLabels | null): string;
9099
+
9100
+ /** One readable sentence for one activity entry. */
9101
+ export declare function renderActivitySentence(entry: ActivityEntryInput, labels?: ActivityLabels | null): string;
9102
+
8625
9103
  export declare type RenderFieldProps = {
8626
9104
  field: any;
8627
9105
  value: unknown;
@@ -9326,6 +9804,13 @@ export declare function setServerClockOffset(ms: number): void;
9326
9804
 
9327
9805
  export declare function setTimeDisplay(mode: string | null | undefined): void;
9328
9806
 
9807
+ /**
9808
+ * Name a page in the trail (what later crumbs will show for it): the current
9809
+ * entry — and, when a key is given, only while that key is still current (a
9810
+ * label that lands after the person moved on is dropped, never misfiled).
9811
+ */
9812
+ export declare function setTrailLabel(label: string | null | undefined, key?: string): void;
9813
+
9329
9814
  /** True when this browser can share a socket between tabs. */
9330
9815
  export declare function sharedWorkerSupported(): boolean;
9331
9816
 
@@ -9405,6 +9890,27 @@ declare type Size = {
9405
9890
  h: number;
9406
9891
  };
9407
9892
 
9893
+ export declare function SkeletonBar({ shape, width, className, style }: {
9894
+ shape?: CellShape;
9895
+ width?: string | number;
9896
+ className?: string;
9897
+ style?: CSSProperties;
9898
+ }): JSX.Element;
9899
+
9900
+ export declare interface SkeletonColumn {
9901
+ key: string;
9902
+ shape?: CellShape;
9903
+ /** Fixed column width in px (a resized column), when the list has one. */
9904
+ width?: number | null;
9905
+ }
9906
+
9907
+ /** Real list columns → skeleton columns (types decide the shapes). */
9908
+ export declare function skeletonColumnsFrom(keys: readonly string[], fields: ReadonlyArray<{
9909
+ field: string;
9910
+ type?: string | null;
9911
+ interface?: string | null;
9912
+ }>, widths?: Record<string, number | undefined>): SkeletonColumn[];
9913
+
9408
9914
  declare type SkipCondition = {
9409
9915
  type: 'no_owners';
9410
9916
  } | {
@@ -9488,6 +9994,80 @@ export declare type SlotRendererProps = {
9488
9994
  defaultExpanded: boolean;
9489
9995
  };
9490
9996
 
9997
+ /**
9998
+ * "This is taking a while" with a reason (#3231). Renders nothing until the read has been
9999
+ * loading ~3 s, then says what it is waiting on in plain words ("Reading 48,000 invoices",
10000
+ * "Waiting for Fusion", "Waiting for a free slot — busy right now") with the elapsed time.
10001
+ *
10002
+ * variant 'inline' — a line inside a loading area (widgets, query results, forms)
10003
+ * variant 'pill' — a floating chip; the caller positions it (absolute) so nothing moves
10004
+ *
10005
+ * `children` ride beside the text (e.g. a Run in background button). Reads that can run in the
10006
+ * background use BackgroundRunOffer, which shows the same reason.
10007
+ */
10008
+ export declare function SlowNotice({ variant, className, children, reason: given, ...opts }: UseSlowReasonOptions & {
10009
+ variant?: 'inline' | 'pill';
10010
+ className?: string;
10011
+ children?: ReactNode;
10012
+ /** A reason the host already computed with useSlowReason (skips the hook's own asking). */
10013
+ reason?: SlowReason;
10014
+ }): JSX.Element | null;
10015
+
10016
+ export declare function SlowNoticeView({ reason, variant, className, extra }: {
10017
+ reason: SlowReason;
10018
+ variant?: 'inline' | 'pill';
10019
+ className?: string;
10020
+ extra?: ReactNode;
10021
+ }): JSX.Element;
10022
+
10023
+ export declare interface SlowReadRequest {
10024
+ method: 'GET' | 'POST';
10025
+ /** API path (with or without /api); a query string is ignored. */
10026
+ url: string;
10027
+ }
10028
+
10029
+ /** The server's answer (api/src/services/wait-reasons.ts WaitAnswer). */
10030
+ export declare interface SlowReadWaitAnswer {
10031
+ found: boolean;
10032
+ kind?: WaitKind;
10033
+ subject?: string | null;
10034
+ shared?: boolean;
10035
+ position?: number | null;
10036
+ of?: number | null;
10037
+ age_ms?: number;
10038
+ statement_ms?: number;
10039
+ rows_so_far?: number;
10040
+ collection?: string | null;
10041
+ /** About how many rows the table holds — administrators only. */
10042
+ table_rows?: number | null;
10043
+ peer?: boolean;
10044
+ }
10045
+
10046
+ export declare interface SlowReason {
10047
+ /** Loading for at least `afterMs`. */
10048
+ slow: boolean;
10049
+ /** How long it has been loading, ms (0 while not slow). */
10050
+ elapsedMs: number;
10051
+ /** The plain sentence, or null when the server knows nothing useful. */
10052
+ text: string | null;
10053
+ answer: SlowReadWaitAnswer | null;
10054
+ }
10055
+
10056
+ declare interface SlowReasonContext {
10057
+ /** Plural noun for what a list reads, e.g. "invoices". */
10058
+ noun?: string | null;
10059
+ /** What the page calls this read, e.g. "PUB health" (a query or widget title). */
10060
+ label?: string | null;
10061
+ /** Rows the person can see, when the page already knows (a total from an earlier load). */
10062
+ knownRows?: number | null;
10063
+ }
10064
+
10065
+ /**
10066
+ * One plain sentence for what a slow read is waiting on, or null when the server knows nothing
10067
+ * useful (the request already finished, another replica not seen yet, cloud mode).
10068
+ */
10069
+ export declare function slowReasonText(answer: SlowReadWaitAnswer | null | undefined, ctx?: SlowReasonContext): string | null;
10070
+
9491
10071
  /**
9492
10072
  * Two-phase capture: take the moment NOW (the clip's events, or the live
9493
10073
  * recording's offset), upload later. A support dialog snapshots when it
@@ -9961,6 +10541,23 @@ export declare type SynonymGroups = ReadonlyArray<ReadonlyArray<string>>;
9961
10541
 
9962
10542
  declare type TabKey = 'catalog' | 'rules' | 'subscriptions' | 'history' | 'widget_alerts' | 'widget_alert_history' | 'anomaly_rules' | 'anomaly_history';
9963
10543
 
10544
+ /**
10545
+ * Body rows for a real `<table>` whose header is already on screen: one cell per real column
10546
+ * (plus the table's own leading/trailing cells), with the row height and cell padding of the
10547
+ * real rows. Render inside `<tbody>`.
10548
+ */
10549
+ export declare function TableSkeletonRows({ columns, rows, rowClassName, cellClassName, leadingCells, trailingCells, cellStyle, barClassName }: {
10550
+ columns: readonly SkeletonColumn[];
10551
+ rows?: number;
10552
+ rowClassName?: string;
10553
+ cellClassName?: string;
10554
+ leadingCells?: number;
10555
+ trailingCells?: number;
10556
+ /** Per-column style (pinned offsets, fixed widths) — the real cells' own. */
10557
+ cellStyle?: (key: string) => CSSProperties | undefined;
10558
+ barClassName?: string;
10559
+ }): JSX.Element;
10560
+
9964
10561
  export declare type TabState = {
9965
10562
  activeTab: string | null;
9966
10563
  setActiveTab: (key: string) => void;
@@ -10141,6 +10738,68 @@ export declare function TrafficMiniMap({ window: win, tone, href, refreshMs, cla
10141
10738
  className?: string;
10142
10739
  }): JSX.Element | null;
10143
10740
 
10741
+ /**
10742
+ * #3225 — breadcrumbs that know the path. A per-TAB record of the history
10743
+ * entries this tab walked through (sessionStorage, so a reload keeps it and
10744
+ * another tab has its own), keyed by the router's history index. The page
10745
+ * header turns the entries before the current one into crumbs — how the
10746
+ * person actually got here (Queue "Pending approvals" → PO 4471 → Line 3) —
10747
+ * and a crumb click goes BACK that many history entries, so the page comes
10748
+ * back exactly as it was left: same URL (filters, tab), and the scroll
10749
+ * position the trail remembered.
10750
+ *
10751
+ * The host feeds it: call `recordNavigation()` on every location change with
10752
+ * the router's action ('PUSH' | 'REPLACE' | 'POP'), the location key and
10753
+ * `history.state.idx` (React Router keeps the index there across reloads).
10754
+ * Nothing here touches the router, so any host (admin, efp-new) can wire it.
10755
+ *
10756
+ * Pure parts (`reduceTrail`, `trailChain`, `displayTrail`) are tested on their own.
10757
+ */
10758
+ declare type TrailAction = 'PUSH' | 'REPLACE' | 'POP';
10759
+
10760
+ /** The unbroken run of entries right before the current one, oldest first. */
10761
+ export declare function trailChain(state: TrailState): TrailEntry[];
10762
+
10763
+ declare interface TrailCrumb {
10764
+ key: string;
10765
+ path: string;
10766
+ label?: string;
10767
+ /** History steps to go back to reach it (negative). */
10768
+ delta: number;
10769
+ }
10770
+
10771
+ declare interface TrailEntry {
10772
+ key: string;
10773
+ idx: number;
10774
+ /** pathname + search + hash */
10775
+ path: string;
10776
+ label?: string;
10777
+ scroll?: TrailScroll[];
10778
+ at: number;
10779
+ }
10780
+
10781
+ declare interface TrailEvent {
10782
+ action: TrailAction;
10783
+ key: string;
10784
+ idx: number;
10785
+ path: string;
10786
+ at?: number;
10787
+ }
10788
+
10789
+ declare interface TrailScroll {
10790
+ /** '' = the page scroller itself; otherwise a child path from it (see scrollPathOf). */
10791
+ path: string;
10792
+ top: number;
10793
+ }
10794
+
10795
+ declare interface TrailState {
10796
+ entries: TrailEntry[];
10797
+ current: {
10798
+ key: string;
10799
+ idx: number;
10800
+ } | null;
10801
+ }
10802
+
10144
10803
  /**
10145
10804
  * Translate scope values (target-collection IDS) into a filter's value space.
10146
10805
  * When the filter compares ids (valueField 'id'/undefined) this is identity;
@@ -10313,6 +10972,15 @@ declare type UploadRow = {
10313
10972
 
10314
10973
  export declare function useActiveGamedays(enabled?: boolean): ActiveGameday[];
10315
10974
 
10975
+ /**
10976
+ * Labels for a page of entries, read AS THE VIEWER (POST /api/activity/labels): records they
10977
+ * cannot see come back null ("a record you can't see"), people by display name, collection /
10978
+ * field / choice labels. Adds the viewer's time zone. One request per distinct page.
10979
+ */
10980
+ export declare function useActivityLabels(entries: ActivityEntryInput[], opts?: {
10981
+ enabled?: boolean;
10982
+ }): ActivityLabels | undefined;
10983
+
10316
10984
  /**
10317
10985
  * `useAfterIdle(ms)` — false on mount, true once the browser has had `ms` of
10318
10986
  * settle time after the first paint. Gate secondary reads on it (freshness
@@ -10645,10 +11313,19 @@ export declare function useHelpVideosFor(ctx: {
10645
11313
  item?: string | number;
10646
11314
  state?: string;
10647
11315
  page?: string;
11316
+ /** #18: the route (`/collections/purchase_orders/12`); the server derives
11317
+ * the screen, screens like it, the record type and the area from it. */
11318
+ path?: string;
11319
+ /** #18: which app `path` belongs to (default `admin`). */
11320
+ app?: string;
11321
+ /** #18: comma list of glossary terms the screen shows. */
11322
+ terms?: string;
10648
11323
  }, enabled?: boolean): UseQueryResult<NoInfer<{
10649
11324
  data: HelpVideoDto[];
10650
11325
  can_author: boolean;
10651
11326
  state: string | null;
11327
+ /** #18: the strong match this person has not finished. Older servers: absent. */
11328
+ best_id?: string | null;
10652
11329
  }>, Error>;
10653
11330
 
10654
11331
  /** "Show me on this page": the published version's labelled clicks. */
@@ -10786,6 +11463,9 @@ export declare function useMyScopes(): {
10786
11463
 
10787
11464
  export declare function useNavigation(): NavigationContextValue;
10788
11465
 
11466
+ /** The tab's trail, re-rendering on every change. */
11467
+ export declare function useNavTrail(): TrailState;
11468
+
10789
11469
  export declare function useNivaroClient(): NivaroClient;
10790
11470
 
10791
11471
  /**
@@ -10911,6 +11591,16 @@ export declare function usePersonProfile(userId: string | null | undefined): Use
10911
11591
 
10912
11592
  export declare function usePlanGridHost(): PlanGridHost | null;
10913
11593
 
11594
+ /**
11595
+ * `const intent = usePrefetchIntent()` → spread `{...intent.record(collection, id)}` on a row or
11596
+ * button that opens a record, `{...intent.list(collection)}` on one that opens a list. Inert
11597
+ * without a client (nothing to fetch with).
11598
+ */
11599
+ export declare function usePrefetchIntent(): {
11600
+ record: (collection: string, id: string | number) => MaybeIntent;
11601
+ list: (collection: string) => MaybeIntent;
11602
+ };
11603
+
10914
11604
  /** Who else is viewing one record (joins its room; names only). */
10915
11605
  export declare function usePresence(collection: string | null | undefined, item: string | number | null | undefined): RecordPresence;
10916
11606
 
@@ -11121,6 +11811,23 @@ action: IntegrationSignalRow["actions"][number];
11121
11811
 
11122
11812
  export declare function useSignals(tab?: string): UseQueryResult<NoInfer<IntegrationSignalsSnapshot>, Error>;
11123
11813
 
11814
+ /**
11815
+ * #3231 — after ~3 s of loading, ask the server what this read is waiting on and keep asking
11816
+ * every few seconds while it still loads (hidden tabs skip a beat). Nothing is asked for a read
11817
+ * that finishes in time, so a fast page makes no extra call.
11818
+ */
11819
+ export declare function useSlowReason(opts: UseSlowReasonOptions): SlowReason;
11820
+
11821
+ declare interface UseSlowReasonOptions extends SlowReasonContext {
11822
+ /** True while the page's own read is still loading. */
11823
+ pending: boolean;
11824
+ /** The read the page is making; null = ask nothing (the notice still says it is slow). */
11825
+ request: SlowReadRequest | null;
11826
+ /** How long before the read counts as slow (default 3 s). */
11827
+ afterMs?: number;
11828
+ enabled?: boolean;
11829
+ }
11830
+
11124
11831
  export declare function useSnooze(): {
11125
11832
  add: UseMutationResult<unknown, Error, SnoozeInput, unknown>;
11126
11833
  remove: UseMutationResult<unknown, Error, number, unknown>;
@@ -11327,6 +12034,17 @@ declare type Visibility = {
11327
12034
  role_ids: string[];
11328
12035
  };
11329
12036
 
12037
+ /**
12038
+ * "This is taking a while" with a reason (#3231) — the client half of
12039
+ * GET /api/background-runs/waiting.
12040
+ *
12041
+ * The server tells a page what ITS OWN unfinished read is waiting on (a free heavy-read slot and
12042
+ * where it stands in line, an external partner, an uncached query or widget, a statement still
12043
+ * running); this file turns that answer into one plain sentence. Pure — the hook and the notice
12044
+ * live in hooks/useSlowReason.ts and components/SlowNotice.tsx.
12045
+ */
12046
+ declare type WaitKind = 'slot' | 'external' | 'query' | 'widget' | 'database' | 'auth' | 'working';
12047
+
11330
12048
  /** One step of "Show me on this page" (GET /help-videos/:id/walk). */
11331
12049
  export declare interface WalkStep {
11332
12050
  label: string;