@nivaro/react 0.1.66 → 0.1.68

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 (3) hide show
  1. package/dist/index.d.ts +1334 -122
  2. package/dist/index.js +38240 -37960
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { CascadeFilterRule } from '@nivaro/sdk';
2
2
  import { default as default_2 } from 'react';
3
3
  import { FieldDependencyConfig } from '@nivaro/sdk';
4
- import type { ImportParseResponse } from '@nivaro/sdk';
4
+ import { ImportParseResponse } from '@nivaro/sdk';
5
+ import { ImportTemplateSummary } from '@nivaro/sdk';
5
6
  import { JSX } from 'react';
6
7
  import { JSXElementConstructor } from 'react';
7
8
  import { NivaroClient } from '@nivaro/sdk';
@@ -15,6 +16,56 @@ import { ReportQueryWidgetConfig } from '@nivaro/sdk';
15
16
  import { UseMutationResult } from '@tanstack/react-query';
16
17
  import { UserScopesInfo } from '@nivaro/sdk';
17
18
 
19
+ export declare function AccessDeniedPanel({ collection, itemId, onBack }: {
20
+ collection: string;
21
+ itemId: string;
22
+ /** Host-provided back action (e.g. navigate(-1)); renders a button when set. */
23
+ onBack?: () => void;
24
+ }): JSX.Element;
25
+
26
+ /**
27
+ * Options filter for every nivaro_users picker: mirrors listUsers() — redacted
28
+ * accounts never appear, and suspended users cannot be PICKED (existing values
29
+ * still display, since current-value lookups fetch by id with no filter).
30
+ * Null-safe on status so a fresh install with unset statuses keeps working.
31
+ */
32
+ export declare const ACTIVE_USER_OPTION_FILTER: {
33
+ _and: ({
34
+ is_redacted: {
35
+ _eq: boolean;
36
+ };
37
+ _or?: undefined;
38
+ } | {
39
+ _or: ({
40
+ status: {
41
+ _neq: string;
42
+ _null?: undefined;
43
+ };
44
+ } | {
45
+ status: {
46
+ _null: boolean;
47
+ _neq?: undefined;
48
+ };
49
+ })[];
50
+ is_redacted?: undefined;
51
+ })[];
52
+ };
53
+
54
+ export declare function AddendumPanel({ collection, item, addendumLayoutId, canCreate, onActiveCountChange, onApplied, defaultExpanded, }: {
55
+ collection: string;
56
+ item: string;
57
+ addendumLayoutId?: number | null;
58
+ canCreate?: boolean;
59
+ onActiveCountChange?: (count: number) => void;
60
+ /** Fired after an addendum is approved/rejected/submitted — i.e. whenever the
61
+ * parent record may now read differently. ItemEditForm uses it to refresh a
62
+ * generated PDF so the attached document matches the amended record. */
63
+ onApplied?: () => void;
64
+ defaultExpanded?: boolean;
65
+ }): JSX.Element | null;
66
+
67
+ export declare function agingBucket(hours: number | null): string;
68
+
18
69
  export declare function AlertManagerView({ defaultTab, hiddenTabs, onOpenReport, className }: AlertManagerViewProps): JSX.Element;
19
70
 
