@nivaro/react 0.1.66 → 0.1.67

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 +1301 -111
  2. package/dist/index.js +41382 -41171
  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,10 +194,41 @@ 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;
@@ -70,6 +237,21 @@ export declare function canPreviewFile(type: string | null | undefined): boolean
70
237
 
71
238
  export { CascadeFilterRule }
72
239
 
240
+ declare type CascadeRule = {
241
+ parent_field: string;
242
+ child_field: string;
243
+ };
244
+
245
+ export declare function CellCopyLayer(): ReactPortal | null;
246
+
247
+ declare interface ChannelMember {
248
+ user: string;
249
+ first_name: string | null;
250
+ last_name: string | null;
251
+ email: string | null;
252
+ joined_at: string;
253
+ }
254
+
73
255
  declare interface ChannelMeta {
74
256
  id: number;
75
257
  visibility: 'open' | 'role' | 'private';
@@ -81,6 +263,26 @@ declare interface ChannelMeta {
81
263
 
82
264
  export declare function chatAvatarColor(id: string): string;
83
265
 
266
+ /**
267
+ * Browse and join channels. The sidebar only lists rooms you belong to, so at
268
+ * hundreds of channels this is how you find the rest — the old list rendered
269
+ * every room anyone had ever posted in.
270
+ */
271
+ export declare function ChatChannelBrowser({ onOpen }: {
272
+ onOpen: (room: string, label: string, channel: ChannelMeta) => void;
273
+ }): JSX.Element;
274
+
275
+ /**
276
+ * Channel settings. Owner or admin edits name/topic/visibility, manages members
277
+ * and archives; everyone else gets the read-only summary, so a member can still
278
+ * see what kind of room they are in and who else is here.
279
+ */
280
+ export declare function ChatChannelSettings({ channel, label, onBack }: {
281
+ channel: ChannelMeta;
282
+ label: string;
283
+ onBack: () => void;
284
+ }): JSX.Element;
285
+
84
286
  export declare interface ChatConfig {
85
287
  collections: {
86
288
  messages: string;
@@ -174,7 +376,7 @@ export declare function ChatPanel({ open, onClose, renderMessageBody, requestedD
174
376
 
175
377
  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
378
 
177
- declare interface ChatProviderProps {
379
+ export declare interface ChatProviderProps {
178
380
  children: React.ReactNode;
179
381
  me: ChatConfig['me'];
180
382
  onlineUsers?: ChatOnlineUser[];
@@ -239,6 +441,15 @@ export declare function ChatRoomView({ room, label, onBack, onOpenSettings, rend
239
441
  initialUnread?: number;
240
442
  }): JSX.Element;
241
443
 
444
+ declare interface ChatSearchHit {
445
+ id: number;
446
+ room: string;
447
+ sender: string | null;
448
+ sender_name: string | null;
449
+ message: string;
450
+ date_created: string;
451
+ }
452
+
242
453
  /**
243
454
  * Nivaro chat UI — a complete team panel (Online tab, grouped rooms, live
244
455
  * conversations with mentions, typing, receipts, search).
@@ -269,9 +480,80 @@ export declare interface ChatTheme {
269
480
  divider: string;
270
481
  }
271
482
 
483
+ declare type CheckFrequency = 'hourly' | 'daily' | 'weekly';
484
+
485
+ /**
486
+ * Select-choice display text. Legacy Directus schema imports store choice
487
+ * labels as i18n keys ('$t:published') that Directus resolved through its own
488
+ * translation bundle — Nivaro has no such bundle, so the raw key leaked into
489
+ * dropdowns and read views. Strip the marker and humanize; plain labels pass
490
+ * through untouched.
491
+ */
492
+ export declare function choiceLabel(text: string | null | undefined): string;
493
+
272
494
  /** Clear the schema cache (e.g. after a schema change). */
273
495
  export declare function clearFormSchemaCache(collection?: string): void;
274
496
 
497
+ declare interface ClientAction {
498
+ type: 'open-sidebar';
499
+ collection: string;
500
+ itemId: string;
501
+ }
502
+
503
+ export declare function CloneDialog({ collection, itemId, fields, relations, currentValues, onSuccess }: CloneDialogProps): JSX.Element;
504
+
505
+ declare interface CloneDialogProps {
506
+ collection: string;
507
+ itemId: string;
508
+ fields: CMSField[];
509
+ relations: CMSRelation[];
510
+ currentValues: Record<string, unknown>;
511
+ onSuccess: (newId: string | number) => void;
512
+ }
513
+
514
+ declare interface CMSField {
515
+ field: string;
516
+ type: string;
517
+ interface: string | null;
518
+ label: string | null;
519
+ required: boolean;
520
+ hidden: boolean;
521
+ readonly: boolean;
522
+ sort: number;
523
+ group_key: string | null;
524
+ options: Record<string, unknown> | string | null;
525
+ computed_formula: string | null;
526
+ computed_type: string | null;
527
+ note: string | null;
528
+ placeholder: string | null;
529
+ repeater_schema: Record<string, unknown>[] | string | null;
530
+ dependency_config: Record<string, unknown> | string | null;
531
+ /** {source_collection, source_fk_field?, field_map:{target:source}} — live copy from a related record when the FK changes. */
532
+ cross_record_defaults?: Record<string, unknown> | string | null;
533
+ validation_rules?: Array<{
534
+ type: string;
535
+ value?: unknown;
536
+ message?: string;
537
+ soft?: boolean;
538
+ }> | string | null;
539
+ layout_assigned?: boolean;
540
+ /** Raw layout-assignment overrides (label, readonly, drilldown, ...) — passed through by field-config. */
541
+ _overrides?: Record<string, unknown> | null;
542
+ }
543
+
544
+ declare interface CMSRelation {
545
+ id: number;
546
+ one_collection: string | null;
547
+ one_field: string | null;
548
+ many_collection: string | null;
549
+ many_field: string | null;
550
+ junction_field: string | null;
551
+ /** M2A only — the junction column naming which collection each row's `item` id
552
+ * belongs to (usually 'collection'); one_collection is null on those rows. */
553
+ one_collection_field?: string | null;
554
+ one_allowed_collections?: string | null;
555
+ }
556
+
275
557
  /** @deprecated Column model changed to plain field keys — kept for source
276
558
  * compatibility with early adopters; maps `path` to a field key. */
277
559
  export declare interface CollectionBrowserColumn {
@@ -317,7 +599,7 @@ export declare function CollectionImportPanel({ initialJobId, onJobOpen }: {
317
599
  onJobOpen?: (id: string | null) => void;
318
600
  }): JSX.Element;
319
601
 
320
- declare interface Column<T = Record<string, unknown>> {
602
+ export declare interface Column<T = Record<string, unknown>> {
321
603
  key: string;
322
604
  header: React_2.ReactNode;
323
605
  sortable?: boolean;
@@ -326,7 +608,7 @@ declare interface Column<T = Record<string, unknown>> {
326
608
  render?: (row: T, index: number) => React_2.ReactNode;
327
609
  }
328
610
 
329
- declare type ColumnFormatConfig = {
611
+ export declare type ColumnFormatConfig = {
330
612
  type: 'datetime';
331
613
  template: string;
332
614
  } | {
@@ -341,6 +623,11 @@ declare type ColumnFormatConfig = {
341
623
  false_label: string;
342
624
  };
343
625
 
626
+ declare interface ColumnPreset {
627
+ name: string;
628
+ columns: string[];
629
+ }
630
+
344
631
  export declare function CommentPanel({ collection, item, title, defaultExpanded, queuedComments, onQueueComment }: {
345
632
  collection: string;
346
633
  item: string | number;
@@ -374,7 +661,7 @@ export declare type ComponentOverrides = {
374
661
 
375
662
  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
663
 
377
- declare interface DataTableProps<T = Record<string, unknown>> {
664
+ export declare interface DataTableProps<T = Record<string, unknown>> {
378
665
  columns: Column<T>[];
379
666
  rows: T[];
380
667
  rowKey?: (row: T, i: number) => string;
@@ -451,15 +738,37 @@ export declare function DateField({ field, value, onChange, error, disabled, rea
451
738
  /** The admin's route shape — the default when the host supplies no itemUrl. */
452
739
  export declare function defaultItemUrl(t: ItemLinkTarget): string;
453
740
 
741
+ export declare function deriveGroupKey<T extends QueueGroupingRow>(row: T, attribute: string): string;
742
+
743
+ declare interface DirectoryChannel {
744
+ id: number;
745
+ key: string;
746
+ name: string;
747
+ topic: string | null;
748
+ visibility: 'open' | 'role' | 'private';
749
+ role: string | null;
750
+ joined: boolean;
751
+ members: number;
752
+ }
753
+
454
754
  declare type DmOpener = (userId: string, displayName?: string) => void;
455
755
 
456
756
  export declare function dmPeer(room: string, self: string): string | null;
457
757
 
458
758
  export declare function dmRoom(a: string, b: string): string;
459
759
 
760
+ declare type DrawerRelationConfig = string | {
761
+ field: string;
762
+ hint?: {
763
+ sum_field: string;
764
+ cap_field: string;
765
+ };
766
+ match?: MatchedDrawerConfig;
767
+ };
768
+
460
769
  export declare const DrilldownContext: React_2.Context<DrilldownContextValue | null>;
461
770
 
462
- declare interface DrilldownContextValue {
771
+ export declare interface DrilldownContextValue {
463
772
  open: (target: DrilldownTarget) => void;
464
773
  }
465
774
 
@@ -482,6 +791,11 @@ export declare function effectiveScopeSeedIds(scopes: {
482
791
  restricted: Record<string, Array<string | number>>;
483
792
  }, dimName: string): Array<string | number>;
484
793
 
794
+ export declare function ErpFailureBanner({ collection, itemId }: {
795
+ collection: string;
796
+ itemId: string;
797
+ }): JSX.Element | null;
798
+
485
799
  /** Boolean convenience — for guards and conditions. */
486
800
  export declare function evaluateBoolean(src: string, row: TokenResolver | Record<string, unknown>, opts?: EvaluateOptions): boolean;
487
801
 
@@ -491,7 +805,7 @@ export declare function evaluateExpression(src: string, resolve: TokenResolver |
491
805
  /** Numeric convenience — the shape every existing call site wants. */
492
806
  export declare function evaluateNumeric(src: string, row: TokenResolver | Record<string, unknown>, opts?: EvaluateOptions): number | null;
493
807
 
494
- declare interface EvaluateOptions {
808
+ export declare interface EvaluateOptions {
495
809
  /**
496
810
  * What an unresolvable or non-numeric token means in arithmetic.
497
811
  * 'zero' (default) matches every existing dialect. 'null' propagates, so a
@@ -500,7 +814,7 @@ declare interface EvaluateOptions {
500
814
  missing?: 'zero' | 'null';
501
815
  }
502
816
 
503
- declare type ExprNode = {
817
+ export declare type ExprNode = {
504
818
  kind: 'number';
505
819
  value: number;
506
820
  } | {
@@ -561,6 +875,18 @@ declare type ExprNode = {
561
875
  */
562
876
  export declare type ExprValue = number | string | boolean | null;
563
877
 
878
+ /**
879
+ * Item-header chip summarizing every external (ERP) request this record has
880
+ * sent — badge counts by outcome, click for the full request/response log.
881
+ * Renders nothing when the record has no submissions. Shares the
882
+ * ['erp-submissions', collection, item] cache with ErpFailureBanner, so
883
+ * transition writebacks refresh both.
884
+ */
885
+ export declare function ExternalRequestsChip({ collection, itemId }: {
886
+ collection: string;
887
+ itemId: string;
888
+ }): JSX.Element | null;
889
+
564
890
  /** Field paths an expression reads, without evaluating it. */
565
891
  export declare function extractExpressionTokens(src: string): string[];
566
892
 
@@ -596,6 +922,31 @@ export declare type FieldComponentProps = {
596
922
 
597
923
  export { FieldDependencyConfig }
598
924
 
925
+ export declare interface FieldDrilldownConfig {
926
+ enabled?: boolean;
927
+ layout_id?: number | null;
928
+ width?: number | string | null;
929
+ }
930
+
931
+ export declare function fieldDrilldownConfig(field: {
932
+ interface?: string | null;
933
+ _overrides?: Record<string, unknown> | null;
934
+ }): FieldDrilldownConfig | null;
935
+
936
+ export declare function FieldRenderer({ field, value, onChange, relations, collection, itemId, cascadeFilter, requiredParentLabel, onCountChange, displayOnly, prefillParentId }: {
937
+ field: CMSField;
938
+ value: unknown;
939
+ onChange: (v: unknown) => void;
940
+ relations: CMSRelation[];
941
+ collection: string;
942
+ itemId: string;
943
+ cascadeFilter?: Record<string, unknown>;
944
+ requiredParentLabel?: string | null;
945
+ onCountChange?: (count: number) => void;
946
+ displayOnly?: boolean;
947
+ prefillParentId?: string;
948
+ }): JSX.Element;
949
+
599
950
  export declare type FieldState = {
600
951
  value: unknown;
601
952
  error: string[] | undefined;
@@ -621,7 +972,14 @@ export declare function FilePreviewLightbox({ file, onClose }: {
621
972
  onClose: () => void;
622
973
  }): ReactPortal;
623
974
 
624
- declare interface FilterDef {
975
+ export declare function FilterControl({ def, value, onChange, layout }: {
976
+ def: FilterDef;
977
+ value: string | string[];
978
+ onChange: (value: string | string[]) => void;
979
+ layout?: 'inline' | 'stacked' | 'cell';
980
+ }): React_2.JSX.Element;
981
+
982
+ export declare interface FilterDef {
625
983
  key: string;
626
984
  placeholder: string;
627
985
  /** Defaults to 'select' — every existing FilterDef without this field keeps its current dropdown behavior unchanged. */
@@ -643,6 +1001,35 @@ declare interface FilterDef {
643
1001
  restricted?: boolean;
644
1002
  }
645
1003
 
1004
+ export declare function filterDefLabel(def: FilterDef): string;
1005
+
1006
+ declare interface FilterOptionSpec {
1007
+ key: string;
1008
+ label?: string;
1009
+ collection?: string;
1010
+ value_field?: string;
1011
+ label_field?: string;
1012
+ sort?: string;
1013
+ }
1014
+
1015
+ /** Human display for an applied filter value — option labels for comboboxes
1016
+ * (ids → names), ≥/≤ for ranges, first two entries + count for multiselects. */
1017
+ export declare function filterValueDisplay(def: FilterDef, value: string | string[]): string;
1018
+
1019
+ export declare function formatDate(date: string | Date, opts?: Intl.DateTimeFormatOptions): string;
1020
+
1021
+ export declare function formatDateTime(date: string | Date): string;
1022
+
1023
+ export declare function formatFileSize(bytes: number | null | undefined): string;
1024
+
1025
+ export declare function formatMultiValue(raw: string, cfg: ColumnFormatConfig): string;
1026
+
1027
+ export declare function formatNumber(n: number | null | undefined): string;
1028
+
1029
+ export declare function formatRelative(date: string | Date): string;
1030
+
1031
+ export declare function formatValue(raw: string, cfg: ColumnFormatConfig): string;
1032
+
646
1033
  export declare type FormDirtyState = {
647
1034
  isDirty: boolean;
648
1035
  dirtyFields: string[];
@@ -699,7 +1086,7 @@ export declare type FormFieldType = 'text' | 'textarea' | 'integer' | 'float' |
699
1086
  export declare type FormGroupDescriptor = {
700
1087
  key: string;
701
1088
  label: string;
702
- type: 'section' | 'tab';
1089
+ type: 'section' | 'tab' | 'metadata' | 'container';
703
1090
  icon: string | null;
704
1091
  sort: number;
705
1092
  isCollapsed: boolean;
@@ -800,6 +1187,13 @@ export declare type FormValidationRule = {
800
1187
  soft?: boolean;
801
1188
  };
802
1189
 
1190
+ declare type FormValidationRule_2 = {
1191
+ type: string;
1192
+ value?: unknown;
1193
+ message?: string;
1194
+ soft?: boolean;
1195
+ };
1196
+
803
1197
  /** A client-evaluable visibility rule. */
804
1198
  export declare type FormVisibilityRule = {
805
1199
  /** key of the field this rule observes */
@@ -817,18 +1211,25 @@ export declare function getMentionQuery(text: string, cursorPos: number): string
817
1211
 
818
1212
  export declare const GridFlushContext: React_2.Context<GridFlushContextValue | null>;
819
1213
 
820
- declare type GridFlushContextValue = {
1214
+ export declare type GridFlushContextValue = {
821
1215
  register: (key: string, fn: () => Promise<void>) => void;
822
1216
  unregister: (key: string) => void;
823
1217
  };
824
1218
 
825
- declare interface HeaderWidgetInfo {
1219
+ export declare interface HeaderWidgetInfo {
826
1220
  field: string;
827
1221
  widgetId: number;
828
1222
  label: string | null;
829
1223
  inputBindings: InputBinding[];
830
1224
  }
831
1225
 
1226
+ /** Always-visible horizontal scrollbar proxy for the table scroller —
1227
+ * overlay-OS scrollbars hide, so wide tables get a persistent draggable
1228
+ * track pinned above the pagination footer. */
1229
+ export declare function HScrollProxy({ scrollerRef }: {
1230
+ scrollerRef: React_2.RefObject<HTMLDivElement | null>;
1231
+ }): React_2.JSX.Element | null;
1232
+
832
1233
  /** Time without real input before someone counts as idle. */
833
1234
  export declare const IDLE_AFTER_MS: number;
834
1235
 
@@ -878,6 +1279,23 @@ export declare interface ImportDefinition {
878
1279
  validation?: string | null;
879
1280
  }
880
1281
 
1282
+ export declare function ImportFromFileButton({ collection, templateFilter, getLabel, onParsed, compact }: {
1283
+ collection: string;
1284
+ templateFilter?: (t: ImportTemplateSummary) => boolean;
1285
+ /** Per-template label source; defaults to the template's button_label. */
1286
+ getLabel?: (t: ImportTemplateSummary) => string | null | undefined;
1287
+ onParsed: (result: ImportParseResponse, template: ImportTemplateSummary) => void;
1288
+ /** Grid-toolbar sizing (h-6, 11px) matching other inline-table buttons. */
1289
+ compact?: boolean;
1290
+ }): JSX.Element | null;
1291
+
1292
+ declare type ImportIssue = ImportParseResponse['issues'][number];
1293
+
1294
+ export declare function ImportIssuesPanel({ issues, onDismiss }: {
1295
+ issues: ImportIssue[];
1296
+ onDismiss?: () => void;
1297
+ }): JSX.Element | null;
1298
+
881
1299
  export declare interface ImportJob {
882
1300
  id: string;
883
1301
  collection: string;
@@ -1006,15 +1424,76 @@ declare interface ImportValidationReport {
1006
1424
  truncated: boolean;
1007
1425
  }
1008
1426
 
1427
+ 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 }: {
1428
+ relatedCollection: string;
1429
+ manyField: string;
1430
+ parentId: string;
1431
+ /** Same table display, but no editing: hides the Add toolbar, + Add row,
1432
+ * row delete/undo, and blocks cell edit entry. */
1433
+ readOnly?: boolean;
1434
+ parentCollection?: string;
1435
+ layoutId?: number | null;
1436
+ showRowRevisions?: boolean;
1437
+ allowRevisionRestore?: boolean;
1438
+ saveMode?: 'immediate' | 'pending';
1439
+ showLineNumbers?: boolean;
1440
+ enableReorder?: boolean;
1441
+ parentCascades?: CascadeRule[];
1442
+ rowRules?: RowRule[];
1443
+ columnPresets?: ColumnPreset[];
1444
+ /** Initial view before the user picks one: a preset name or '__all__'. */
1445
+ defaultPreset?: string;
1446
+ drawerRelations?: DrawerRelationConfig[];
1447
+ parentContextFields?: string[];
1448
+ uniqueBy?: string[];
1449
+ sortField?: string;
1450
+ sortDir?: 'asc' | 'desc';
1451
+ /** Dotted relation path on the child collection (e.g. 'item.category.name'):
1452
+ * saved rows render grouped into collapsible sections by the resolved value. */
1453
+ sectionGroupBy?: string;
1454
+ /** Static filter narrowing which child rows this grid shows — flat {col: value}
1455
+ * entries become _eq, object values pass through as filter operators. Lets two
1456
+ * grids on the same relation show disjoint views (e.g. is_osp split). */
1457
+ rowFilter?: Record<string, unknown>;
1458
+ /** Values seeded onto every NEW row created from this grid (e.g. {is_osp: true}). */
1459
+ rowDefaults?: Record<string, unknown>;
1460
+ /** EFP-style allocate drawer: browse all eligible options, type amounts. */
1461
+ allocateDrawer?: AllocateDrawerConfig;
1462
+ autoAllocate?: AutoAllocateConfig;
1463
+ /** Toolbar buttons that rewrite EVERY row in one go from an aggregate of a
1464
+ * related collection — the generic form of EFP's "Close Out Lines" (reduce
1465
+ * each REQ line by the open unbilled amount across its PO lines). */
1466
+ rowBulkActions?: RowBulkActionConfig[];
1467
+ /** Import template NAME — renders that template's upload button in this
1468
+ * grid's toolbar (existing records; wired to ItemEditForm's reimport flow). */
1469
+ uploadTemplate?: string;
1470
+ /** Flag rows a failed ERP push rejected (options.submission_errors) — the
1471
+ * latest failed nivaro_erp_submissions row for the PARENT record is parsed
1472
+ * for "LineNumber N: reason" entries and matching rows tint red with the
1473
+ * reason beneath. `line_field` = the row column holding the pushed line
1474
+ * number (default 'line_number'). Mirrors CatalogPickerField's
1475
+ * submission_errors for catalog grids. */
1476
+ submissionErrors?: {
1477
+ line_field?: string;
1478
+ };
1479
+ prefillParentId?: string;
1480
+ parentFieldKey?: string;
1481
+ }): JSX.Element;
1482
+
1009
1483
  declare interface InputBinding {
1010
1484
  key: string;
1011
1485
  binding_type: 'item_field' | 'static' | 'url_param';
1012
1486
  binding_value: string;
1013
1487
  }
1014
1488
 
1489
+ export declare function ItemActionButtons({ collection, itemId }: {
1490
+ collection: string;
1491
+ itemId: string;
1492
+ }): JSX.Element | null;
1493
+
1015
1494
  export declare const ItemEditAuthContext: React_2.Context<ItemEditAuthContextValue>;
1016
1495
 
1017
- declare type ItemEditAuthContextValue = {
1496
+ export declare type ItemEditAuthContextValue = {
1018
1497
  isAdmin: boolean;
1019
1498
  userId: string;
1020
1499
  };
@@ -1095,6 +1574,73 @@ export declare function ItemLockBanner({ lockHolder, onTakeOver, takingOver, isA
1095
1574
  isAdmin?: boolean;
1096
1575
  }): JSX.Element | null;
1097
1576
 
1577
+ export declare function JsonMapEditor({ config, scope }: {
1578
+ config: JsonMapEditorConfig;
1579
+ scope: Record<string, unknown>;
1580
+ }): JSX.Element;
1581
+
1582
+ declare interface JsonMapEditorConfig {
1583
+ collection: string;
1584
+ title?: string;
1585
+ /** Record picker label field (also the required name on create). */
1586
+ label_field?: string;
1587
+ /** Filter for the record picker; '$scope.<field>' tokens resolve from the
1588
+ * host-provided scope. Scope values are also seeded onto created records. */
1589
+ record_filter?: Record<string, unknown>;
1590
+ /** Plain record fields edited beside the picker (small option sets). */
1591
+ fields?: Array<{
1592
+ field: string;
1593
+ label?: string;
1594
+ options: string[];
1595
+ }>;
1596
+ /** JSON-dict columns; each stored under `json_field` on the record. */
1597
+ map_columns: Array<{
1598
+ json_field: string;
1599
+ label: string;
1600
+ min?: number;
1601
+ max?: number;
1602
+ }>;
1603
+ /** Key rows: records of other collections. Template supports one-hop dotted
1604
+ * refs ('{{core_category.name}}'); filter supports '$scope' tokens. */
1605
+ sections: Array<{
1606
+ collection: string;
1607
+ label_template: string;
1608
+ filter?: Record<string, unknown>;
1609
+ section_label?: string;
1610
+ }>;
1611
+ /** Static sentinel keys appended after the sections (bg = row tint). */
1612
+ extra_rows?: Array<{
1613
+ key: string;
1614
+ label: string;
1615
+ bg?: string;
1616
+ }>;
1617
+ /** 'Left to allocate' style hint: shown when record[when_field] ===
1618
+ * when_value; remaining = total − Σ json_field values. */
1619
+ remaining_hint?: {
1620
+ when_field: string;
1621
+ when_value: string;
1622
+ total: number;
1623
+ json_field: string;
1624
+ label?: string;
1625
+ };
1626
+ /** Extra fields seeded onto created records from scope values. */
1627
+ seed_fields?: string[];
1628
+ /** Custom query run after save (recompute hooks); '$scope' tokens. */
1629
+ after_save?: {
1630
+ query_slug: string;
1631
+ params?: Record<string, unknown>;
1632
+ };
1633
+ }
1634
+
1635
+ declare interface LayoutAssignment {
1636
+ field: string;
1637
+ group_key: string | null;
1638
+ sort: number;
1639
+ label_override: string | null;
1640
+ is_visible: boolean | number;
1641
+ overrides?: string | Record<string, unknown> | null;
1642
+ }
1643
+
1098
1644
  /**
1099
1645
  * Full layout-aware form renderer.
1100
1646
  *
@@ -1153,6 +1699,13 @@ declare type LayoutFormProps = {
1153
1699
  layoutSlug?: string;
1154
1700
  };
1155
1701
 
1702
+ declare interface LayoutGroup {
1703
+ key: string;
1704
+ label: string;
1705
+ type: string | null;
1706
+ sort: number;
1707
+ }
1708
+
1156
1709
  export declare type LayoutItem = FormGroupDescriptor | '__ungrouped__';
1157
1710
 
1158
1711
  declare interface LockHolder {
@@ -1160,6 +1713,25 @@ declare interface LockHolder {
1160
1713
  locked_by_name: string | null;
1161
1714
  }
1162
1715
 
1716
+ export declare interface M2MStagingCtx {
1717
+ getStagedLinks: (key: string) => unknown[];
1718
+ getStagedUnlinks: (key: string) => Set<unknown>;
1719
+ stageLink: (key: string, relatedId: unknown) => void;
1720
+ stageUnlink: (key: string, junctionId: unknown) => void;
1721
+ unstageLink: (key: string, relatedId: unknown) => void;
1722
+ unstageUnlink: (key: string, junctionId: unknown) => void;
1723
+ }
1724
+
1725
+ declare type MatchedDrawerConfig = {
1726
+ /** Rows selected by filter instead of an FK to the child row — values may be
1727
+ * '$parent.id', '$parent.<field>', '$row.<field>', or literals; dotted keys
1728
+ * become nested relation filters. Creates are seeded with `defaults`
1729
+ * (same token resolution, plain columns only). */
1730
+ collection: string;
1731
+ filters: Record<string, unknown>;
1732
+ defaults?: Record<string, unknown>;
1733
+ };
1734
+
1163
1735
  /** Match a filter surface (by its own key and/or its options collection) to a
1164
1736
  * scope dimension. Convention: key === dimension name wins, else the filter's
1165
1737
  * options collection === the dimension's target collection. */
@@ -1168,22 +1740,147 @@ export declare function matchScopeDimension(scopes: UserScopesInfo | null, probe
1168
1740
  collection?: string;
1169
1741
  }): UserScopesInfo['dimensions'][number] | null;
1170
1742
 
1743
+ export declare function MatrixEditor({ config, initialScope }: {
1744
+ config: MatrixEditorConfig;
1745
+ /** Pre-seed scope pickers (drill-down hosts scope the matrix to a record). */
1746
+ initialScope?: Record<string, unknown>;
1747
+ }): JSX.Element;
1748
+
1749
+ declare interface MatrixEditorConfig {
1750
+ title?: string;
1751
+ target_collection: string;
1752
+ option_collection: string;
1753
+ /** Plain display column (default 'name') — or use option_label_template. */
1754
+ option_label?: string;
1755
+ /** Dotted display template, e.g. '{{core_category.name}} - {{sub_category.name}}'. */
1756
+ option_label_template?: string;
1757
+ /** Filter for the option fetch. '$scope.<field>' tokens resolve from scope. */
1758
+ option_filter?: Record<string, unknown>;
1759
+ key_field: string;
1760
+ value_field: string;
1761
+ value_label?: string;
1762
+ value_format?: 'currency' | 'number';
1763
+ /** Scope pickers. `filter` narrows a picker's options; '$scope.<field>'
1764
+ * tokens resolve from the other scope values (EFP: sub types limited to the
1765
+ * chosen project's linked sub types) — unresolved tokens drop the filter. */
1766
+ scope_fields: Array<{
1767
+ field: string;
1768
+ collection: string;
1769
+ label?: string;
1770
+ filter?: Record<string, unknown>;
1771
+ }>;
1772
+ /** Third level: sub-group options WITHIN a group section by a template over
1773
+ * the option record (EFP region → core category → Labor/Materials/Equipment:
1774
+ * label_template '{{core_category.name}}', row_label_template
1775
+ * '{{sub_category.name}}'). Sub-headers aggregate their children and
1776
+ * collapse independently. */
1777
+ option_section?: {
1778
+ label_template: string;
1779
+ /** Leaf row label; defaults to the full option label. */
1780
+ row_label_template?: string;
1781
+ };
1782
+ /** Second axis: sections per group record; target rows keyed (key, group, scope). */
1783
+ group?: {
1784
+ field: string;
1785
+ collection: string;
1786
+ label_field?: string;
1787
+ filter?: Record<string, unknown>;
1788
+ };
1789
+ /** Extra seeds on created rows, '$<scopeField>.<path>' — path may be dotted
1790
+ * and use [n] (e.g. '$project.funding_years[0].funding_year'). */
1791
+ scope_seeds?: Record<string, string>;
1792
+ /** Auto-fill a scope picker from another scope record when empty, e.g.
1793
+ * {project_sub_type: '$project.default_sub_type'} (EFP default sub type). */
1794
+ scope_defaults?: Record<string, string>;
1795
+ /** Seeds copied from the GROUP record onto rows created in that section,
1796
+ * {targetField: groupField} — e.g. {division: 'division'} (allocation rows
1797
+ * carry the region's division, not one picked from the project). */
1798
+ group_seeds?: Record<string, string>;
1799
+ /** Cap strip: cap value read from a scope record's field; total of all cells
1800
+ * is clamped so it never exceeds the cap (server sum_cap should back this). */
1801
+ cap?: {
1802
+ scope_field: string;
1803
+ field: string;
1804
+ label?: string;
1805
+ };
1806
+ /** Synthetic rows per group for target rows with a NULL key (Uncategorized)
1807
+ * and/or a boolean bucket flag (Inventory: rows with flag true, key null). */
1808
+ specials?: {
1809
+ uncategorized?: {
1810
+ label?: string;
1811
+ metric_value?: number | null;
1812
+ };
1813
+ bucket?: {
1814
+ flag_field: string;
1815
+ label?: string;
1816
+ metric_value?: number | null;
1817
+ };
1818
+ };
1819
+ /** Per-cell read-only metric columns from a custom query. */
1820
+ metrics?: {
1821
+ query_slug: string;
1822
+ params?: Record<string, unknown>;
1823
+ match_option_field: string;
1824
+ match_group_field?: string;
1825
+ columns: Array<{
1826
+ field: string;
1827
+ label?: string;
1828
+ format?: 'currency' | 'number';
1829
+ }>;
1830
+ };
1831
+ }
1832
+
1171
1833
  declare interface MessageToken {
1172
1834
  text: string;
1173
1835
  entity?: string;
1174
1836
  mention?: string;
1175
1837
  }
1176
1838
 
1839
+ declare interface MetricAlertRule {
1840
+ id: number;
1841
+ name: string;
1842
+ definition_id: number;
1843
+ operator: string;
1844
+ threshold_value: number;
1845
+ filters: Record<string, Array<string | number>> | null;
1846
+ check_frequency: CheckFrequency | string;
1847
+ is_shared: boolean;
1848
+ status: string;
1849
+ created_by: string | null;
1850
+ created_at: string | null;
1851
+ definition?: {
1852
+ id: number;
1853
+ name: string;
1854
+ description: string | null;
1855
+ category: string;
1856
+ unit: string;
1857
+ };
1858
+ }
1859
+
1860
+ declare interface MetricDefinition {
1861
+ id: number;
1862
+ name: string;
1863
+ description: string | null;
1864
+ metric_key: string;
1865
+ category: string;
1866
+ unit: AlertUnit | string;
1867
+ default_operator: AlertOperator | string;
1868
+ default_threshold: number | null;
1869
+ supported_filters: FilterOptionSpec[] | null;
1870
+ status: string;
1871
+ sort: number | null;
1872
+ }
1873
+
1177
1874
  export declare function MyWorkView({ notificationRoutes, onOpenPath }?: {
1178
1875
  /** Host page map so notification clicks land on real pages (reports, queues,
1179
1876
  * alerts…); without it only record targets are clickable. */
1180
- notificationRoutes?: NotificationRouteMap_2;
1877
+ notificationRoutes?: NotificationRouteMap;
1181
1878
  onOpenPath?: (path: string) => void;
1182
1879
  }): JSX.Element;
1183
1880
 
1184
1881
  export declare const NavigationContext: React_2.Context<NavigationContextValue>;
1185
1882
 
1186
- declare type NavigationContextValue = {
1883
+ export declare type NavigationContextValue = {
1187
1884
  /** `options.state` rides along for hosts whose router supports it (e.g. react-router's
1188
1885
  * useNavigate) — hosts that ignore the second argument still navigate correctly. */
1189
1886
  navigate: (path: string, options?: {
@@ -1271,34 +1968,7 @@ export declare function normalizeFieldType(rawType: string, iface: string | null
1271
1968
  * system collections map to their owning pages, and the pseudo-collection
1272
1969
  * `__chat__` carries a chat room key.
1273
1970
  */
1274
-
1275
1971
  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
1972
  /** Record route for a business collection — return null when the host has none. */
1303
1973
  record: (collection: string, item: string) => string | null;
1304
1974
  /** Collection listing (item-less business notifications, e.g. view digests). */
@@ -1312,13 +1982,19 @@ declare interface NotificationRouteMap_2 {
1312
1982
  issues?: () => string | null;
1313
1983
  }
1314
1984
 
1315
- export declare type NotificationTarget =
1316
- | { type: 'path'; path: string }
1317
- | { type: 'chat'; room: string }
1318
- | null
1985
+ export declare type NotificationTarget = {
1986
+ type: 'path';
1987
+ path: string;
1988
+ } | {
1989
+ type: 'chat';
1990
+ room: string;
1991
+ } | null;
1319
1992
 
1320
1993
  export declare function NumberField({ field, value, onChange, error, disabled, readOnly, inputId, errorId }: FieldComponentProps): JSX.Element;
1321
1994
 
1995
+ /** Intl options for a numeric field, honoring its configured precision. */
1996
+ export declare function numericIntlOptions(options: unknown, format?: string): Intl.NumberFormatOptions;
1997
+
1322
1998
  declare interface O2MFieldInfo {
1323
1999
  field: string;
1324
2000
  label: string;
@@ -1375,6 +2051,13 @@ declare interface OwnerLike {
1375
2051
  name: string;
1376
2052
  }
1377
2053
 
2054
+ export declare function OwnersSlot({ collection, item, title, defaultExpanded }: {
2055
+ collection: string;
2056
+ item: string;
2057
+ title?: string;
2058
+ defaultExpanded?: boolean;
2059
+ }): JSX.Element | null;
2060
+
1378
2061
  export declare function PageRenderer({ slug, hideHeader, className }: PageRendererProps): JSX.Element;
1379
2062
 
1380
2063
  export declare interface PageRendererPage {
@@ -1413,7 +2096,7 @@ declare interface ParseFailure {
1413
2096
  position: number;
1414
2097
  }
1415
2098
 
1416
- declare type ParseResult = ParseSuccess | ParseFailure;
2099
+ export declare type ParseResult = ParseSuccess | ParseFailure;
1417
2100
 
1418
2101
  declare interface ParseSuccess {
1419
2102
  ok: true;
@@ -1428,6 +2111,14 @@ declare interface PendingTask {
1428
2111
  due_date: string;
1429
2112
  }
1430
2113
 
2114
+ declare interface PinnedMessage {
2115
+ pin_id: number;
2116
+ id: number;
2117
+ sender_name: string | null;
2118
+ message: string;
2119
+ date_created: string;
2120
+ }
2121
+
1431
2122
  export declare function PipelinePanel({ collection, item, defaultExpanded, title, showApprovalChain, onBeforeTransition, addendumPending, addendumView }: {
1432
2123
  collection: string;
1433
2124
  item: string;
@@ -1445,6 +2136,17 @@ export declare function PipelineTransitionButtons({ collection, item, onBeforeTr
1445
2136
  onBeforeTransition?: () => boolean | Promise<boolean>;
1446
2137
  }): JSX.Element | null;
1447
2138
 
2139
+ /**
2140
+ * How many decimal places a numeric field wants, from its `precision` option.
2141
+ *
2142
+ * One helper because the answer was hardcoded to 2 in five places — the field
2143
+ * input, the inline grid's cells, its formula and aggregate columns, and the
2144
+ * read-only display — so a field configured for 4 showed 4 in one of them and
2145
+ * 2 everywhere else. Out-of-range or missing values fall back rather than
2146
+ * throwing at Intl, which rejects anything outside 0–20.
2147
+ */
2148
+ export declare function precisionOf(options: unknown, fallback?: number): number;
2149
+
1448
2150
  /**
1449
2151
  * Inline attachment preview — click a file, see it, without a download round
1450
2152
  * trip. Images, PDFs, video and audio render in an overlay; anything else
@@ -1467,6 +2169,136 @@ export declare function ProfileView({ userId, className }: {
1467
2169
  className?: string;
1468
2170
  }): JSX.Element;
1469
2171
 
2172
+ export declare function QueryStatStrip({ stats, rows, effectiveParams, loading }: {
2173
+ stats: QueryWidgetStat[];
2174
+ rows: Array<Record<string, unknown>>;
2175
+ effectiveParams: Record<string, unknown>;
2176
+ loading: boolean;
2177
+ }): JSX.Element | null;
2178
+
2179
+ export declare function QueryTable({ rows, config, onRowClick, pivotYear, rowActions }: {
2180
+ rows: Array<Record<string, unknown>>;
2181
+ config?: QueryTableConfig;
2182
+ /** Makes rows clickable (hover highlight); receives the (post-grouping) row. */
2183
+ onRowClick?: (row: Record<string, unknown>) => void;
2184
+ /** Resolved pivot year (from the widget's params) — overrides config.pivot.year. */
2185
+ pivotYear?: number;
2186
+ /** Trailing action buttons per row (and on the totals row, receiving null). */
2187
+ rowActions?: Array<{
2188
+ label: string;
2189
+ onClick: (row: Record<string, unknown> | null) => void;
2190
+ }>;
2191
+ }): JSX.Element;
2192
+
2193
+ declare interface QueryTableColumn {
2194
+ /** Row key. Omit for pure formula columns. */
2195
+ field?: string;
2196
+ label?: string;
2197
+ format?: 'currency' | 'number' | 'percent' | 'text';
2198
+ /** Arithmetic over {{col}} refs, e.g. '{{carAmount}} - {{allocated}}' or
2199
+ * '{{allocated}} / {{carAmount}} * 100'. Evaluated per row (and over the
2200
+ * totals row for the footer). */
2201
+ formula?: string;
2202
+ /** Include in the totals footer (plain fields sum; formula columns re-run
2203
+ * the formula over the summed values — weighted, not averaged). */
2204
+ sum?: boolean;
2205
+ /** 'progress' renders the value with a bar underneath, filled to
2206
+ * value / progress_max (EFP PUB'd-vs-budget style). Over 100% turns red. */
2207
+ display?: 'progress';
2208
+ /** Denominator for display 'progress': a row field name or a {{col}} formula. */
2209
+ progress_max?: string;
2210
+ /** Column group label. When any column has one, the header renders two
2211
+ * rows — group cells spanning their children ('Jan' over Fcst/Act) — and
2212
+ * alternating groups get a faint band so wide grids stay scannable. */
2213
+ group?: string;
2214
+ /** Cell text color (any CSS color) — EFP forecast/actual column tinting.
2215
+ * color_dark overrides in dark mode (defaults to color). */
2216
+ color?: string;
2217
+ color_dark?: string;
2218
+ /** Second field rendered as a stacked line under the main value in the SAME
2219
+ * cell (EFP month cells: Fcst over Act — halves the column count). Toggles
2220
+ * hide individual lines; the column collapses when both lines hide. */
2221
+ stack?: string;
2222
+ stack_color?: string;
2223
+ stack_color_dark?: string;
2224
+ }
2225
+
2226
+ declare interface QueryTableConfig {
2227
+ columns?: QueryTableColumn[];
2228
+ /** Group rows by this field, summing every numeric column per group. */
2229
+ group_by?: string;
2230
+ /** Render the totals footer row. */
2231
+ totals?: boolean;
2232
+ /** Collapsible section rows: one header row per distinct value of this
2233
+ * field (per-column sums on the header), child rows listed under it.
2234
+ * Unlike group_by (which REPLACES rows with aggregates), children stay. */
2235
+ tree_group_by?: string;
2236
+ /** Sections start collapsed (default false = expanded). */
2237
+ tree_collapsed?: boolean;
2238
+ /** SECOND tree level inside each section, derived by splitting `field` on
2239
+ * its LAST `separator` (default ' - '): left side = collapsible summed
2240
+ * sub-section (category), right side = the leaf's label (Labor/Materials).
2241
+ * Rows without the separator stay directly under the section. */
2242
+ tree_sub_split?: {
2243
+ field: string;
2244
+ separator?: string;
2245
+ };
2246
+ /** Sub-sections start collapsed (default true). */
2247
+ tree_sub_collapsed?: boolean;
2248
+ /** Leaf rows strip a leading '<section name> - ' from the first column —
2249
+ * de-duplicates labels when the section field is the label's prefix. */
2250
+ tree_strip_section_prefix?: boolean;
2251
+ /** Per-row numeric format override: name of a row field holding
2252
+ * 'currency' | 'number' — lets unit-count rows sit alongside dollar rows
2253
+ * (EFP forecasting grid). Falls back to the column format. */
2254
+ row_format_field?: string;
2255
+ /** Pin the first column and the header row while the grid scrolls — wide
2256
+ * month grids keep their row labels in view. */
2257
+ sticky?: boolean;
2258
+ /** Render zeroes as an em dash — de-noises mostly-empty month grids. */
2259
+ zero_dash?: boolean;
2260
+ /** Column show/hide toggle pills above the table (EFP Actuals / Forecasts /
2261
+ * Calendar Year). A toggle turned OFF hides columns matched by exact
2262
+ * `fields` or a field-name `suffix` — unless `hide_when_on` inverts it
2263
+ * (Calendar Year ON hides the Prior/Carryover columns). */
2264
+ toggles?: Array<{
2265
+ label: string;
2266
+ fields?: string[];
2267
+ suffix?: string;
2268
+ default_on?: boolean;
2269
+ hide_when_on?: boolean;
2270
+ }>;
2271
+ /** Tint every column of this group (header + cells + totals). The sentinel
2272
+ * '$current_month' resolves to the current month's short label (Jan…Dec).
2273
+ * Pair with highlight_year_param so it only applies on the current year. */
2274
+ highlight_group?: string;
2275
+ /** Widget-layer guard: name of the effective param holding the selected
2276
+ * year — highlight_group is dropped unless it equals the current year. */
2277
+ highlight_year_param?: string;
2278
+ /** Tiny colored line-name legend under stacked-column headers, e.g.
2279
+ * ['Fcst', 'Act'] (EFP listHeaderSubLabel). */
2280
+ stack_legend?: [string, string];
2281
+ /** Pivot long rows into month columns BEFORE rendering. Rows sharing the
2282
+ * same key fields merge into one row with Jan…Dec (+ optional
2283
+ * prior/later-year buckets and a total) from `value_field`, driven by a
2284
+ * 'yyyy-MM' `date_field` and the `year` param/prop. */
2285
+ pivot?: {
2286
+ date_field: string;
2287
+ value_field: string;
2288
+ /** Fields that identify a row (everything else is dropped). */
2289
+ key_fields: string[];
2290
+ /** Which year's months to expand; other years fold into per-year columns.
2291
+ * Resolved from the widget's effective params when `year_param` set. */
2292
+ year?: number;
2293
+ year_param?: string;
2294
+ /** Add a Total column summing every pivoted cell. */
2295
+ total?: boolean;
2296
+ /** Render a Months/Quarters toggle above the table (EFP quarterly view).
2297
+ * Quarter columns sum their three months — same drill/total math. */
2298
+ quarter_toggle?: boolean;
2299
+ };
2300
+ }
2301
+
1470
2302
  export declare function QueryWidgetBody({ cfg, dateRange, entityFilters, refetchInterval, onStatus, onDrill }: {
1471
2303
  cfg: ReportQueryWidgetConfig;
1472
2304
  dateRange: ReportDateRange | null;
@@ -1483,6 +2315,42 @@ export declare function QueryWidgetBody({ cfg, dateRange, entityFilters, refetch
1483
2315
  }) => void;
1484
2316
  }): JSX.Element;
1485
2317
 
2318
+ declare interface QueryWidgetStat {
2319
+ label: string;
2320
+ /** Sum this field over the main query's rows. */
2321
+ field?: string;
2322
+ format?: 'currency' | 'number';
2323
+ /** Hover breakdown: each entry summed over the same rows. */
2324
+ details?: Array<{
2325
+ label: string;
2326
+ field: string;
2327
+ }>;
2328
+ /** Independent source: own custom query. Value = sum of value_field over its
2329
+ * rows; when label_field set, rows also list in the hover breakdown.
2330
+ * param_from copies values from the main widget's effective params
2331
+ * {statQueryParam: widgetParam}. */
2332
+ query?: {
2333
+ slug: string;
2334
+ params?: Record<string, unknown>;
2335
+ param_from?: Record<string, string>;
2336
+ value_field: string;
2337
+ label_field?: string;
2338
+ };
2339
+ /** Card tint (any CSS color, e.g. '#f9fbd1'). */
2340
+ bg?: string;
2341
+ /** Only sum rows matching these field values (equality AND) — EFP
2342
+ * forecasting stats scope to the Workflow Forecast section. */
2343
+ row_match?: Record<string, unknown>;
2344
+ /** Delta stat: value = sum(field) − sum(field_subtract); positive values
2345
+ * render with a leading '+'. */
2346
+ field_subtract?: string;
2347
+ /** EFP stat-card accent: colored top border + value (accent_dark in dark
2348
+ * mode; accent_negative when a delta goes negative). */
2349
+ accent?: string;
2350
+ accent_dark?: string;
2351
+ accent_negative?: string;
2352
+ }
2353
+
1486
2354
  export declare function QueueBulkBar({ count, states, busy, claimsEnabled, onClaim, onRelease, onTransition, onClear }: {
1487
2355
  count: number;
1488
2356
  states: Array<{
@@ -1497,6 +2365,25 @@ export declare function QueueBulkBar({ count, states, busy, claimsEnabled, onCla
1497
2365
  onClear: () => void;
1498
2366
  }): JSX.Element | null;
1499
2367
 
2368
+ export declare interface QueueGroup<T extends QueueGroupingRow = QueueGroupingRow> {
2369
+ key: string;
2370
+ rows: T[];
2371
+ breached: number;
2372
+ atRisk: number;
2373
+ }
2374
+
2375
+ declare interface QueueGroupingRow {
2376
+ collection: string;
2377
+ state: string | null;
2378
+ sla_status: 'ok' | 'warning' | 'breached' | null;
2379
+ at_risk: boolean;
2380
+ owners: {
2381
+ name: string;
2382
+ }[];
2383
+ aging_hours: number | null;
2384
+ extra?: Record<string, unknown>;
2385
+ }
2386
+
1500
2387
  declare interface QueueItemRow {
1501
2388
  collection: string;
1502
2389
  item_id: string;
@@ -1595,6 +2482,23 @@ declare interface QuickFilterDef {
1595
2482
  sort?: string;
1596
2483
  }
1597
2484
 
2485
+ /** The reaction palette — mirrored server-side; anything else is rejected. */
2486
+ export declare const REACTION_EMOJI: readonly ["👍", "✅", "👀", "🎉", "❤️", "😂"];
2487
+
2488
+ export declare interface ReadViewLayout {
2489
+ layout: {
2490
+ id: number;
2491
+ name: string;
2492
+ };
2493
+ groups: LayoutGroup[];
2494
+ assignments: LayoutAssignment[];
2495
+ }
2496
+
2497
+ export declare function RecordChatActions({ collection, itemDraft }: {
2498
+ collection: string;
2499
+ itemDraft: Record<string, unknown>;
2500
+ }): JSX.Element | null;
2501
+
1598
2502
  export declare function RecordDrilldownSheet({ collection, itemId, layoutId, rootLayoutSlug, width, title, stack: controlledStack, onPush, onPop, onClose }: {
1599
2503
  collection: string;
1600
2504
  itemId: string;
@@ -1616,11 +2520,150 @@ export declare function RecordDrilldownSheet({ collection, itemId, layoutId, roo
1616
2520
  onClose: () => void;
1617
2521
  }): JSX.Element;
1618
2522
 
2523
+ export declare function RecordGridEditor({ config }: {
2524
+ config: RecordGridEditorConfig;
2525
+ }): JSX.Element;
2526
+
2527
+ declare interface RecordGridEditorConfig {
2528
+ collection: string;
2529
+ title?: string;
2530
+ /** Scope pickers: every field must be chosen before rows load; values filter
2531
+ * loaded rows and seed created rows. `filter` supports '$scope.<field>'
2532
+ * tokens off the other scope values. */
2533
+ scope?: Array<{
2534
+ field: string;
2535
+ collection: string;
2536
+ label?: string;
2537
+ filter?: Record<string, unknown>;
2538
+ }>;
2539
+ /** Free-form identity pickers on each row (year / division / …) — part of
2540
+ * what makes a row unique; rendered as comboboxes on NEW rows, read-only
2541
+ * labels on existing ones. */
2542
+ key_columns?: Array<{
2543
+ field: string;
2544
+ label?: string;
2545
+ collection: string;
2546
+ }>;
2547
+ /** One editable row per record of this collection; existing target rows are
2548
+ * matched via fk_field. `filter` supports '$scope.<field>' tokens. */
2549
+ row_source?: {
2550
+ collection: string;
2551
+ label_field: string;
2552
+ fk_field: string;
2553
+ filter?: Record<string, unknown>;
2554
+ sort?: string;
2555
+ };
2556
+ /** Plain editable numeric columns. */
2557
+ columns?: Array<{
2558
+ field: string;
2559
+ label?: string;
2560
+ readonly?: boolean;
2561
+ }>;
2562
+ /** Generates january…december editable columns per set (field =
2563
+ * '<month><suffix>'). */
2564
+ month_sets?: Array<{
2565
+ suffix: string;
2566
+ label: string;
2567
+ }>;
2568
+ allow_add?: boolean;
2569
+ allow_delete?: boolean;
2570
+ /** Written on save as the sum of the FIRST month set's values. */
2571
+ computed_total_field?: string;
2572
+ /** Custom query run after a successful save — params support '$scope.<f>'. */
2573
+ after_save?: {
2574
+ query_slug: string;
2575
+ params?: Record<string, unknown>;
2576
+ };
2577
+ /** Nested map editors opened from toolbar buttons (EFP Manage Cost Tables) —
2578
+ * receive the current scope; enabled once `require_scope` fields are set. */
2579
+ toolbar_editors?: Array<{
2580
+ button_label: string;
2581
+ sheet_width?: number | string;
2582
+ require_scope?: string[];
2583
+ json_map: JsonMapEditorConfig;
2584
+ }>;
2585
+ }
2586
+
2587
+ export declare function RecordReadView({ collection, itemId, layoutData }: {
2588
+ collection: string;
2589
+ itemId: string;
2590
+ layoutData: ReadViewLayout;
2591
+ }): JSX.Element;
2592
+
2593
+ /**
2594
+ * "Since you last looked" — opening a saved record touches the per-user view
2595
+ * watermark and, when other people changed the record in between, renders a
2596
+ * one-line recap above the form. The server owns the session semantics (a
2597
+ * refresh within 30 minutes keeps the same baseline), the strip just shows
2598
+ * whatever the touch returned. Dismiss is per-mount — the next genuine visit
2599
+ * recomputes against the new baseline anyway.
2600
+ */
2601
+ export declare function RecordRecapStrip({ collection, itemId }: {
2602
+ collection: string;
2603
+ itemId: string;
2604
+ }): JSX.Element | null;
2605
+
2606
+ /**
2607
+ * Per-record notification subscription — a bell in the item header that lets
2608
+ * any user watch this specific record. Backed entirely by the existing
2609
+ * nivaro_notification_subscriptions engine:
2610
+ *
2611
+ * State changes only → one event_type='workflow_transition' row scoped to
2612
+ * the record via filters [{field:'id', op:'eq', value:<id>}] (filter_field
2613
+ * stays null — on transition subs it means "to_state").
2614
+ * All changes → that row PLUS an event_type='all' row scoped via
2615
+ * filter_field='id' / filter_value (the create/update/delete path reads
2616
+ * only the flat filter pair, never the filters JSON).
2617
+ *
2618
+ * Transitions never fire the items-service update hook (state mirrors are raw
2619
+ * writes), so the two rows can't double-notify.
2620
+ */
2621
+ export declare function RecordSubscribeButton({ collection, itemId, recordLabel }: {
2622
+ collection: string;
2623
+ itemId: string;
2624
+ recordLabel?: string;
2625
+ }): JSX.Element;
2626
+
1619
2627
  /** Register the host's DM opener. Returns an unregister function. */
1620
2628
  export declare function registerDmOpener(fn: DmOpener): () => void;
1621
2629
 
1622
2630
  export declare function registerRoomOpener(fn: RoomOpener): () => void;
1623
2631
 
2632
+ export declare function RelatedRecordsPanel({ collection, itemId, defaultExpanded }: {
2633
+ collection: string;
2634
+ itemId: string;
2635
+ defaultExpanded?: boolean;
2636
+ }): JSX.Element;
2637
+
2638
+ export declare function RelationCombobox({ collection, value, onChange, disabled, placeholder, extraFilter, autoSelectSingle, optionSort, requiredParent, facets, fieldKey }: {
2639
+ collection: string;
2640
+ value: unknown;
2641
+ onChange: (v: unknown) => void;
2642
+ disabled?: boolean;
2643
+ placeholder?: string;
2644
+ extraFilter?: Record<string, unknown>;
2645
+ /** When the filtered option set has EXACTLY one option and the field is
2646
+ * empty, pick it automatically (options.auto_select_single). Two or more
2647
+ * options — or zero — leave the field blank; an existing value is never
2648
+ * overridden. Re-evaluates when the effective filter changes (e.g. a
2649
+ * cascade parent narrowing the list). */
2650
+ autoSelectSingle?: boolean;
2651
+ /** Option ordering: 'column' | '-column' (server sort) | 'label' | '-label' (display-label sort). Default: label ascending. */
2652
+ optionSort?: string;
2653
+ requiredParent?: string;
2654
+ /** Field name, so staleness can be reported to the form by name. */
2655
+ fieldKey?: string;
2656
+ /** In-picker filter facets: M2O fields ON THE TARGET COLLECTION rendered as
2657
+ * small pickers inside the dropdown. Ephemeral — they only narrow the
2658
+ * option list, nothing is written to the form. */
2659
+ facets?: Array<{
2660
+ field: string;
2661
+ label?: string;
2662
+ sort?: string;
2663
+ filter?: Record<string, unknown>;
2664
+ }>;
2665
+ }): JSX.Element;
2666
+
1624
2667
  /**
1625
2668
  * Unstyled relation picker.
1626
2669
  * - m2o: single <select> of related items
@@ -1641,7 +2684,7 @@ export declare type RelationOption = {
1641
2684
  raw: Record<string, unknown>;
1642
2685
  };
1643
2686
 
1644
- declare type RenderFieldProps = {
2687
+ export declare type RenderFieldProps = {
1645
2688
  field: any;
1646
2689
  value: unknown;
1647
2690
  onChange: (v: unknown) => void;
@@ -1684,56 +2727,9 @@ export declare interface ReportViewProps {
1684
2727
  emptyState?: React.ReactNode;
1685
2728
  }
1686
2729
 
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
- }
2730
+ export declare function resolveCollectionIcon(iconName: string | null | undefined): React.ElementType | null;
2731
+
2732
+ export declare function resolveNotificationTarget(collection: string | null | undefined, item: string | null | undefined, routes: NotificationRouteMap): NotificationTarget;
1737
2733
 
1738
2734
  export declare function RevisionsPanel({ collection, item, onRollback, triggerClassName, inlineTableFields, open, onOpenChange }: {
1739
2735
  collection: string;
@@ -1767,20 +2763,67 @@ export declare interface RoomInfo {
1767
2763
  */
1768
2764
  declare type RoomOpener = (room: string, label?: string) => void;
1769
2765
 
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
2766
+ export declare const ROW_HIGHLIGHT_TINTS: Record<string, {
2767
+ row: string;
2768
+ text: string;
2769
+ }>;
2770
+
2771
+ /**
2772
+ * A one-click rewrite of every row in the grid, driven by an aggregate of a
2773
+ * related collection.
2774
+ *
2775
+ * EFP's "Close Out Lines" is one instance of this shape: for each REQ line,
2776
+ * sum `open_unbilled_amount` across the PO lines pointing at it and, where the
2777
+ * line covers that remainder, subtract it. Expressed as config rather than
2778
+ * EFP code so any collection can do the same:
2779
+ *
2780
+ * {label:'Close Out Lines', relation:'po_line_items',
2781
+ * aggregate:{field:'open_unbilled_amount', op:'sum'},
2782
+ * guard:'{{amount}} >= {{__agg__}}',
2783
+ * set:{amount:'{{amount}} - {{__agg__}}'}}
2784
+ *
2785
+ * `{{__agg__}}` is the aggregate for that row (same token match-agg-column
2786
+ * uses); every other `{{field}}` reads the row's current value. Rows failing
2787
+ * the guard are left exactly as they are.
2788
+ *
2789
+ * Writes go through the SAME paths a manual cell edit uses, so a staged grid
2790
+ * (new record, addendum, save_mode 'pending') stages the change and an
2791
+ * immediate grid PATCHes it — the action never invents its own persistence.
2792
+ */
2793
+ declare interface RowBulkActionConfig {
2794
+ label: string;
2795
+ /** O2M alias on the ROW's collection pointing at the rows to aggregate. */
2796
+ relation: string;
2797
+ aggregate: {
2798
+ field: string;
2799
+ op?: 'sum' | 'count' | 'min' | 'max';
2800
+ };
2801
+ /** Optional per-row condition; rows that fail are skipped untouched. */
2802
+ guard?: string;
2803
+ /** field → formula. */
2804
+ set: Record<string, string>;
2805
+ confirm?: string;
2806
+ variant?: 'default' | 'danger';
2807
+ }
2808
+
2809
+ export declare function rowHighlightClass(color?: string | null): string | undefined;
2810
+
2811
+ export declare function rowHighlightTextClass(color?: string | null): string;
2812
+
2813
+ declare interface RowRule {
2814
+ trigger_field?: string | null;
2815
+ trigger_op?: string;
2816
+ trigger_value?: string | null;
2817
+ target_field: string;
2818
+ target_type: 'set' | 'clear' | 'relation_field';
2819
+ target_value?: string | null;
2820
+ only_if_empty?: boolean;
2821
+ sort?: number;
1782
2822
  }
1783
2823
 
2824
+ /** Convenience: run a resolved target (navigate or open the chat room). */
2825
+ export declare function runNotificationTarget(target: NotificationTarget, navigate: (path: string) => void): boolean;
2826
+
1784
2827
  export declare type SectionState = {
1785
2828
  isCollapsed: (key: string) => boolean;
1786
2829
  toggle: (key: string) => void;
@@ -1870,7 +2913,9 @@ export declare function TextField({ field, value, onChange, error, disabled, rea
1870
2913
 
1871
2914
  export declare function TipLayer(): ReactPortal | null;
1872
2915
 
1873
- declare type TokenResolver = (path: string) => unknown;
2916
+ export declare function titleCase(str: string): string;
2917
+
2918
+ export declare type TokenResolver = (path: string) => unknown;
1874
2919
 
1875
2920
  /** Install the listeners. Safe to call more than once. */
1876
2921
  export declare function trackActivity(): void;
@@ -1886,8 +2931,45 @@ export declare function translateScopeValues(client: {
1886
2931
  target_collection: string;
1887
2932
  }, ids: Array<string | number>, valueField?: string): Promise<Array<string | number>>;
1888
2933
 
2934
+ /**
2935
+ * Base URL + auth for the few raw `fetch` calls (widgets, PDF blobs) that
2936
+ * can't go through `client.request`. Derives everything from the ambient
2937
+ * Nivaro client so external hosts (different origin, token auth) work the
2938
+ * same as the same-origin admin; falls back to relative '/api' + cookies
2939
+ * when no provider is mounted.
2940
+ */
2941
+ export declare function useApiFetchConfig(): {
2942
+ apiBase: string;
2943
+ authHeaders: Record<string, string>;
2944
+ credentials: RequestCredentials;
2945
+ };
2946
+
1889
2947
  export declare function useApiUpdate(): ApiVersionInfo | null;
1890
2948
 
2949
+ export declare function useChannelAdmin(channelId: number | null): {
2950
+ update: UseMutationResult<unknown, Error, {
2951
+ name?: string;
2952
+ topic?: string | null;
2953
+ visibility?: "open" | "role" | "private";
2954
+ role?: string | null;
2955
+ is_archived?: boolean;
2956
+ }, unknown>;
2957
+ addMember: UseMutationResult<unknown, Error, string, unknown>;
2958
+ removeMember: UseMutationResult<unknown, Error, string, unknown>;
2959
+ };
2960
+
2961
+ /** Browsable channels — what keeps the sidebar to joined rooms only. */
2962
+ export declare function useChannelDirectory(search: string): {
2963
+ channels: NoInfer<DirectoryChannel[]>;
2964
+ loading: boolean;
2965
+ };
2966
+
2967
+ /** Members of a channel — the owner-facing list for private channels. */
2968
+ export declare function useChannelMembers(channelId: number | null): {
2969
+ members: NoInfer<ChannelMember[]>;
2970
+ loading: boolean;
2971
+ };
2972
+
1891
2973
  export declare function useChatConfig(): ChatConfig;
1892
2974
 
1893
2975
  export declare function useChatMessages(room: string | null): {
@@ -1895,6 +2977,13 @@ export declare function useChatMessages(room: string | null): {
1895
2977
  loading: boolean;
1896
2978
  };
1897
2979
 
2980
+ /** Id + name only — /api/roles is admin-gated, so the channel picker reads the
2981
+ * chat route's own lightweight list. */
2982
+ export declare function useChatRoles(): NoInfer<{
2983
+ id: string;
2984
+ name: string;
2985
+ }[]>;
2986
+
1898
2987
  /**
1899
2988
  * The sidebar. The server decides WHICH rooms (visibility) and computes unread
1900
2989
  * in SQL against the watermark; the client only labels them. The old version
@@ -1908,8 +2997,48 @@ export declare function useChatRooms(): {
1908
2997
  loading: boolean;
1909
2998
  };
1910
2999
 
3000
+ /** Search every room in MY sidebar (server enforces visibility by
3001
+ * construction — the room set is the user's own). */
3002
+ export declare function useChatSearch(q: string): {
3003
+ hits: NoInfer<ChatSearchHit[]>;
3004
+ loading: boolean;
3005
+ };
3006
+
3007
+ export declare function useCreateChannel(): UseMutationResult<unknown, Error, {
3008
+ name: string;
3009
+ key?: string;
3010
+ topic?: string;
3011
+ visibility?: "open" | "role" | "private";
3012
+ role?: string | null;
3013
+ }, unknown>;
3014
+
3015
+ export declare function useCreateGroupDm(): UseMutationResult< {
3016
+ room: string;
3017
+ name: string;
3018
+ }, Error, {
3019
+ user_ids: string[];
3020
+ name?: string;
3021
+ }, unknown>;
3022
+
3023
+ export declare function useDeleteMessage(room: string): UseMutationResult<unknown, Error, number, unknown>;
3024
+
1911
3025
  export declare function useDrilldown(): DrilldownContextValue | null;
1912
3026
 
3027
+ export declare function useEditMessage(room: string): UseMutationResult<unknown, Error, {
3028
+ messageId: number;
3029
+ text: string;
3030
+ }, unknown>;
3031
+
3032
+ /**
3033
+ * Resolves an entity room ('wf:CR26-76773') to the record's URL, host-routed
3034
+ * via cfg.recordUrl. The room-type registry maps the prefix to a collection +
3035
+ * match field; when the match field isn't the PK the record is looked up by
3036
+ * it (readable by construction — room visibility already required record
3037
+ * read). Null for non-entity rooms, unregistered prefixes, or when the record
3038
+ * doesn't resolve.
3039
+ */
3040
+ export declare function useEntityRoomLink(room: string | null): string | null;
3041
+
1913
3042
  export declare function useFieldArray(form: UseNivaroFormReturn, field: string): FieldArrayReturn;
1914
3043
 
1915
3044
  export declare function useFieldState(form: UseNivaroFormReturn, field: string): FieldState;
@@ -2033,6 +3162,8 @@ export declare type UseNivaroFormReturn = {
2033
3162
  gridFlushersRef: default_2.MutableRefObject<Map<string, () => Promise<void>>>;
2034
3163
  };
2035
3164
 
3165
+ export declare function useOptionalNivaroClient(): NivaroClient | null;
3166
+
2036
3167
  export declare function useOrderedLayout(form: UseNivaroFormReturn): {
2037
3168
  items: LayoutItem[];
2038
3169
  hasTabs: boolean;
@@ -2099,6 +3230,21 @@ export declare function useRelationOptions(client: NivaroClient | null, relatedC
2099
3230
  error: Error | null;
2100
3231
  };
2101
3232
 
3233
+ export declare function useRoomMembership(): {
3234
+ join: UseMutationResult<unknown, Error, string, unknown>;
3235
+ leave: UseMutationResult<unknown, Error, string, unknown>;
3236
+ setMuted: UseMutationResult<unknown, Error, {
3237
+ room: string;
3238
+ muted: boolean;
3239
+ }, unknown>;
3240
+ setNotifyMode: UseMutationResult<unknown, Error, {
3241
+ room: string;
3242
+ mode: "all" | "mentions";
3243
+ }, unknown>;
3244
+ };
3245
+
3246
+ export declare function useRoomPins(room: string | null): NoInfer<PinnedMessage[]>;
3247
+
2102
3248
  /** Stacked avatar cluster (+N) with a hover/pinnable portal roster whose rows
2103
3249
  * are UserChips — the single user-display primitive for owners/user lists.
2104
3250
  * Reused by the item-header Owners chip and the collection browser. */
@@ -2122,6 +3268,13 @@ export declare function useSessionRecorder(options?: SessionRecorderOptions): vo
2122
3268
 
2123
3269
  export declare function useTabState(form: UseNivaroFormReturn): TabState;
2124
3270
 
3271
+ export declare function useTogglePin(room: string): UseMutationResult<unknown, Error, number, unknown>;
3272
+
3273
+ export declare function useToggleReaction(room: string): UseMutationResult<unknown, Error, {
3274
+ messageId: number;
3275
+ emoji: string;
3276
+ }, unknown>;
3277
+
2125
3278
  export declare function useTypingIndicator(room: string | null): {
2126
3279
  onType: () => void;
2127
3280
  clearTyping: () => void;
@@ -2130,6 +3283,17 @@ export declare function useTypingIndicator(room: string | null): {
2130
3283
 
2131
3284
  export declare function useUnreadChirp(totalUnread: number, rooms?: RoomInfo[]): void;
2132
3285
 
3286
+ /** Directory of users to add to a private channel. */
3287
+ export declare function useUserSearch(search: string, enabled: boolean): {
3288
+ users: NoInfer<{
3289
+ id: string;
3290
+ first_name: string | null;
3291
+ last_name: string | null;
3292
+ email: string | null;
3293
+ }[]>;
3294
+ loading: boolean;
3295
+ };
3296
+
2133
3297
  export declare function useWatchFields(form: UseNivaroFormReturn, fields: string[]): Record<string, unknown>;
2134
3298
 
2135
3299
  /**
@@ -2149,6 +3313,32 @@ declare interface ValidationResult {
2149
3313
  unknownTokens: string[];
2150
3314
  }
2151
3315
 
3316
+ export declare function WidgetSlot({ widgetId, inputBindings, itemDraft, itemCollection, ready, label, defaultExpanded, frameless, apiBase: apiBaseProp, compact, strip, onClientAction, onWidgetType, onContentChange }: WidgetSlotProps): JSX.Element | null;
3317
+
3318
+ declare interface WidgetSlotProps {
3319
+ widgetId: number;
3320
+ inputBindings?: InputBinding[];
3321
+ itemDraft?: Record<string, unknown>;
3322
+ itemCollection?: string;
3323
+ ready?: boolean;
3324
+ label?: string;
3325
+ defaultExpanded?: boolean;
3326
+ /** Render body only — no card border/header/collapse (host provides chrome). */
3327
+ frameless?: boolean;
3328
+ apiBase?: string;
3329
+ compact?: boolean;
3330
+ strip?: boolean;
3331
+ onClientAction?: (action: ClientAction) => void;
3332
+ onWidgetType?: (type: string) => void;
3333
+ /**
3334
+ * Reports whether the widget has anything to show (review_list: any rows;
3335
+ * other types: any successful render; errors count as content so failures
3336
+ * stay visible). Lets a host section's hide_when_empty consider widget
3337
+ * content, not just field values.
3338
+ */
3339
+ onContentChange?: (hasContent: boolean) => void;
3340
+ }
3341
+
2152
3342
  export declare function WorkflowPanel({ collection, item }: {
2153
3343
  collection: string;
2154
3344
  item: string;