20
71
  export declare interface AlertManagerViewProps {
@@ -27,6 +78,91 @@ export declare interface AlertManagerViewProps {
27
78
  className?: string;
28
79
  }
29
80
 
81
+ declare type AlertOperator = 'gt' | 'gte' | 'lt' | 'lte' | 'eq' | 'change_pct';
82
+
83
+ export declare function AlertRuleDrawer({ open, definitions, editRule, preselectedDefinitionId, scopeSeeds, onClose, onSave, saving }: {
84
+ open: boolean;
85
+ definitions: MetricDefinition[];
86
+ editRule: MetricAlertRule | null;
87
+ preselectedDefinitionId?: number | null;
88
+ /** Pre-seeded filter values (e.g. from the user's scope restrictions), keyed by filter key. */
89
+ scopeSeeds?: Record<string, Array<string | number>>;
90
+ onClose: () => void;
91
+ onSave: (values: Record<string, unknown>) => void;
92
+ saving: boolean;
93
+ }): JSX.Element;
94
+
95
+ declare type AlertUnit = 'percent' | 'dollar' | 'count' | 'days';
96
+
97
+ declare type AllocateDrawerConfig = {
98
+ /** Option collection browsed in the drawer (e.g. workflow_line_items). */
99
+ collection: string;
100
+ /** Grid FK column that receives the picked option's id. */
101
+ target_field: string;
102
+ /** Grid column that receives the entered amount. */
103
+ value_field: string;
104
+ title?: string;
105
+ value_label?: string;
106
+ /** Option filter — same '$parent.<field>' token semantics as option_filter. */
107
+ filter?: Record<string, unknown>;
108
+ /** Display columns over the option rows: dotted paths resolve via nested
109
+ * field expansion; formula entries compute from the option row's values. */
110
+ columns?: Array<string | {
111
+ path?: string;
112
+ label?: string;
113
+ format?: string;
114
+ formula?: string;
115
+ width?: number;
116
+ }>;
117
+ /** Group option rows under collapsible headers by this (dotted) path —
118
+ * EFP grouped allocation lines by workflow. Groups start collapsed;
119
+ * groups containing an existing allocation start expanded. */
120
+ group_by?: string;
121
+ /** Per-row allocation ceiling formula (same tokens as column formulas incl.
122
+ * {{__saved__}} = this row's saved amount): inputs clamp to it on commit
123
+ * and show invalid state while exceeding it. */
124
+ value_max?: string;
125
+ };
126
+
127
+ declare interface AnomalyDefinition {
128
+ id: number;
129
+ key: string;
130
+ name: string;
131
+ description: string | null;
132
+ category: string;
133
+ status: string;
134
+ scope_options: FilterOptionSpec[] | null;
135
+ sensitivity_hints: Record<string, string> | null;
136
+ }
137
+
138
+ declare interface AnomalyRule {
139
+ id: number;
140
+ name: string;
141
+ definition_id: number;
142
+ sensitivity: 'low' | 'medium' | 'high' | string;
143
+ scopes: Record<string, Array<string | number>> | null;
144
+ check_frequency: 'daily' | 'weekly' | string;
145
+ delivery_in_app: boolean;
146
+ delivery_email: boolean;
147
+ status: string;
148
+ created_by: string | null;
149
+ definition?: {
150
+ id: number;
151
+ key: string;
152
+ name: string;
153
+ description: string | null;
154
+ };
155
+ }
156
+
157
+ export declare function AnomalyRuleDrawer({ open, definitions, editRule, onClose, onSave, saving }: {
158
+ open: boolean;
159
+ definitions: AnomalyDefinition[];
160
+ editRule: AnomalyRule | null;
161
+ onClose: () => void;
162
+ onSave: (values: Record<string, unknown>) => void;
163
+ saving: boolean;
164
+ }): JSX.Element;
165
+
30
166
  /**
31
167
  * "The API was redeployed since you loaded this page" bar.
32
168
  *
@@ -58,18 +194,72 @@ export declare interface ApiVersionInfo {
58
194
  environment?: string;
59
195
  }
60
196
 
197
+ /** Evaluate one field validation rule against a value. Returns an error message
198
+ * or null. Shared by useNivaroForm (headless) and ItemEditForm (admin). */
199
+ export declare function applyValidationRule(rule: FormValidationRule_2, value: unknown, label: string): string | null;
200
+
201
+ declare type AutoAllocateConfig = {
202
+ /** Button label; defaults to 'Auto allocate'. */
203
+ label?: string;
204
+ /** drawer_relations entry (by field name) whose `match` supplies the
205
+ * existing-allocation query + create defaults for this grid's rows. */
206
+ relation: string;
207
+ /** Grid row column holding the required quantity. */
208
+ row_qty_field: string;
209
+ /** Quantity column on the allocation rows. */
210
+ alloc_qty_field: string;
211
+ /** FK column on allocation rows pointing at a candidate record. */
212
+ fk_field: string;
213
+ candidates: {
214
+ collection: string;
215
+ /** Same '$parent.*' / '$row.*' token semantics as matched drawer filters. */
216
+ filters: Record<string, unknown>;
217
+ /** Server sort, e.g. 'date' for FIFO. */
218
+ sort?: string;
219
+ /** Gross capacity per candidate row, e.g. '0 - {{quantity}}'. */
220
+ capacity_formula: string;
221
+ /** Candidates with net capacity below this are skipped (default 1). */
222
+ min_capacity?: number;
223
+ };
224
+ };
225
+
61
226
  declare type BinaryOp = '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | '&&' | '||';
62
227
 
63
228
  export declare function BooleanField({ field, value, onChange, error, disabled, readOnly, inputId, errorId }: FieldComponentProps): JSX.Element;
64
229
 
230
+ export declare function buildGroups<T extends QueueGroupingRow>(rows: T[], attribute: string): QueueGroup<T>[];
231
+
65
232
  export declare function canOpenChatRoom(): boolean;
66
233
 
67
234
  export declare function canOpenDm(): boolean;
68
235
 
69
236
  export declare function canPreviewFile(type: string | null | undefined): boolean;
70
237
 
238
+ /**
239
+ * Grab the replay link for an error being reported right now. Resolves null
240
+ * when no recorder is active (both settings off, recording failed, or the
241
+ * clip upload took longer than 4s) — error reporting must never wait on or
242
+ * break because of replay capture.
243
+ */
244
+ export declare function captureErrorClip(): Promise<ErrorReplayLink | null>;
245
+
71
246
  export { CascadeFilterRule }
72
247
 
248
+ declare type CascadeRule = {
249
+ parent_field: string;
250
+ child_field: string;
251
+ };
252
+
253
+ export declare function CellCopyLayer(): ReactPortal | null;
254
+
255
+ declare interface ChannelMember {
256
+ user: string;
257
+ first_name: string | null;
258
+ last_name: string | null;
259
+ email: string | null;
260
+ joined_at: string;
261
+ }
262
+
73
263
  declare interface ChannelMeta {
74
264
  id: number;
75
265
  visibility: 'open' | 'role' | 'private';
@@ -81,6 +271,26 @@ declare interface ChannelMeta {
81
271
 
82
272
  export declare function chatAvatarColor(id: string): string;
83
273
 
274
+ /**
275
+ * Browse and join channels. The sidebar only lists rooms you belong to, so at
276
+ * hundreds of channels this is how you find the rest — the old list rendered
277
+ * every room anyone had ever posted in.
278
+ */
279
+ export declare function ChatChannelBrowser({ onOpen }: {
280
+ onOpen: (room: string, label: string, channel: ChannelMeta) => void;
281
+ }): JSX.Element;
282
+
283
+ /**
284
+ * Channel settings. Owner or admin edits name/topic/visibility, manages members
285
+ * and archives; everyone else gets the read-only summary, so a member can still
286
+ * see what kind of room they are in and who else is here.
287
+ */
288
+ export declare function ChatChannelSettings({ channel, label, onBack }: {
289
+ channel: ChannelMeta;
290
+ label: string;
291
+ onBack: () => void;
292
+ }): JSX.Element;
293
+
84
294
  export declare interface ChatConfig {
85
295
  collections: {
86
296
  messages: string;
@@ -174,7 +384,7 @@ export declare function ChatPanel({ open, onClose, renderMessageBody, requestedD
174
384
 
175
385
  export declare function ChatProvider({ children, me, onlineUsers, collections, globalRoom, globalLabel, entityPattern, entityUrl, roomLabel, recordUrl, sessionUrl, realtime, subscribeRooms, setTypingRoom, navigate, sound, theme }: ChatProviderProps): JSX.Element;
176
386
 
177
- declare interface ChatProviderProps {
387
+ export declare interface ChatProviderProps {
178
388
  children: React.ReactNode;
179
389
  me: ChatConfig['me'];
180
390
  onlineUsers?: ChatOnlineUser[];
@@ -239,6 +449,15 @@ export declare function ChatRoomView({ room, label, onBack, onOpenSettings, rend
239
449
  initialUnread?: number;
240
450
  }): JSX.Element;
241
451
 
452
+ declare interface ChatSearchHit {
453
+ id: number;
454
+ room: string;
455
+ sender: string | null;
456
+ sender_name: string | null;
457
+ message: string;
458
+ date_created: string;
459
+ }
460
+
242
461
  /**
243
462
  * Nivaro chat UI — a complete team panel (Online tab, grouped rooms, live
244
463
  * conversations with mentions, typing, receipts, search).
@@ -269,9 +488,80 @@ export declare interface ChatTheme {
269
488
  divider: string;
270
489
  }
271
490
 
491
+ declare type CheckFrequency = 'hourly' | 'daily' | 'weekly';
492
+
493
+ /**
494
+ * Select-choice display text. Legacy Directus schema imports store choice
495
+ * labels as i18n keys ('$t:published') that Directus resolved through its own
496
+ * translation bundle — Nivaro has no such bundle, so the raw key leaked into
497
+ * dropdowns and read views. Strip the marker and humanize; plain labels pass
498
+ * through untouched.
499
+ */
500
+ export declare function choiceLabel(text: string | null | undefined): string;
501
+
272
502
  /** Clear the schema cache (e.g. after a schema change). */
273
503
  export declare function clearFormSchemaCache(collection?: string): void;
274
504
 
505
+ declare interface ClientAction {
506
+ type: 'open-sidebar';
507
+ collection: string;
508
+ itemId: string;
509
+ }
510
+
511
+ export declare function CloneDialog({ collection, itemId, fields, relations, currentValues, onSuccess }: CloneDialogProps): JSX.Element;
512
+
513
+ declare interface CloneDialogProps {
514
+ collection: string;
515
+ itemId: string;
516
+ fields: CMSField[];
517
+ relations: CMSRelation[];
518
+ currentValues: Record<string, unknown>;
519
+ onSuccess: (newId: string | number) => void;
520
+ }
521
+
522
+ declare interface CMSField {
523
+ field: string;
524
+ type: string;
525
+ interface: string | null;
526
+ label: string | null;
527
+ required: boolean;
528
+ hidden: boolean;
529
+ readonly: boolean;
530
+ sort: number;
531
+ group_key: string | null;
532
+ options: Record<string, unknown> | string | null;
533
+ computed_formula: string | null;
534
+ computed_type: string | null;
535
+ note: string | null;
536
+ placeholder: string | null;
537
+ repeater_schema: Record<string, unknown>[] | string | null;
538
+ dependency_config: Record<string, unknown> | string | null;
539
+ /** {source_collection, source_fk_field?, field_map:{target:source}} — live copy from a related record when the FK changes. */
540
+ cross_record_defaults?: Record<string, unknown> | string | null;
541
+ validation_rules?: Array<{
542
+ type: string;
543
+ value?: unknown;
544
+ message?: string;
545
+ soft?: boolean;
546
+ }> | string | null;
547
+ layout_assigned?: boolean;
548
+ /** Raw layout-assignment overrides (label, readonly, drilldown, ...) — passed through by field-config. */
549
+ _overrides?: Record<string, unknown> | null;
550
+ }
551
+
552
+ declare interface CMSRelation {
553
+ id: number;
554
+ one_collection: string | null;
555
+ one_field: string | null;
556
+ many_collection: string | null;
557
+ many_field: string | null;
558
+ junction_field: string | null;
559
+ /** M2A only — the junction column naming which collection each row's `item` id
560
+ * belongs to (usually 'collection'); one_collection is null on those rows. */
561
+ one_collection_field?: string | null;
562
+ one_allowed_collections?: string | null;
563
+ }
564
+
275
565
  /** @deprecated Column model changed to plain field keys — kept for source
276
566
  * compatibility with early adopters; maps `path` to a field key. */
277
567
  export declare interface CollectionBrowserColumn {
@@ -317,7 +607,7 @@ export declare function CollectionImportPanel({ initialJobId, onJobOpen }: {
317
607
  onJobOpen?: (id: string | null) => void;
318
608
  }): JSX.Element;
319
609
 
320
- declare interface Column<T = Record<string, unknown>> {
610
+ export declare interface Column<T = Record<string, unknown>> {
321
611
  key: string;
322
612
  header: React_2.ReactNode;
323
613
  sortable?: boolean;
@@ -326,7 +616,7 @@ declare interface Column<T = Record<string, unknown>> {
326
616
  render?: (row: T, index: number) => React_2.ReactNode;
327
617
  }
328
618
 
329
- declare type ColumnFormatConfig = {
619
+ export declare type ColumnFormatConfig = {
330
620
  type: 'datetime';
331
621
  template: string;
332
622
  } | {
@@ -341,6 +631,11 @@ declare type ColumnFormatConfig = {
341
631
  false_label: string;
342
632
  };
343
633
 
634
+ declare interface ColumnPreset {
635
+ name: string;
636
+ columns: string[];
637
+ }
638
+
344
639
  export declare function CommentPanel({ collection, item, title, defaultExpanded, queuedComments, onQueueComment }: {
345
640
  collection: string;
346
641
  item: string | number;
@@ -374,7 +669,7 @@ export declare type ComponentOverrides = {
374
669
 
375
670
  export declare function DataTable<T = Record<string, unknown>>({ columns, rows, rowKey, total, page, limit, isLoading, isError, errorMessage, sort, onSortChange, onPageChange, onRowClick, searchValue, onSearchChange, searchPlaceholder, filterDefs, filterValues, onFilterChange, toolbarRight, emptyMessage, minBodyHeight, fillHeight, hideFilterRow, nowrapCells, density, pinFirstColumn, columnPins, onColumnPinChange, columnFilterRow, hScrollProxy, selectedIds, onSelectionChange, rowClassName, rowGroups, collapsedGroups, onToggleGroup }: DataTableProps<T>): React_2.JSX.Element;
376
671
 
377
- declare interface DataTableProps<T = Record<string, unknown>> {
672
+ export declare interface DataTableProps<T = Record<string, unknown>> {
378
673
  columns: Column<T>[];
379
674
  rows: T[];
380
675
  rowKey?: (row: T, i: number) => string;
@@ -451,15 +746,37 @@ export declare function DateField({ field, value, onChange, error, disabled, rea
451
746
  /** The admin's route shape — the default when the host supplies no itemUrl. */
452
747
  export declare function defaultItemUrl(t: ItemLinkTarget): string;
453
748
 
749
+ export declare function deriveGroupKey<T extends QueueGroupingRow>(row: T, attribute: string): string;
750
+
751
+ declare interface DirectoryChannel {
752
+ id: number;
753
+ key: string;
754
+ name: string;
755
+ topic: string | null;
756
+ visibility: 'open' | 'role' | 'private';
757
+ role: string | null;
758
+ joined: boolean;
759
+ members: number;
760
+ }
761
+
454
762
  declare type DmOpener = (userId: string, displayName?: string) => void;
455
763
 
456
764
  export declare function dmPeer(room: string, self: string): string | null;
457
765
 
458
766
  export declare function dmRoom(a: string, b: string): string;
459
767
 
768
+ declare type DrawerRelationConfig = string | {
769
+ field: string;
770
+ hint?: {
771
+ sum_field: string;
772
+ cap_field: string;
773
+ };
774
+ match?: MatchedDrawerConfig;
775
+ };
776
+
460
777
  export declare const DrilldownContext: React_2.Context<DrilldownContextValue | null>;
461
778
 
462
- declare interface DrilldownContextValue {
779
+ export declare interface DrilldownContextValue {
463
780
  open: (target: DrilldownTarget) => void;
464
781
  }
465
782
 
@@ -482,6 +799,18 @@ export declare function effectiveScopeSeedIds(scopes: {
482
799
  restricted: Record<string, Array<string | number>>;
483
800
  }, dimName: string): Array<string | number>;
484
801
 
802
+ export declare function ErpFailureBanner({ collection, itemId }: {
803
+ collection: string;
804
+ itemId: string;
805
+ }): JSX.Element | null;
806
+
807
+ export declare interface ErrorReplayLink {
808
+ recording_id: string;
809
+ /** Milliseconds into the recording where the error happened. Null for
810
+ * clips — the whole clip IS the error context. */
811
+ offset_ms: number | null;
812
+ }
813
+
485
814
  /** Boolean convenience — for guards and conditions. */
486
815
  export declare function evaluateBoolean(src: string, row: TokenResolver | Record<string, unknown>, opts?: EvaluateOptions): boolean;
487
816
 
@@ -491,7 +820,7 @@ export declare function evaluateExpression(src: string, resolve: TokenResolver |
491
820
  /** Numeric convenience — the shape every existing call site wants. */
492
821
  export declare function evaluateNumeric(src: string, row: TokenResolver | Record<string, unknown>, opts?: EvaluateOptions): number | null;
493
822
 
494
- declare interface EvaluateOptions {
823
+ export declare interface EvaluateOptions {
495
824
  /**
496
825
  * What an unresolvable or non-numeric token means in arithmetic.
497
826
  * 'zero' (default) matches every existing dialect. 'null' propagates, so a
@@ -500,7 +829,7 @@ declare interface EvaluateOptions {
500
829
  missing?: 'zero' | 'null';
501
830
  }
502
831
 
503
- declare type ExprNode = {
832
+ export declare type ExprNode = {
504
833
  kind: 'number';
505
834
  value: number;
506
835
  } | {
@@ -561,6 +890,18 @@ declare type ExprNode = {
561
890
  */
562
891
  export declare type ExprValue = number | string | boolean | null;
563
892
 
893
+ /**
894
+ * Item-header chip summarizing every external (ERP) request this record has
895
+ * sent — badge counts by outcome, click for the full request/response log.
896
+ * Renders nothing when the record has no submissions. Shares the
897
+ * ['erp-submissions', collection, item] cache with ErpFailureBanner, so
898
+ * transition writebacks refresh both.
899
+ */
900
+ export declare function ExternalRequestsChip({ collection, itemId }: {
901
+ collection: string;
902
+ itemId: string;
903
+ }): JSX.Element | null;
904
+
564
905
  /** Field paths an expression reads, without evaluating it. */
565
906
  export declare function extractExpressionTokens(src: string): string[];
566
907
 
@@ -596,6 +937,31 @@ export declare type FieldComponentProps = {
596
937
 
597
938
  export { FieldDependencyConfig }
598
939
 
940
+ export declare interface FieldDrilldownConfig {
941
+ enabled?: boolean;
942
+ layout_id?: number | null;
943
+ width?: number | string | null;
944
+ }
945
+
946
+ export declare function fieldDrilldownConfig(field: {
947
+ interface?: string | null;
948
+ _overrides?: Record<string, unknown> | null;
949
+ }): FieldDrilldownConfig | null;
950
+
951
+ export declare function FieldRenderer({ field, value, onChange, relations, collection, itemId, cascadeFilter, requiredParentLabel, onCountChange, displayOnly, prefillParentId }: {
952
+ field: CMSField;
953
+ value: unknown;
954
+ onChange: (v: unknown) => void;
955
+ relations: CMSRelation[];
956
+ collection: string;
957
+ itemId: string;
958
+ cascadeFilter?: Record<string, unknown>;
959
+ requiredParentLabel?: string | null;
960
+ onCountChange?: (count: number) => void;
961
+ displayOnly?: boolean;
962
+ prefillParentId?: string;
963
+ }): JSX.Element;
964
+
599
965
  export declare type FieldState = {
600
966
  value: unknown;
601
967
  error: string[] | undefined;
@@ -621,7 +987,14 @@ export declare function FilePreviewLightbox({ file, onClose }: {
621
987
  onClose: () => void;
622
988
  }): ReactPortal;
623
989
 
624
- declare interface FilterDef {
990
+ export declare function FilterControl({ def, value, onChange, layout }: {
991
+ def: FilterDef;
992
+ value: string | string[];
993
+ onChange: (value: string | string[]) => void;
994
+ layout?: 'inline' | 'stacked' | 'cell';
995
+ }): React_2.JSX.Element;
996
+
997
+ export declare interface FilterDef {
625
998
  key: string;
626
999
  placeholder: string;
627
1000
  /** Defaults to 'select' — every existing FilterDef without this field keeps its current dropdown behavior unchanged. */
@@ -643,6 +1016,35 @@ declare interface FilterDef {
643
1016
  restricted?: boolean;
644
1017
  }
645
1018
 
1019
+ export declare function filterDefLabel(def: FilterDef): string;
1020
+
1021
+ declare interface FilterOptionSpec {
1022
+ key: string;
1023
+ label?: string;
1024
+ collection?: string;
1025
+ value_field?: string;
1026
+ label_field?: string;
1027
+ sort?: string;
1028
+ }
1029
+
1030
+ /** Human display for an applied filter value — option labels for comboboxes
1031
+ * (ids → names), ≥/≤ for ranges, first two entries + count for multiselects. */
1032
+ export declare function filterValueDisplay(def: FilterDef, value: string | string[]): string;
1033
+
1034
+ export declare function formatDate(date: string | Date, opts?: Intl.DateTimeFormatOptions): string;
1035
+
1036
+ export declare function formatDateTime(date: string | Date): string;
1037
+
1038
+ export declare function formatFileSize(bytes: number | null | undefined): string;
1039
+
1040
+ export declare function formatMultiValue(raw: string, cfg: ColumnFormatConfig): string;
1041
+
1042
+ export declare function formatNumber(n: number | null | undefined): string;
1043
+
1044
+ export declare function formatRelative(date: string | Date): string;
1045
+
1046
+ export declare function formatValue(raw: string, cfg: ColumnFormatConfig): string;
1047
+
646
1048
  export declare type FormDirtyState = {
647
1049
  isDirty: boolean;
648
1050
  dirtyFields: string[];
@@ -699,7 +1101,7 @@ export declare type FormFieldType = 'text' | 'textarea' | 'integer' | 'float' |
699
1101
  export declare type FormGroupDescriptor = {
700
1102
  key: string;
701
1103
  label: string;
702
- type: 'section' | 'tab';
1104
+ type: 'section' | 'tab' | 'metadata' | 'container';
703
1105
  icon: string | null;
704
1106
  sort: number;
705
1107
  isCollapsed: boolean;
@@ -800,6 +1202,13 @@ export declare type FormValidationRule = {
800
1202
  soft?: boolean;
801
1203
  };
802
1204
 
1205
+ declare type FormValidationRule_2 = {
1206
+ type: string;
1207
+ value?: unknown;
1208
+ message?: string;
1209
+ soft?: boolean;
1210
+ };
1211
+
803
1212
  /** A client-evaluable visibility rule. */
804
1213
  export declare type FormVisibilityRule = {
805
1214
  /** key of the field this rule observes */
@@ -817,18 +1226,25 @@ export declare function getMentionQuery(text: string, cursorPos: number): string
817
1226
 
818
1227
  export declare const GridFlushContext: React_2.Context<GridFlushContextValue | null>;
819
1228
 
820
- declare type GridFlushContextValue = {
1229
+ export declare type GridFlushContextValue = {
821
1230
  register: (key: string, fn: () => Promise<void>) => void;
822
1231
  unregister: (key: string) => void;
823
1232
  };
824
1233
 
825
- declare interface HeaderWidgetInfo {
1234
+ export declare interface HeaderWidgetInfo {
826
1235
  field: string;
827
1236
  widgetId: number;
828
1237
  label: string | null;
829
1238
  inputBindings: InputBinding[];
830
1239
  }
831
1240
 
1241
+ /** Always-visible horizontal scrollbar proxy for the table scroller —
1242
+ * overlay-OS scrollbars hide, so wide tables get a persistent draggable
1243
+ * track pinned above the pagination footer. */
1244
+ export declare function HScrollProxy({ scrollerRef }: {
1245
+ scrollerRef: React_2.RefObject<HTMLDivElement | null>;
1246
+ }): React_2.JSX.Element | null;
1247
+
832
1248
  /** Time without real input before someone counts as idle. */
833
1249
  export declare const IDLE_AFTER_MS: number;
834
1250
 
@@ -878,6 +1294,23 @@ export declare interface ImportDefinition {
878
1294
  validation?: string | null;
879
1295
  }
880
1296
 
1297
+ export declare function ImportFromFileButton({ collection, templateFilter, getLabel, onParsed, compact }: {
1298
+ collection: string;
1299
+ templateFilter?: (t: ImportTemplateSummary) => boolean;
1300
+ /** Per-template label source; defaults to the template's button_label. */
1301
+ getLabel?: (t: ImportTemplateSummary) => string | null | undefined;
1302
+ onParsed: (result: ImportParseResponse, template: ImportTemplateSummary) => void;
1303
+ /** Grid-toolbar sizing (h-6, 11px) matching other inline-table buttons. */
1304
+ compact?: boolean;
1305
+ }): JSX.Element | null;
1306
+
1307
+ declare type ImportIssue = ImportParseResponse['issues'][number];
1308
+
1309
+ export declare function ImportIssuesPanel({ issues, onDismiss }: {
1310
+ issues: ImportIssue[];
1311
+ onDismiss?: () => void;
1312
+ }): JSX.Element | null;
1313
+
881
1314
  export declare interface ImportJob {
882
1315
  id: string;
883
1316
  collection: string;
@@ -1006,15 +1439,76 @@ declare interface ImportValidationReport {
1006
1439
  truncated: boolean;
1007
1440
  }
1008
1441
 
1442
+ export declare function InlineTableField({ relatedCollection, manyField, parentId, parentCollection, layoutId, showRowRevisions, allowRevisionRestore, saveMode, showLineNumbers, enableReorder, parentCascades, rowRules, columnPresets, defaultPreset, drawerRelations, parentContextFields, uniqueBy, sortField, sortDir, sectionGroupBy, rowFilter, rowDefaults, allocateDrawer, autoAllocate, rowBulkActions, uploadTemplate, submissionErrors, prefillParentId, parentFieldKey, readOnly }: {
1443
+ relatedCollection: string;
1444
+ manyField: string;
1445
+ parentId: string;
1446
+ /** Same table display, but no editing: hides the Add toolbar, + Add row,
1447
+ * row delete/undo, and blocks cell edit entry. */
1448
+ readOnly?: boolean;
1449
+ parentCollection?: string;
1450
+ layoutId?: number | null;
1451
+ showRowRevisions?: boolean;
1452
+ allowRevisionRestore?: boolean;
1453
+ saveMode?: 'immediate' | 'pending';
1454
+ showLineNumbers?: boolean;
1455
+ enableReorder?: boolean;
1456
+ parentCascades?: CascadeRule[];
1457
+ rowRules?: RowRule[];
1458
+ columnPresets?: ColumnPreset[];
1459
+ /** Initial view before the user picks one: a preset name or '__all__'. */
1460
+ defaultPreset?: string;
1461
+ drawerRelations?: DrawerRelationConfig[];
1462
+ parentContextFields?: string[];
1463
+ uniqueBy?: string[];
1464
+ sortField?: string;
1465
+ sortDir?: 'asc' | 'desc';
1466
+ /** Dotted relation path on the child collection (e.g. 'item.category.name'):
1467
+ * saved rows render grouped into collapsible sections by the resolved value. */
1468
+ sectionGroupBy?: string;
1469
+ /** Static filter narrowing which child rows this grid shows — flat {col: value}
1470
+ * entries become _eq, object values pass through as filter operators. Lets two
1471
+ * grids on the same relation show disjoint views (e.g. is_osp split). */
1472
+ rowFilter?: Record<string, unknown>;
1473
+ /** Values seeded onto every NEW row created from this grid (e.g. {is_osp: true}). */
1474
+ rowDefaults?: Record<string, unknown>;
1475
+ /** EFP-style allocate drawer: browse all eligible options, type amounts. */
1476
+ allocateDrawer?: AllocateDrawerConfig;
1477
+ autoAllocate?: AutoAllocateConfig;
1478
+ /** Toolbar buttons that rewrite EVERY row in one go from an aggregate of a
1479
+ * related collection — the generic form of EFP's "Close Out Lines" (reduce
1480
+ * each REQ line by the open unbilled amount across its PO lines). */
1481
+ rowBulkActions?: RowBulkActionConfig[];
1482
+ /** Import template NAME — renders that template's upload button in this
1483
+ * grid's toolbar (existing records; wired to ItemEditForm's reimport flow). */
1484
+ uploadTemplate?: string;
1485
+ /** Flag rows a failed ERP push rejected (options.submission_errors) — the
1486
+ * latest failed nivaro_erp_submissions row for the PARENT record is parsed
1487
+ * for "LineNumber N: reason" entries and matching rows tint red with the
1488
+ * reason beneath. `line_field` = the row column holding the pushed line
1489
+ * number (default 'line_number'). Mirrors CatalogPickerField's
1490
+ * submission_errors for catalog grids. */
1491
+ submissionErrors?: {
1492
+ line_field?: string;
1493
+ };
1494
+ prefillParentId?: string;
1495
+ parentFieldKey?: string;
1496
+ }): JSX.Element;
1497
+
1009
1498
  declare interface InputBinding {
1010
1499
  key: string;
1011
1500
  binding_type: 'item_field' | 'static' | 'url_param';
1012
1501
  binding_value: string;
1013
1502
  }
1014
1503
 
1504
+ export declare function ItemActionButtons({ collection, itemId }: {
1505
+ collection: string;
1506
+ itemId: string;
1507
+ }): JSX.Element | null;
1508
+
1015
1509
  export declare const ItemEditAuthContext: React_2.Context<ItemEditAuthContextValue>;
1016
1510
 
1017
- declare type ItemEditAuthContextValue = {
1511
+ export declare type ItemEditAuthContextValue = {
1018
1512
  isAdmin: boolean;
1019
1513
  userId: string;
1020
1514
  };
@@ -1095,6 +1589,73 @@ export declare function ItemLockBanner({ lockHolder, onTakeOver, takingOver, isA
1095
1589
  isAdmin?: boolean;
1096
1590
  }): JSX.Element | null;
1097
1591
 
1592
+ export declare function JsonMapEditor({ config, scope }: {
1593
+ config: JsonMapEditorConfig;
1594
+ scope: Record<string, unknown>;
1595
+ }): JSX.Element;
1596
+
1597
+ declare interface JsonMapEditorConfig {
1598
+ collection: string;
1599
+ title?: string;
1600
+ /** Record picker label field (also the required name on create). */
1601
+ label_field?: string;
1602
+ /** Filter for the record picker; '$scope.<field>' tokens resolve from the
1603
+ * host-provided scope. Scope values are also seeded onto created records. */
1604
+ record_filter?: Record<string, unknown>;
1605
+ /** Plain record fields edited beside the picker (small option sets). */
1606
+ fields?: Array<{
1607
+ field: string;
1608
+ label?: string;
1609
+ options: string[];
1610
+ }>;
1611
+ /** JSON-dict columns; each stored under `json_field` on the record. */
1612
+ map_columns: Array<{
1613
+ json_field: string;
1614
+ label: string;
1615
+ min?: number;
1616
+ max?: number;
1617
+ }>;
1618
+ /** Key rows: records of other collections. Template supports one-hop dotted
1619
+ * refs ('{{core_category.name}}'); filter supports '$scope' tokens. */
1620
+ sections: Array<{
1621
+ collection: string;
1622
+ label_template: string;
1623
+ filter?: Record<string, unknown>;
1624
+ section_label?: string;
1625
+ }>;
1626
+ /** Static sentinel keys appended after the sections (bg = row tint). */
1627
+ extra_rows?: Array<{
1628
+ key: string;
1629
+ label: string;
1630
+ bg?: string;
1631
+ }>;
1632
+ /** 'Left to allocate' style hint: shown when record[when_field] ===
1633
+ * when_value; remaining = total − Σ json_field values. */
1634
+ remaining_hint?: {
1635
+ when_field: string;
1636
+ when_value: string;
1637
+ total: number;
1638
+ json_field: string;
1639
+ label?: string;
1640
+ };
1641
+ /** Extra fields seeded onto created records from scope values. */
1642
+ seed_fields?: string[];
1643
+ /** Custom query run after save (recompute hooks); '$scope' tokens. */
1644
+ after_save?: {
1645
+ query_slug: string;
1646
+ params?: Record<string, unknown>;
1647
+ };
1648
+ }
1649
+
1650
+ declare interface LayoutAssignment {
1651
+ field: string;
1652
+ group_key: string | null;
1653
+ sort: number;
1654
+ label_override: string | null;
1655
+ is_visible: boolean | number;
1656
+ overrides?: string | Record<string, unknown> | null;
1657
+ }
1658
+
1098
1659
  /**
1099
1660
  * Full layout-aware form renderer.
1100
1661
  *
@@ -1153,6 +1714,13 @@ declare type LayoutFormProps = {
1153
1714
  layoutSlug?: string;
1154
1715
  };
1155
1716
 
1717
+ declare interface LayoutGroup {
1718
+ key: string;
1719
+ label: string;
1720
+ type: string | null;
1721
+ sort: number;
1722
+ }
1723
+
1156
1724
  export declare type LayoutItem = FormGroupDescriptor | '__ungrouped__';
1157
1725
 
1158
1726
  declare interface LockHolder {
@@ -1160,6 +1728,25 @@ declare interface LockHolder {
1160
1728
  locked_by_name: string | null;
1161
1729
  }
1162
1730
 
1731
+ export declare interface M2MStagingCtx {
1732
+ getStagedLinks: (key: string) => unknown[];
1733
+ getStagedUnlinks: (key: string) => Set<unknown>;
1734
+ stageLink: (key: string, relatedId: unknown) => void;
1735
+ stageUnlink: (key: string, junctionId: unknown) => void;
1736
+ unstageLink: (key: string, relatedId: unknown) => void;
1737
+ unstageUnlink: (key: string, junctionId: unknown) => void;
1738
+ }
1739
+
1740
+ declare type MatchedDrawerConfig = {
1741
+ /** Rows selected by filter instead of an FK to the child row — values may be
1742
+ * '$parent.id', '$parent.<field>', '$row.<field>', or literals; dotted keys
1743
+ * become nested relation filters. Creates are seeded with `defaults`
1744
+ * (same token resolution, plain columns only). */
1745
+ collection: string;
1746
+ filters: Record<string, unknown>;
1747
+ defaults?: Record<string, unknown>;
1748
+ };
1749
+
1163
1750
  /** Match a filter surface (by its own key and/or its options collection) to a
1164
1751
  * scope dimension. Convention: key === dimension name wins, else the filter's
1165
1752
  * options collection === the dimension's target collection. */
@@ -1168,22 +1755,147 @@ export declare function matchScopeDimension(scopes: UserScopesInfo | null, probe
1168
1755
  collection?: string;
1169
1756
  }): UserScopesInfo['dimensions'][number] | null;
1170
1757
 
1758
+ export declare function MatrixEditor({ config, initialScope }: {
1759
+ config: MatrixEditorConfig;
1760
+ /** Pre-seed scope pickers (drill-down hosts scope the matrix to a record). */
1761
+ initialScope?: Record<string, unknown>;
1762
+ }): JSX.Element;
1763
+
1764
+ declare interface MatrixEditorConfig {
1765
+ title?: string;
1766
+ target_collection: string;
1767
+ option_collection: string;
1768
+ /** Plain display column (default 'name') — or use option_label_template. */
1769
+ option_label?: string;
1770
+ /** Dotted display template, e.g. '{{core_category.name}} - {{sub_category.name}}'. */
1771
+ option_label_template?: string;
1772
+ /** Filter for the option fetch. '$scope.<field>' tokens resolve from scope. */
1773
+ option_filter?: Record<string, unknown>;
1774
+ key_field: string;
1775
+ value_field: string;
1776
+ value_label?: string;
1777
+ value_format?: 'currency' | 'number';
1778
+ /** Scope pickers. `filter` narrows a picker's options; '$scope.<field>'
1779
+ * tokens resolve from the other scope values (EFP: sub types limited to the
1780
+ * chosen project's linked sub types) — unresolved tokens drop the filter. */
1781
+ scope_fields: Array<{
1782
+ field: string;
1783
+ collection: string;
1784
+ label?: string;
1785
+ filter?: Record<string, unknown>;
1786
+ }>;
1787
+ /** Third level: sub-group options WITHIN a group section by a template over
1788
+ * the option record (EFP region → core category → Labor/Materials/Equipment:
1789
+ * label_template '{{core_category.name}}', row_label_template
1790
+ * '{{sub_category.name}}'). Sub-headers aggregate their children and
1791
+ * collapse independently. */
1792
+ option_section?: {
1793
+ label_template: string;
1794
+ /** Leaf row label; defaults to the full option label. */
1795
+ row_label_template?: string;
1796
+ };
1797
+ /** Second axis: sections per group record; target rows keyed (key, group, scope). */
1798
+ group?: {
1799
+ field: string;
1800
+ collection: string;
1801
+ label_field?: string;
1802
+ filter?: Record<string, unknown>;
1803
+ };
1804
+ /** Extra seeds on created rows, '$<scopeField>.<path>' — path may be dotted
1805
+ * and use [n] (e.g. '$project.funding_years[0].funding_year'). */
1806
+ scope_seeds?: Record<string, string>;
1807
+ /** Auto-fill a scope picker from another scope record when empty, e.g.
1808
+ * {project_sub_type: '$project.default_sub_type'} (EFP default sub type). */
1809
+ scope_defaults?: Record<string, string>;
1810
+ /** Seeds copied from the GROUP record onto rows created in that section,
1811
+ * {targetField: groupField} — e.g. {division: 'division'} (allocation rows
1812
+ * carry the region's division, not one picked from the project). */
1813
+ group_seeds?: Record<string, string>;
1814
+ /** Cap strip: cap value read from a scope record's field; total of all cells
1815
+ * is clamped so it never exceeds the cap (server sum_cap should back this). */
1816
+ cap?: {
1817
+ scope_field: string;
1818
+ field: string;
1819
+ label?: string;
1820
+ };
1821
+ /** Synthetic rows per group for target rows with a NULL key (Uncategorized)
1822
+ * and/or a boolean bucket flag (Inventory: rows with flag true, key null). */
1823
+ specials?: {
1824
+ uncategorized?: {
1825
+ label?: string;
1826
+ metric_value?: number | null;
1827
+ };
1828
+ bucket?: {
1829
+ flag_field: string;
1830
+ label?: string;
1831
+ metric_value?: number | null;
1832
+ };
1833
+ };
1834
+ /** Per-cell read-only metric columns from a custom query. */
1835
+ metrics?: {
1836
+ query_slug: string;
1837
+ params?: Record<string, unknown>;
1838
+ match_option_field: string;
1839
+ match_group_field?: string;
1840
+ columns: Array<{
1841
+ field: string;
1842
+ label?: string;
1843
+ format?: 'currency' | 'number';
1844
+ }>;
1845
+ };
1846
+ }
1847
+
1171
1848
  declare interface MessageToken {
1172
1849
  text: string;
1173
1850
  entity?: string;
1174
1851
  mention?: string;
1175
1852
  }
1176
1853
 
1854
+ declare interface MetricAlertRule {
1855
+ id: number;
1856
+ name: string;
1857
+ definition_id: number;
1858
+ operator: string;
1859
+ threshold_value: number;
1860
+ filters: Record<string, Array<string | number>> | null;
1861
+ check_frequency: CheckFrequency | string;
1862
+ is_shared: boolean;
1863
+ status: string;
1864
+ created_by: string | null;
1865
+ created_at: string | null;
1866
+ definition?: {
1867
+ id: number;
1868
+ name: string;
1869
+ description: string | null;
1870
+ category: string;
1871
+ unit: string;
1872
+ };
1873
+ }
1874
+
1875
+ declare interface MetricDefinition {
1876
+ id: number;
1877
+ name: string;
1878
+ description: string | null;
1879
+ metric_key: string;
1880
+ category: string;
1881
+ unit: AlertUnit | string;
1882
+ default_operator: AlertOperator | string;
1883
+ default_threshold: number | null;
1884
+ supported_filters: FilterOptionSpec[] | null;
1885
+ status: string;
1886
+ sort: number | null;
1887
+ }
1888
+
1177
1889
  export declare function MyWorkView({ notificationRoutes, onOpenPath }?: {
1178
1890
  /** Host page map so notification clicks land on real pages (reports, queues,
1179
1891
  * alerts…); without it only record targets are clickable. */
1180
- notificationRoutes?: NotificationRouteMap_2;
1892
+ notificationRoutes?: NotificationRouteMap;
1181
1893
  onOpenPath?: (path: string) => void;
1182
1894
  }): JSX.Element;
1183
1895
 
1184
1896
  export declare const NavigationContext: React_2.Context<NavigationContextValue>;
1185
1897
 
1186
- declare type NavigationContextValue = {
1898
+ export declare type NavigationContextValue = {
1187
1899
  /** `options.state` rides along for hosts whose router supports it (e.g. react-router's
1188
1900
  * useNavigate) — hosts that ignore the second argument still navigate correctly. */
1189
1901
  navigate: (path: string, options?: {
@@ -1271,34 +1983,7 @@ export declare function normalizeFieldType(rawType: string, iface: string | null
1271
1983
  * system collections map to their owning pages, and the pseudo-collection
1272
1984
  * `__chat__` carries a chat room key.
1273
1985
  */
1274
-
1275
1986
  export declare interface NotificationRouteMap {
1276
- /** Record route for a business collection — return null when the host has none. */
1277
- record: (collection: string, item: string) => string | null
1278
- /** Collection listing (item-less business notifications, e.g. view digests). */
1279
- list?: (collection: string) => string | null
1280
- report?: (id: string) => string | null
1281
- queue?: (id: string) => string | null
1282
- dashboard?: (id: string) => string | null
1283
- /** Alert-manager page (metric/anomaly/per-record alert notifications). */
1284
- alerts?: () => string | null
1285
- imports?: () => string | null
1286
- issues?: () => string | null
1287
- }
1288
-
1289
- /**
1290
- * Every in-app notification must land somewhere meaningful — this is the ONE
1291
- * resolver both bells, the notifications page, and My Work use to decide what
1292
- * a click does. A row that resolves to null renders as plain text (no hover,
1293
- * no cursor), which is the honest state for broadcast-style messages; it must
1294
- * never render as a link that goes nowhere.
1295
- *
1296
- * Server writers use `collection` + `item` as the target: business collections
1297
- * point at records (or the collection list when item is null), a handful of
1298
- * system collections map to their owning pages, and the pseudo-collection
1299
- * `__chat__` carries a chat room key.
1300
- */
1301
- declare interface NotificationRouteMap_2 {
1302
1987
  /** Record route for a business collection — return null when the host has none. */
1303
1988
  record: (collection: string, item: string) => string | null;
1304
1989
  /** Collection listing (item-less business notifications, e.g. view digests). */
@@ -1312,13 +1997,19 @@ declare interface NotificationRouteMap_2 {
1312
1997
  issues?: () => string | null;
1313
1998
  }
1314
1999
 
1315
- export declare type NotificationTarget =
1316
- | { type: 'path'; path: string }
1317
- | { type: 'chat'; room: string }
1318
- | null
2000
+ export declare type NotificationTarget = {
2001
+ type: 'path';
2002
+ path: string;
2003
+ } | {
2004
+ type: 'chat';
2005
+ room: string;
2006
+ } | null;
1319
2007
 
1320
2008
  export declare function NumberField({ field, value, onChange, error, disabled, readOnly, inputId, errorId }: FieldComponentProps): JSX.Element;
1321
2009
 
2010
+ /** Intl options for a numeric field, honoring its configured precision. */
2011
+ export declare function numericIntlOptions(options: unknown, format?: string): Intl.NumberFormatOptions;
2012
+
1322
2013
  declare interface O2MFieldInfo {
1323
2014
  field: string;
1324
2015
  label: string;
@@ -1375,6 +2066,13 @@ declare interface OwnerLike {
1375
2066
  name: string;
1376
2067
  }
1377
2068
 
2069
+ export declare function OwnersSlot({ collection, item, title, defaultExpanded }: {
2070
+ collection: string;
2071
+ item: string;
2072
+ title?: string;
2073
+ defaultExpanded?: boolean;
2074
+ }): JSX.Element | null;
2075
+
1378
2076
  export declare function PageRenderer({ slug, hideHeader, className }: PageRendererProps): JSX.Element;
1379
2077
 
1380
2078
  export declare interface PageRendererPage {
@@ -1413,7 +2111,7 @@ declare interface ParseFailure {
1413
2111
  position: number;
1414
2112
  }
1415
2113
 
1416
- declare type ParseResult = ParseSuccess | ParseFailure;
2114
+ export declare type ParseResult = ParseSuccess | ParseFailure;
1417
2115
 
1418
2116
  declare interface ParseSuccess {
1419
2117
  ok: true;
@@ -1428,6 +2126,14 @@ declare interface PendingTask {
1428
2126
  due_date: string;
1429
2127
  }
1430
2128
 
2129
+ declare interface PinnedMessage {
2130
+ pin_id: number;
2131
+ id: number;
2132
+ sender_name: string | null;
2133
+ message: string;
2134
+ date_created: string;
2135
+ }
2136
+
1431
2137
  export declare function PipelinePanel({ collection, item, defaultExpanded, title, showApprovalChain, onBeforeTransition, addendumPending, addendumView }: {
1432
2138
  collection: string;
1433
2139
  item: string;
@@ -1445,6 +2151,17 @@ export declare function PipelineTransitionButtons({ collection, item, onBeforeTr
1445
2151
  onBeforeTransition?: () => boolean | Promise<boolean>;
1446
2152
  }): JSX.Element | null;
1447
2153
 
2154
+ /**
2155
+ * How many decimal places a numeric field wants, from its `precision` option.
2156
+ *
2157
+ * One helper because the answer was hardcoded to 2 in five places — the field
2158
+ * input, the inline grid's cells, its formula and aggregate columns, and the
2159
+ * read-only display — so a field configured for 4 showed 4 in one of them and
2160
+ * 2 everywhere else. Out-of-range or missing values fall back rather than
2161
+ * throwing at Intl, which rejects anything outside 0–20.
2162
+ */
2163
+ export declare function precisionOf(options: unknown, fallback?: number): number;
2164
+
1448
2165
  /**
1449
2166
  * Inline attachment preview — click a file, see it, without a download round
1450
2167
  * trip. Images, PDFs, video and audio render in an overlay; anything else
@@ -1467,6 +2184,136 @@ export declare function ProfileView({ userId, className }: {
1467
2184
  className?: string;
1468
2185
  }): JSX.Element;
1469
2186
 
2187
+ export declare function QueryStatStrip({ stats, rows, effectiveParams, loading }: {
2188
+ stats: QueryWidgetStat[];
2189
+ rows: Array<Record<string, unknown>>;
2190
+ effectiveParams: Record<string, unknown>;
2191
+ loading: boolean;
2192
+ }): JSX.Element | null;
2193
+
2194
+ export declare function QueryTable({ rows, config, onRowClick, pivotYear, rowActions }: {
2195
+ rows: Array<Record<string, unknown>>;
2196
+ config?: QueryTableConfig;
2197
+ /** Makes rows clickable (hover highlight); receives the (post-grouping) row. */
2198
+ onRowClick?: (row: Record<string, unknown>) => void;
2199
+ /** Resolved pivot year (from the widget's params) — overrides config.pivot.year. */
2200
+ pivotYear?: number;
2201
+ /** Trailing action buttons per row (and on the totals row, receiving null). */
2202
+ rowActions?: Array<{
2203
+ label: string;
2204
+ onClick: (row: Record<string, unknown> | null) => void;
2205
+ }>;
2206
+ }): JSX.Element;
2207
+
2208
+ declare interface QueryTableColumn {
2209
+ /** Row key. Omit for pure formula columns. */
2210
+ field?: string;
2211
+ label?: string;
2212
+ format?: 'currency' | 'number' | 'percent' | 'text';
2213
+ /** Arithmetic over {{col}} refs, e.g. '{{carAmount}} - {{allocated}}' or
2214
+ * '{{allocated}} / {{carAmount}} * 100'. Evaluated per row (and over the
2215
+ * totals row for the footer). */
2216
+ formula?: string;
2217
+ /** Include in the totals footer (plain fields sum; formula columns re-run
2218
+ * the formula over the summed values — weighted, not averaged). */
2219
+ sum?: boolean;
2220
+ /** 'progress' renders the value with a bar underneath, filled to
2221
+ * value / progress_max (EFP PUB'd-vs-budget style). Over 100% turns red. */
2222
+ display?: 'progress';
2223
+ /** Denominator for display 'progress': a row field name or a {{col}} formula. */
2224
+ progress_max?: string;
2225
+ /** Column group label. When any column has one, the header renders two
2226
+ * rows — group cells spanning their children ('Jan' over Fcst/Act) — and
2227
+ * alternating groups get a faint band so wide grids stay scannable. */
2228
+ group?: string;
2229
+ /** Cell text color (any CSS color) — EFP forecast/actual column tinting.
2230
+ * color_dark overrides in dark mode (defaults to color). */
2231
+ color?: string;
2232
+ color_dark?: string;
2233
+ /** Second field rendered as a stacked line under the main value in the SAME
2234
+ * cell (EFP month cells: Fcst over Act — halves the column count). Toggles
2235
+ * hide individual lines; the column collapses when both lines hide. */
2236
+ stack?: string;
2237
+ stack_color?: string;
2238
+ stack_color_dark?: string;
2239
+ }
2240
+
2241
+ declare interface QueryTableConfig {
2242
+ columns?: QueryTableColumn[];
2243
+ /** Group rows by this field, summing every numeric column per group. */
2244
+ group_by?: string;
2245
+ /** Render the totals footer row. */
2246
+ totals?: boolean;
2247
+ /** Collapsible section rows: one header row per distinct value of this
2248
+ * field (per-column sums on the header), child rows listed under it.
2249
+ * Unlike group_by (which REPLACES rows with aggregates), children stay. */
2250
+ tree_group_by?: string;
2251
+ /** Sections start collapsed (default false = expanded). */
2252
+ tree_collapsed?: boolean;
2253
+ /** SECOND tree level inside each section, derived by splitting `field` on
2254
+ * its LAST `separator` (default ' - '): left side = collapsible summed
2255
+ * sub-section (category), right side = the leaf's label (Labor/Materials).
2256
+ * Rows without the separator stay directly under the section. */
2257
+ tree_sub_split?: {
2258
+ field: string;
2259
+ separator?: string;
2260
+ };
2261
+ /** Sub-sections start collapsed (default true). */
2262
+ tree_sub_collapsed?: boolean;
2263
+ /** Leaf rows strip a leading '<section name> - ' from the first column —
2264
+ * de-duplicates labels when the section field is the label's prefix. */
2265
+ tree_strip_section_prefix?: boolean;
2266
+ /** Per-row numeric format override: name of a row field holding
2267
+ * 'currency' | 'number' — lets unit-count rows sit alongside dollar rows
2268
+ * (EFP forecasting grid). Falls back to the column format. */
2269
+ row_format_field?: string;
2270
+ /** Pin the first column and the header row while the grid scrolls — wide
2271
+ * month grids keep their row labels in view. */
2272
+ sticky?: boolean;
2273
+ /** Render zeroes as an em dash — de-noises mostly-empty month grids. */
2274
+ zero_dash?: boolean;
2275
+ /** Column show/hide toggle pills above the table (EFP Actuals / Forecasts /
2276
+ * Calendar Year). A toggle turned OFF hides columns matched by exact
2277
+ * `fields` or a field-name `suffix` — unless `hide_when_on` inverts it
2278
+ * (Calendar Year ON hides the Prior/Carryover columns). */
2279
+ toggles?: Array<{
2280
+ label: string;
2281
+ fields?: string[];
2282
+ suffix?: string;
2283
+ default_on?: boolean;
2284
+ hide_when_on?: boolean;
2285
+ }>;
2286
+ /** Tint every column of this group (header + cells + totals). The sentinel
2287
+ * '$current_month' resolves to the current month's short label (Jan…Dec).
2288
+ * Pair with highlight_year_param so it only applies on the current year. */
2289
+ highlight_group?: string;
2290
+ /** Widget-layer guard: name of the effective param holding the selected
2291
+ * year — highlight_group is dropped unless it equals the current year. */
2292
+ highlight_year_param?: string;
2293
+ /** Tiny colored line-name legend under stacked-column headers, e.g.
2294
+ * ['Fcst', 'Act'] (EFP listHeaderSubLabel). */
2295
+ stack_legend?: [string, string];
2296
+ /** Pivot long rows into month columns BEFORE rendering. Rows sharing the
2297
+ * same key fields merge into one row with Jan…Dec (+ optional
2298
+ * prior/later-year buckets and a total) from `value_field`, driven by a
2299
+ * 'yyyy-MM' `date_field` and the `year` param/prop. */
2300
+ pivot?: {
2301
+ date_field: string;
2302
+ value_field: string;
2303
+ /** Fields that identify a row (everything else is dropped). */
2304
+ key_fields: string[];
2305
+ /** Which year's months to expand; other years fold into per-year columns.
2306
+ * Resolved from the widget's effective params when `year_param` set. */
2307
+ year?: number;
2308
+ year_param?: string;
2309
+ /** Add a Total column summing every pivoted cell. */
2310
+ total?: boolean;
2311
+ /** Render a Months/Quarters toggle above the table (EFP quarterly view).
2312
+ * Quarter columns sum their three months — same drill/total math. */
2313
+ quarter_toggle?: boolean;
2314
+ };
2315
+ }
2316
+
1470
2317
  export declare function QueryWidgetBody({ cfg, dateRange, entityFilters, refetchInterval, onStatus, onDrill }: {
1471
2318
  cfg: ReportQueryWidgetConfig;
1472
2319
  dateRange: ReportDateRange | null;
@@ -1483,6 +2330,42 @@ export declare function QueryWidgetBody({ cfg, dateRange, entityFilters, refetch
1483
2330
  }) => void;
1484
2331
  }): JSX.Element;
1485
2332
 
2333
+ declare interface QueryWidgetStat {
2334
+ label: string;
2335
+ /** Sum this field over the main query's rows. */
2336
+ field?: string;
2337
+ format?: 'currency' | 'number';
2338
+ /** Hover breakdown: each entry summed over the same rows. */
2339
+ details?: Array<{
2340
+ label: string;
2341
+ field: string;
2342
+ }>;
2343
+ /** Independent source: own custom query. Value = sum of value_field over its
2344
+ * rows; when label_field set, rows also list in the hover breakdown.
2345
+ * param_from copies values from the main widget's effective params
2346
+ * {statQueryParam: widgetParam}. */
2347
+ query?: {
2348
+ slug: string;
2349
+ params?: Record<string, unknown>;
2350
+ param_from?: Record<string, string>;
2351
+ value_field: string;
2352
+ label_field?: string;
2353
+ };
2354
+ /** Card tint (any CSS color, e.g. '#f9fbd1'). */
2355
+ bg?: string;
2356
+ /** Only sum rows matching these field values (equality AND) — EFP
2357
+ * forecasting stats scope to the Workflow Forecast section. */
2358
+ row_match?: Record<string, unknown>;
2359
+ /** Delta stat: value = sum(field) − sum(field_subtract); positive values
2360
+ * render with a leading '+'. */
2361
+ field_subtract?: string;
2362
+ /** EFP stat-card accent: colored top border + value (accent_dark in dark
2363
+ * mode; accent_negative when a delta goes negative). */
2364
+ accent?: string;
2365
+ accent_dark?: string;
2366
+ accent_negative?: string;
2367
+ }
2368
+
1486
2369
  export declare function QueueBulkBar({ count, states, busy, claimsEnabled, onClaim, onRelease, onTransition, onClear }: {
1487
2370
  count: number;
1488
2371
  states: Array<{
@@ -1497,6 +2380,25 @@ export declare function QueueBulkBar({ count, states, busy, claimsEnabled, onCla
1497
2380
  onClear: () => void;
1498
2381
  }): JSX.Element | null;
1499
2382
 
2383
+ export declare interface QueueGroup<T extends QueueGroupingRow = QueueGroupingRow> {
2384
+ key: string;
2385
+ rows: T[];
2386
+ breached: number;
2387
+ atRisk: number;
2388
+ }
2389
+
2390
+ declare interface QueueGroupingRow {
2391
+ collection: string;
2392
+ state: string | null;
2393
+ sla_status: 'ok' | 'warning' | 'breached' | null;
2394
+ at_risk: boolean;
2395
+ owners: {
2396
+ name: string;
2397
+ }[];
2398
+ aging_hours: number | null;
2399
+ extra?: Record<string, unknown>;
2400
+ }
2401
+
1500
2402
  declare interface QueueItemRow {
1501
2403
  collection: string;
1502
2404
  item_id: string;
@@ -1595,6 +2497,23 @@ declare interface QuickFilterDef {
1595
2497
  sort?: string;
1596
2498
  }
1597
2499
 
2500
+ /** The reaction palette — mirrored server-side; anything else is rejected. */
2501
+ export declare const REACTION_EMOJI: readonly ["👍", "✅", "👀", "🎉", "❤️", "😂"];
2502
+
2503
+ export declare interface ReadViewLayout {
2504
+ layout: {
2505
+ id: number;
2506
+ name: string;
2507
+ };
2508
+ groups: LayoutGroup[];
2509
+ assignments: LayoutAssignment[];
2510
+ }
2511
+
2512
+ export declare function RecordChatActions({ collection, itemDraft }: {
2513
+ collection: string;
2514
+ itemDraft: Record<string, unknown>;
2515
+ }): JSX.Element | null;
2516
+
1598
2517
  export declare function RecordDrilldownSheet({ collection, itemId, layoutId, rootLayoutSlug, width, title, stack: controlledStack, onPush, onPop, onClose }: {
1599
2518
  collection: string;
1600
2519
  itemId: string;
@@ -1616,11 +2535,150 @@ export declare function RecordDrilldownSheet({ collection, itemId, layoutId, roo
1616
2535
  onClose: () => void;
1617
2536
  }): JSX.Element;
1618
2537
 
2538
+ export declare function RecordGridEditor({ config }: {
2539
+ config: RecordGridEditorConfig;
2540
+ }): JSX.Element;
2541
+
2542
+ declare interface RecordGridEditorConfig {
2543
+ collection: string;
2544
+ title?: string;
2545
+ /** Scope pickers: every field must be chosen before rows load; values filter
2546
+ * loaded rows and seed created rows. `filter` supports '$scope.<field>'
2547
+ * tokens off the other scope values. */
2548
+ scope?: Array<{
2549
+ field: string;
2550
+ collection: string;
2551
+ label?: string;
2552
+ filter?: Record<string, unknown>;
2553
+ }>;
2554
+ /** Free-form identity pickers on each row (year / division / …) — part of
2555
+ * what makes a row unique; rendered as comboboxes on NEW rows, read-only
2556
+ * labels on existing ones. */
2557
+ key_columns?: Array<{
2558
+ field: string;
2559
+ label?: string;
2560
+ collection: string;
2561
+ }>;
2562
+ /** One editable row per record of this collection; existing target rows are
2563
+ * matched via fk_field. `filter` supports '$scope.<field>' tokens. */
2564
+ row_source?: {
2565
+ collection: string;
2566
+ label_field: string;
2567
+ fk_field: string;
2568
+ filter?: Record<string, unknown>;
2569
+ sort?: string;
2570
+ };
2571
+ /** Plain editable numeric columns. */
2572
+ columns?: Array<{
2573
+ field: string;
2574
+ label?: string;
2575
+ readonly?: boolean;
2576
+ }>;
2577
+ /** Generates january…december editable columns per set (field =
2578
+ * '<month><suffix>'). */
2579
+ month_sets?: Array<{
2580
+ suffix: string;
2581
+ label: string;
2582
+ }>;
2583
+ allow_add?: boolean;
2584
+ allow_delete?: boolean;
2585
+ /** Written on save as the sum of the FIRST month set's values. */
2586
+ computed_total_field?: string;
2587
+ /** Custom query run after a successful save — params support '$scope.<f>'. */
2588
+ after_save?: {
2589
+ query_slug: string;
2590
+ params?: Record<string, unknown>;
2591
+ };
2592
+ /** Nested map editors opened from toolbar buttons (EFP Manage Cost Tables) —
2593
+ * receive the current scope; enabled once `require_scope` fields are set. */
2594
+ toolbar_editors?: Array<{
2595
+ button_label: string;
2596
+ sheet_width?: number | string;
2597
+ require_scope?: string[];
2598
+ json_map: JsonMapEditorConfig;
2599
+ }>;
2600
+ }
2601
+
2602
+ export declare function RecordReadView({ collection, itemId, layoutData }: {
2603
+ collection: string;
2604
+ itemId: string;
2605
+ layoutData: ReadViewLayout;
2606
+ }): JSX.Element;
2607
+
2608
+ /**
2609
+ * "Since you last looked" — opening a saved record touches the per-user view
2610
+ * watermark and, when other people changed the record in between, renders a
2611
+ * one-line recap above the form. The server owns the session semantics (a
2612
+ * refresh within 30 minutes keeps the same baseline), the strip just shows
2613
+ * whatever the touch returned. Dismiss is per-mount — the next genuine visit
2614
+ * recomputes against the new baseline anyway.
2615
+ */
2616
+ export declare function RecordRecapStrip({ collection, itemId }: {
2617
+ collection: string;
2618
+ itemId: string;
2619
+ }): JSX.Element | null;
2620
+
2621
+ /**
2622
+ * Per-record notification subscription — a bell in the item header that lets
2623
+ * any user watch this specific record. Backed entirely by the existing
2624
+ * nivaro_notification_subscriptions engine:
2625
+ *
2626
+ * State changes only → one event_type='workflow_transition' row scoped to
2627
+ * the record via filters [{field:'id', op:'eq', value:<id>}] (filter_field
2628
+ * stays null — on transition subs it means "to_state").
2629
+ * All changes → that row PLUS an event_type='all' row scoped via
2630
+ * filter_field='id' / filter_value (the create/update/delete path reads
2631
+ * only the flat filter pair, never the filters JSON).
2632
+ *
2633
+ * Transitions never fire the items-service update hook (state mirrors are raw
2634
+ * writes), so the two rows can't double-notify.
2635
+ */
2636
+ export declare function RecordSubscribeButton({ collection, itemId, recordLabel }: {
2637
+ collection: string;
2638
+ itemId: string;
2639
+ recordLabel?: string;
2640
+ }): JSX.Element;
2641
+
1619
2642
  /** Register the host's DM opener. Returns an unregister function. */
1620
2643
  export declare function registerDmOpener(fn: DmOpener): () => void;
1621
2644
 
1622
2645
  export declare function registerRoomOpener(fn: RoomOpener): () => void;
1623
2646
 
2647
+ export declare function RelatedRecordsPanel({ collection, itemId, defaultExpanded }: {
2648
+ collection: string;
2649
+ itemId: string;
2650
+ defaultExpanded?: boolean;
2651
+ }): JSX.Element;
2652
+
2653
+ export declare function RelationCombobox({ collection, value, onChange, disabled, placeholder, extraFilter, autoSelectSingle, optionSort, requiredParent, facets, fieldKey }: {
2654
+ collection: string;
2655
+ value: unknown;
2656
+ onChange: (v: unknown) => void;
2657
+ disabled?: boolean;
2658
+ placeholder?: string;
2659
+ extraFilter?: Record<string, unknown>;
2660
+ /** When the filtered option set has EXACTLY one option and the field is
2661
+ * empty, pick it automatically (options.auto_select_single). Two or more
2662
+ * options — or zero — leave the field blank; an existing value is never
2663
+ * overridden. Re-evaluates when the effective filter changes (e.g. a
2664
+ * cascade parent narrowing the list). */
2665
+ autoSelectSingle?: boolean;
2666
+ /** Option ordering: 'column' | '-column' (server sort) | 'label' | '-label' (display-label sort). Default: label ascending. */
2667
+ optionSort?: string;
2668
+ requiredParent?: string;
2669
+ /** Field name, so staleness can be reported to the form by name. */
2670
+ fieldKey?: string;
2671
+ /** In-picker filter facets: M2O fields ON THE TARGET COLLECTION rendered as
2672
+ * small pickers inside the dropdown. Ephemeral — they only narrow the
2673
+ * option list, nothing is written to the form. */
2674
+ facets?: Array<{
2675
+ field: string;
2676
+ label?: string;
2677
+ sort?: string;
2678
+ filter?: Record<string, unknown>;
2679
+ }>;
2680
+ }): JSX.Element;
2681
+
1624
2682
  /**
1625
2683
  * Unstyled relation picker.
1626
2684
  * - m2o: single <select> of related items
@@ -1641,7 +2699,7 @@ export declare type RelationOption = {
1641
2699
  raw: Record<string, unknown>;
1642
2700
  };
1643
2701
 
1644
- declare type RenderFieldProps = {
2702
+ export declare type RenderFieldProps = {
1645
2703
  field: any;
1646
2704
  value: unknown;
1647
2705
  onChange: (v: unknown) => void;
@@ -1684,56 +2742,9 @@ export declare interface ReportViewProps {
1684
2742
  emptyState?: React.ReactNode;
1685
2743
  }
1686
2744
 
1687
- export declare function resolveNotificationTarget(
1688
- collection: string | null | undefined,
1689
- item: string | null | undefined,
1690
- routes: NotificationRouteMap
1691
- ): NotificationTarget {
1692
- const c = collection?.trim()
1693
- if (!c) return null
1694
- const i = item != null && String(item).trim() !== '' ? String(item) : null
1695
-
1696
- if (c === '__chat__') {
1697
- // Opening a room needs a registered chat dock — without one there is no
1698
- // chat surface to open, so the row stays plain.
1699
- return i && canOpenChatRoom() ? { type: 'chat', room: i } : null
1700
- }
1701
-
1702
- if (c === 'nivaro_report_defs') {
1703
- const p = i ? routes.report?.(i) : null
1704
- return p ? { type: 'path', path: p } : null
1705
- }
1706
- if (c === 'nivaro_queues') {
1707
- const p = i ? routes.queue?.(i) : null
1708
- return p ? { type: 'path', path: p } : null
1709
- }
1710
- if (c === 'nivaro_dashboards') {
1711
- const p = i ? routes.dashboard?.(i) : null
1712
- return p ? { type: 'path', path: p } : null
1713
- }
1714
- if (ALERT_COLLECTIONS.has(c)) {
1715
- const p = routes.alerts?.()
1716
- return p ? { type: 'path', path: p } : null
1717
- }
1718
- if (IMPORT_COLLECTIONS.has(c)) {
1719
- const p = routes.imports?.()
1720
- return p ? { type: 'path', path: p } : null
1721
- }
1722
- if (c === 'nivaro_issues') {
1723
- const p = routes.issues?.()
1724
- return p ? { type: 'path', path: p } : null
1725
- }
1726
- // Any other system collection has no user-facing page — plain row, never a
1727
- // /collections/nivaro_* route that would 404 or 403.
1728
- if (/^nivaro_/i.test(c) || /^directus_/i.test(c)) return null
1729
-
1730
- if (i) {
1731
- const p = routes.record(c, i)
1732
- return p ? { type: 'path', path: p } : null
1733
- }
1734
- const p = routes.list?.(c)
1735
- return p ? { type: 'path', path: p } : null
1736
- }
2745
+ export declare function resolveCollectionIcon(iconName: string | null | undefined): React.ElementType | null;
2746
+
2747
+ export declare function resolveNotificationTarget(collection: string | null | undefined, item: string | null | undefined, routes: NotificationRouteMap): NotificationTarget;
1737
2748
 
1738
2749
  export declare function RevisionsPanel({ collection, item, onRollback, triggerClassName, inlineTableFields, open, onOpenChange }: {
1739
2750
  collection: string;
@@ -1767,20 +2778,67 @@ export declare interface RoomInfo {
1767
2778
  */
1768
2779
  declare type RoomOpener = (room: string, label?: string) => void;
1769
2780
 
1770
- /** Convenience: run a resolved target (navigate or open the chat room). */
1771
- export declare function runNotificationTarget(
1772
- target: NotificationTarget,
1773
- navigate: (path: string) => void
1774
- ): boolean {
1775
- if (!target) return false
1776
- if (target.type === 'chat') {
1777
- openChatRoom(target.room)
1778
- return true
1779
- }
1780
- navigate(target.path)
1781
- return true
2781
+ export declare const ROW_HIGHLIGHT_TINTS: Record<string, {
2782
+ row: string;
2783
+ text: string;
2784
+ }>;
2785
+
2786
+ /**
2787
+ * A one-click rewrite of every row in the grid, driven by an aggregate of a
2788
+ * related collection.
2789
+ *
2790
+ * EFP's "Close Out Lines" is one instance of this shape: for each REQ line,
2791
+ * sum `open_unbilled_amount` across the PO lines pointing at it and, where the
2792
+ * line covers that remainder, subtract it. Expressed as config rather than
2793
+ * EFP code so any collection can do the same:
2794
+ *
2795
+ * {label:'Close Out Lines', relation:'po_line_items',
2796
+ * aggregate:{field:'open_unbilled_amount', op:'sum'},
2797
+ * guard:'{{amount}} >= {{__agg__}}',
2798
+ * set:{amount:'{{amount}} - {{__agg__}}'}}
2799
+ *
2800
+ * `{{__agg__}}` is the aggregate for that row (same token match-agg-column
2801
+ * uses); every other `{{field}}` reads the row's current value. Rows failing
2802
+ * the guard are left exactly as they are.
2803
+ *
2804
+ * Writes go through the SAME paths a manual cell edit uses, so a staged grid
2805
+ * (new record, addendum, save_mode 'pending') stages the change and an
2806
+ * immediate grid PATCHes it — the action never invents its own persistence.
2807
+ */
2808
+ declare interface RowBulkActionConfig {
2809
+ label: string;
2810
+ /** O2M alias on the ROW's collection pointing at the rows to aggregate. */
2811
+ relation: string;
2812
+ aggregate: {
2813
+ field: string;
2814
+ op?: 'sum' | 'count' | 'min' | 'max';
2815
+ };
2816
+ /** Optional per-row condition; rows that fail are skipped untouched. */
2817
+ guard?: string;
2818
+ /** field → formula. */
2819
+ set: Record<string, string>;
2820
+ confirm?: string;
2821
+ variant?: 'default' | 'danger';
2822
+ }
2823
+
2824
+ export declare function rowHighlightClass(color?: string | null): string | undefined;
2825
+
2826
+ export declare function rowHighlightTextClass(color?: string | null): string;
2827
+
2828
+ declare interface RowRule {
2829
+ trigger_field?: string | null;
2830
+ trigger_op?: string;
2831
+ trigger_value?: string | null;
2832
+ target_field: string;
2833
+ target_type: 'set' | 'clear' | 'relation_field';
2834
+ target_value?: string | null;
2835
+ only_if_empty?: boolean;
2836
+ sort?: number;
1782
2837
  }
1783
2838
 
2839
+ /** Convenience: run a resolved target (navigate or open the chat room). */
2840
+ export declare function runNotificationTarget(target: NotificationTarget, navigate: (path: string) => void): boolean;
2841
+
1784
2842
  export declare type SectionState = {
1785
2843
  isCollapsed: (key: string) => boolean;
1786
2844
  toggle: (key: string) => void;
@@ -1791,20 +2849,27 @@ export declare type SectionState = {
1791
2849
  export declare function SelectField({ field, value, onChange, error, disabled, readOnly, inputId, errorId }: FieldComponentProps): JSX.Element;
1792
2850
 
1793
2851
  /**
1794
- * rrweb session recorder for headless frontends.
2852
+ * rrweb session recorder for headless frontends — two modes, one hook
2853
+ * (the exact model the admin app runs; see admin/src/lib/use-session-recorder.ts).
2854
+ *
2855
+ * FULL mode (`session_recording_enabled` on the instance): events stream to
2856
+ * the server continuously.
2857
+ *
2858
+ * ERROR-CLIP mode (`error_replay_enabled`, when full recording is off):
2859
+ * rrweb records into a rolling IN-MEMORY buffer — two 30s checkout windows,
2860
+ * so 30–60s of history — and uploads NOTHING. When the host reports an error,
2861
+ * `captureErrorClip()` flushes the buffer as a short recording labelled
2862
+ * app='error-clip'; attach the returned `recording_id`/`offset_ms` to your
2863
+ * `POST /issues/client` body and the issue links straight to the replay.
1795
2864
  *
1796
2865
  * Drop `useSessionRecorder({ app: 'customer-portal' })` anywhere inside a
1797
- * NivaroProvider (or pass a client explicitly) and the app records itself —
1798
- * ONLY when the Nivaro instance has session recording enabled in Settings,
1799
- * and ONLY if the host app has `rrweb` installed (optional peer dependency;
1800
- * without it the hook warns once and no-ops).
2866
+ * NivaroProvider (or pass a client explicitly). Needs `rrweb` installed in
2867
+ * the host (optional peer dependency; without it the hook warns once and
2868
+ * no-ops). Privacy in both modes: every input masked, `nvr-no-record`
2869
+ * class blocked; in clip mode events never leave the browser except on error.
1801
2870
  *
1802
- * Privacy defaults match the admin recorder: every input masked before
1803
- * events leave the browser, elements with the `nvr-no-record` class blocked.
1804
- * Events flush every 10s (or 150 events); the server enforces size caps and
1805
- * a 7-day retention. Recordings attribute to the authenticated SDK identity
1806
- * (usually your service token's user) with the `app` label telling replays
1807
- * apart per frontend.
2871
+ * `captureErrorClip` is a module singleton so an error boundary — a class
2872
+ * component far from this hook can call it without plumbing.
1808
2873
  */
1809
2874
  export declare interface SessionRecorderOptions {
1810
2875
  /** Label shown in the admin replay list — which frontend this is. */
@@ -1870,7 +2935,9 @@ export declare function TextField({ field, value, onChange, error, disabled, rea
1870
2935
 
1871
2936
  export declare function TipLayer(): ReactPortal | null;
1872
2937
 
1873
- declare type TokenResolver = (path: string) => unknown;
2938
+ export declare function titleCase(str: string): string;
2939
+
2940
+ export declare type TokenResolver = (path: string) => unknown;
1874
2941
 
1875
2942
  /** Install the listeners. Safe to call more than once. */
1876
2943
  export declare function trackActivity(): void;
@@ -1886,8 +2953,45 @@ export declare function translateScopeValues(client: {
1886
2953
  target_collection: string;
1887
2954
  }, ids: Array<string | number>, valueField?: string): Promise<Array<string | number>>;
1888
2955
 
2956
+ /**
2957
+ * Base URL + auth for the few raw `fetch` calls (widgets, PDF blobs) that
2958
+ * can't go through `client.request`. Derives everything from the ambient
2959
+ * Nivaro client so external hosts (different origin, token auth) work the
2960
+ * same as the same-origin admin; falls back to relative '/api' + cookies
2961
+ * when no provider is mounted.
2962
+ */
2963
+ export declare function useApiFetchConfig(): {
2964
+ apiBase: string;
2965
+ authHeaders: Record<string, string>;
2966
+ credentials: RequestCredentials;
2967
+ };
2968
+
1889
2969
  export declare function useApiUpdate(): ApiVersionInfo | null;
1890
2970
 
2971
+ export declare function useChannelAdmin(channelId: number | null): {
2972
+ update: UseMutationResult<unknown, Error, {
2973
+ name?: string;
2974
+ topic?: string | null;
2975
+ visibility?: "open" | "role" | "private";
2976
+ role?: string | null;
2977
+ is_archived?: boolean;
2978
+ }, unknown>;
2979
+ addMember: UseMutationResult<unknown, Error, string, unknown>;
2980
+ removeMember: UseMutationResult<unknown, Error, string, unknown>;
2981
+ };
2982
+
2983
+ /** Browsable channels — what keeps the sidebar to joined rooms only. */
2984
+ export declare function useChannelDirectory(search: string): {
2985
+ channels: NoInfer<DirectoryChannel[]>;
2986
+ loading: boolean;
2987
+ };
2988
+
2989
+ /** Members of a channel — the owner-facing list for private channels. */
2990
+ export declare function useChannelMembers(channelId: number | null): {
2991
+ members: NoInfer<ChannelMember[]>;
2992
+ loading: boolean;
2993
+ };
2994
+
1891
2995
  export declare function useChatConfig(): ChatConfig;
1892
2996
 
1893
2997
  export declare function useChatMessages(room: string | null): {
@@ -1895,6 +2999,13 @@ export declare function useChatMessages(room: string | null): {
1895
2999
  loading: boolean;
1896
3000
  };
1897
3001
 
3002
+ /** Id + name only — /api/roles is admin-gated, so the channel picker reads the
3003
+ * chat route's own lightweight list. */
3004
+ export declare function useChatRoles(): NoInfer<{
3005
+ id: string;
3006
+ name: string;
3007
+ }[]>;
3008
+
1898
3009
  /**
1899
3010
  * The sidebar. The server decides WHICH rooms (visibility) and computes unread
1900
3011
  * in SQL against the watermark; the client only labels them. The old version
@@ -1908,8 +3019,48 @@ export declare function useChatRooms(): {
1908
3019
  loading: boolean;
1909
3020
  };
1910
3021
 
3022
+ /** Search every room in MY sidebar (server enforces visibility by
3023
+ * construction — the room set is the user's own). */
3024
+ export declare function useChatSearch(q: string): {
3025
+ hits: NoInfer<ChatSearchHit[]>;
3026
+ loading: boolean;
3027
+ };
3028
+
3029
+ export declare function useCreateChannel(): UseMutationResult<unknown, Error, {
3030
+ name: string;
3031
+ key?: string;
3032
+ topic?: string;
3033
+ visibility?: "open" | "role" | "private";
3034
+ role?: string | null;
3035
+ }, unknown>;
3036
+
3037
+ export declare function useCreateGroupDm(): UseMutationResult< {
3038
+ room: string;
3039
+ name: string;
3040
+ }, Error, {
3041
+ user_ids: string[];
3042
+ name?: string;
3043
+ }, unknown>;
3044
+
3045
+ export declare function useDeleteMessage(room: string): UseMutationResult<unknown, Error, number, unknown>;
3046
+
1911
3047
  export declare function useDrilldown(): DrilldownContextValue | null;
1912
3048
 
3049
+ export declare function useEditMessage(room: string): UseMutationResult<unknown, Error, {
3050
+ messageId: number;
3051
+ text: string;
3052
+ }, unknown>;
3053
+
3054
+ /**
3055
+ * Resolves an entity room ('wf:CR26-76773') to the record's URL, host-routed
3056
+ * via cfg.recordUrl. The room-type registry maps the prefix to a collection +
3057
+ * match field; when the match field isn't the PK the record is looked up by
3058
+ * it (readable by construction — room visibility already required record
3059
+ * read). Null for non-entity rooms, unregistered prefixes, or when the record
3060
+ * doesn't resolve.
3061
+ */
3062
+ export declare function useEntityRoomLink(room: string | null): string | null;
3063
+
1913
3064
  export declare function useFieldArray(form: UseNivaroFormReturn, field: string): FieldArrayReturn;
1914
3065
 
1915
3066
  export declare function useFieldState(form: UseNivaroFormReturn, field: string): FieldState;
@@ -2033,6 +3184,8 @@ export declare type UseNivaroFormReturn = {
2033
3184
  gridFlushersRef: default_2.MutableRefObject<Map<string, () => Promise<void>>>;
2034
3185
  };
2035
3186
 
3187
+ export declare function useOptionalNivaroClient(): NivaroClient | null;
3188
+
2036
3189
  export declare function useOrderedLayout(form: UseNivaroFormReturn): {
2037
3190
  items: LayoutItem[];
2038
3191
  hasTabs: boolean;
@@ -2099,6 +3252,21 @@ export declare function useRelationOptions(client: NivaroClient | null, relatedC
2099
3252
  error: Error | null;
2100
3253
  };
2101
3254
 
3255
+ export declare function useRoomMembership(): {
3256
+ join: UseMutationResult<unknown, Error, string, unknown>;
3257
+ leave: UseMutationResult<unknown, Error, string, unknown>;
3258
+ setMuted: UseMutationResult<unknown, Error, {
3259
+ room: string;
3260
+ muted: boolean;
3261
+ }, unknown>;
3262
+ setNotifyMode: UseMutationResult<unknown, Error, {
3263
+ room: string;
3264
+ mode: "all" | "mentions";
3265
+ }, unknown>;
3266
+ };
3267
+
3268
+ export declare function useRoomPins(room: string | null): NoInfer<PinnedMessage[]>;
3269
+
2102
3270
  /** Stacked avatar cluster (+N) with a hover/pinnable portal roster whose rows
2103
3271
  * are UserChips — the single user-display primitive for owners/user lists.
2104
3272
  * Reused by the item-header Owners chip and the collection browser. */
@@ -2122,6 +3290,13 @@ export declare function useSessionRecorder(options?: SessionRecorderOptions): vo
2122
3290
 
2123
3291
  export declare function useTabState(form: UseNivaroFormReturn): TabState;
2124
3292
 
3293
+ export declare function useTogglePin(room: string): UseMutationResult<unknown, Error, number, unknown>;
3294
+
3295
+ export declare function useToggleReaction(room: string): UseMutationResult<unknown, Error, {
3296
+ messageId: number;
3297
+ emoji: string;
3298
+ }, unknown>;
3299
+
2125
3300
  export declare function useTypingIndicator(room: string | null): {
2126
3301
  onType: () => void;
2127
3302
  clearTyping: () => void;
@@ -2130,6 +3305,17 @@ export declare function useTypingIndicator(room: string | null): {
2130
3305
 
2131
3306
  export declare function useUnreadChirp(totalUnread: number, rooms?: RoomInfo[]): void;
2132
3307
 
3308
+ /** Directory of users to add to a private channel. */
3309
+ export declare function useUserSearch(search: string, enabled: boolean): {
3310
+ users: NoInfer<{
3311
+ id: string;
3312
+ first_name: string | null;
3313
+ last_name: string | null;
3314
+ email: string | null;
3315
+ }[]>;
3316
+ loading: boolean;
3317
+ };
3318
+
2133
3319
  export declare function useWatchFields(form: UseNivaroFormReturn, fields: string[]): Record<string, unknown>;
2134
3320
 
2135
3321
  /**
@@ -2149,6 +3335,32 @@ declare interface ValidationResult {
2149
3335
  unknownTokens: string[];
2150
3336
  }
2151
3337
 
3338
+ export declare function WidgetSlot({ widgetId, inputBindings, itemDraft, itemCollection, ready, label, defaultExpanded, frameless, apiBase: apiBaseProp, compact, strip, onClientAction, onWidgetType, onContentChange }: WidgetSlotProps): JSX.Element | null;
3339
+
3340
+ declare interface WidgetSlotProps {
3341
+ widgetId: number;
3342
+ inputBindings?: InputBinding[];
3343
+ itemDraft?: Record<string, unknown>;
3344
+ itemCollection?: string;
3345
+ ready?: boolean;
3346
+ label?: string;
3347
+ defaultExpanded?: boolean;
3348
+ /** Render body only — no card border/header/collapse (host provides chrome). */
3349
+ frameless?: boolean;
3350
+ apiBase?: string;
3351
+ compact?: boolean;
3352
+ strip?: boolean;
3353
+ onClientAction?: (action: ClientAction) => void;
3354
+ onWidgetType?: (type: string) => void;
3355
+ /**
3356
+ * Reports whether the widget has anything to show (review_list: any rows;
3357
+ * other types: any successful render; errors count as content so failures
3358
+ * stay visible). Lets a host section's hide_when_empty consider widget
3359
+ * content, not just field values.
3360
+ */
3361
+ onContentChange?: (hasContent: boolean) => void;
3362
+ }
3363
+
2152
3364
  export declare function WorkflowPanel({ collection, item }: {
2153
3365
  collection: string;
2154
3366
  item: string;