@object-ui/plugin-detail 17.6.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +7755 -0
  2. package/README.md +557 -71
  3. package/dist/ActivityTimeline.d.ts.map +1 -1
  4. package/dist/CommentAttachment.d.ts.map +1 -1
  5. package/dist/CommentAuthorControls.d.ts +40 -0
  6. package/dist/CommentAuthorControls.d.ts.map +1 -0
  7. package/dist/ConcurrentUpdateDialog.d.ts +16 -1
  8. package/dist/ConcurrentUpdateDialog.d.ts.map +1 -1
  9. package/dist/DetailSection.d.ts.map +1 -1
  10. package/dist/DetailSectionNode.d.ts +97 -0
  11. package/dist/DetailSectionNode.d.ts.map +1 -0
  12. package/dist/DetailView.d.ts +13 -0
  13. package/dist/DetailView.d.ts.map +1 -1
  14. package/dist/HeaderHighlight.d.ts.map +1 -1
  15. package/dist/HistoryTimeline.d.ts +21 -4
  16. package/dist/HistoryTimeline.d.ts.map +1 -1
  17. package/dist/InlineEditSaveBar.d.ts +68 -0
  18. package/dist/InlineEditSaveBar.d.ts.map +1 -1
  19. package/dist/InlineFieldInput.d.ts +80 -7
  20. package/dist/InlineFieldInput.d.ts.map +1 -1
  21. package/dist/ReactionPicker.d.ts.map +1 -1
  22. package/dist/RecordActivityTimeline.d.ts +32 -4
  23. package/dist/RecordActivityTimeline.d.ts.map +1 -1
  24. package/dist/RecordChatterPanel.d.ts +10 -1
  25. package/dist/RecordChatterPanel.d.ts.map +1 -1
  26. package/dist/RecordComments.d.ts +1 -0
  27. package/dist/RecordComments.d.ts.map +1 -1
  28. package/dist/RecordDetailDrawer.d.ts +20 -6
  29. package/dist/RecordDetailDrawer.d.ts.map +1 -1
  30. package/dist/RecordDetailPanel.d.ts +101 -0
  31. package/dist/RecordDetailPanel.d.ts.map +1 -0
  32. package/dist/RecordMetaFooter.d.ts +1 -1
  33. package/dist/RecordMetaFooter.d.ts.map +1 -1
  34. package/dist/RelatedList.d.ts +83 -10
  35. package/dist/RelatedList.d.ts.map +1 -1
  36. package/dist/ThreadedReplies.d.ts +9 -1
  37. package/dist/ThreadedReplies.d.ts.map +1 -1
  38. package/dist/emptiness.d.ts +142 -0
  39. package/dist/emptiness.d.ts.map +1 -0
  40. package/dist/fieldEnrichment.d.ts +46 -5
  41. package/dist/fieldEnrichment.d.ts.map +1 -1
  42. package/dist/headerColor.d.ts +20 -0
  43. package/dist/headerColor.d.ts.map +1 -0
  44. package/dist/index.d.ts +94 -77
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +5693 -5487
  47. package/dist/index.umd.cjs +2 -5
  48. package/dist/recordPageProvenance.d.ts +39 -0
  49. package/dist/recordPageProvenance.d.ts.map +1 -0
  50. package/dist/renderers/record-activity.d.ts.map +1 -1
  51. package/dist/renderers/record-alert.d.ts +23 -3
  52. package/dist/renderers/record-alert.d.ts.map +1 -1
  53. package/dist/renderers/record-alert.readProps.d.ts +40 -0
  54. package/dist/renderers/record-alert.readProps.d.ts.map +1 -0
  55. package/dist/renderers/record-chatter.d.ts.map +1 -1
  56. package/dist/renderers/record-details.d.ts +12 -0
  57. package/dist/renderers/record-details.d.ts.map +1 -1
  58. package/dist/renderers/record-highlights.d.ts +10 -0
  59. package/dist/renderers/record-highlights.d.ts.map +1 -1
  60. package/dist/renderers/record-history.d.ts +1 -1
  61. package/dist/renderers/record-history.d.ts.map +1 -1
  62. package/dist/renderers/record-path.d.ts.map +1 -1
  63. package/dist/renderers/record-quick-actions.d.ts +26 -6
  64. package/dist/renderers/record-quick-actions.d.ts.map +1 -1
  65. package/dist/renderers/record-reference-rail.d.ts +40 -0
  66. package/dist/renderers/record-reference-rail.d.ts.map +1 -1
  67. package/dist/renderers/record-related-list.d.ts +51 -7
  68. package/dist/renderers/record-related-list.d.ts.map +1 -1
  69. package/dist/renderers/recordActivityFeed.d.ts +343 -33
  70. package/dist/renderers/recordActivityFeed.d.ts.map +1 -1
  71. package/dist/renderers/recordBinding.d.ts +180 -0
  72. package/dist/renderers/recordBinding.d.ts.map +1 -0
  73. package/dist/renderers/recordComponentAria.d.ts +44 -0
  74. package/dist/renderers/recordComponentAria.d.ts.map +1 -0
  75. package/dist/renderers/relatedListActions.d.ts +102 -0
  76. package/dist/renderers/relatedListActions.d.ts.map +1 -0
  77. package/dist/summaryChipPercent.d.ts +83 -0
  78. package/dist/summaryChipPercent.d.ts.map +1 -0
  79. package/dist/summaryChipRenderers.d.ts +83 -0
  80. package/dist/summaryChipRenderers.d.ts.map +1 -0
  81. package/dist/synth/buildDefaultPageSchema.d.ts +188 -21
  82. package/dist/synth/buildDefaultPageSchema.d.ts.map +1 -1
  83. package/dist/useDetailTranslation.d.ts.map +1 -1
  84. package/dist/useRecordEditable.d.ts +4 -1
  85. package/dist/useRecordEditable.d.ts.map +1 -1
  86. package/package.json +19 -18
  87. package/dist/CommentInput.d.ts +0 -24
  88. package/dist/CommentInput.d.ts.map +0 -1
  89. package/dist/DiffView.d.ts +0 -24
  90. package/dist/DiffView.d.ts.map +0 -1
  91. package/dist/InlineCreateRelated.d.ts +0 -32
  92. package/dist/InlineCreateRelated.d.ts.map +0 -1
  93. package/dist/MentionAutocomplete.d.ts +0 -43
  94. package/dist/MentionAutocomplete.d.ts.map +0 -1
  95. package/dist/PointInTimeRestore.d.ts +0 -28
  96. package/dist/PointInTimeRestore.d.ts.map +0 -1
  97. package/dist/RecordNavigationEnhanced.d.ts +0 -18
  98. package/dist/RecordNavigationEnhanced.d.ts.map +0 -1
  99. package/dist/RelationshipGraph.d.ts +0 -23
  100. package/dist/RelationshipGraph.d.ts.map +0 -1
  101. package/dist/renderers/PermissionFacetLink.d.ts +0 -38
  102. package/dist/renderers/PermissionFacetLink.d.ts.map +0 -1
@@ -0,0 +1,44 @@
1
+ import { RecordComponentAriaProps } from '@object-ui/types';
2
+ /**
3
+ * The authored `aria` bag as a renderer actually receives it: the declared
4
+ * face, plus the undeclared `label` spelling a stored document may still carry.
5
+ */
6
+ export type AuthoredRecordAria = RecordComponentAriaProps & {
7
+ /**
8
+ * ⛔ Not a member of the contract. It is the shared ARIA shape's alias entry
9
+ * and is refused on parse. Declared on this type ONLY so the diagnostic can
10
+ * see it without an `as any` cast (a cast is how `record:path` once came to
11
+ * read this spelling and no other).
12
+ *
13
+ * ⚠️ It is never read as a name. objectui#9945 retired the fold that did;
14
+ * a served value is reported, and only for a caller that names its `block`.
15
+ * See this module's header.
16
+ */
17
+ label?: string;
18
+ };
19
+ /** What {@link useRecordAriaProps} spreads onto a block's container element. */
20
+ export interface RecordAriaDomProps {
21
+ 'aria-label'?: string;
22
+ 'aria-describedby'?: string;
23
+ role?: string;
24
+ }
25
+ /**
26
+ * The ARIA DOM props for a `record:*` block's own container.
27
+ *
28
+ * @param aria - the authored bag, straight off the node's `aria` key.
29
+ * @param defaultRole - the role this block renders when the author declares
30
+ * none. Omit it and the container stays role-less until an author declares a
31
+ * name, a description or a role — at which point `'region'` carries the name,
32
+ * because an attribute on a `generic` element carries nothing.
33
+ * @param defaultLabel - the block's built-in accessible name, used when the
34
+ * author declared none (or declared an empty one).
35
+ * @param block - the block's registered type (`record:path`), named in the
36
+ * report of a served `aria.label`. Omit it and that spelling is still not
37
+ * read, just not reported. See this module's header for which blocks pass it.
38
+ */
39
+ export declare function useRecordAriaProps(aria: AuthoredRecordAria | undefined, { defaultRole, defaultLabel, block, }?: {
40
+ defaultRole?: string;
41
+ defaultLabel?: string;
42
+ block?: string;
43
+ }): RecordAriaDomProps;
44
+ //# sourceMappingURL=recordComponentAria.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recordComponentAria.d.ts","sourceRoot":"","sources":["../../src/renderers/recordComponentAria.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmGG;AAKH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAEjE;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,wBAAwB,GAAG;IAC1D;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,gFAAgF;AAChF,MAAM,WAAW,kBAAkB;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAwCD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,kBAAkB,GAAG,SAAS,EACpC,EACE,WAAW,EACX,YAAY,EACZ,KAAK,GACN,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACtE,kBAAkB,CAapB"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ *
8
+ * `record:related_list.actions` — where an authored action id lands on the list,
9
+ * or why it is refused (objectui#11163; the maintainer's ENFORCE ruling on
10
+ * objectstack-ai/objectstack#20665).
11
+ *
12
+ * The contract declares the key as `z.array(z.string())`, "Action IDs available
13
+ * for related records". This module is the whole of its meaning, kept apart
14
+ * from the renderer so the file that draws the block keeps exporting
15
+ * components only:
16
+ *
17
+ * - RESOLUTION is `resolveDeclaredActionIds` — the one function
18
+ * `page:header`, `record:quick_actions` and `record:alert` resolve an
19
+ * authored action id through (objectui#7182, objectui#7382) — against the
20
+ * RELATED object's registered `actions`. ⛔ No second lookup: the caller
21
+ * reads that registry through `useMetadataItem('object', …)`, the same
22
+ * entry `record:quick_actions.actionNames` uses.
23
+ * - Only the IDS arm is this key's. The inline-object arm of the shared
24
+ * rule is `page:header`'s transition tolerance (objectstack#11592) and is
25
+ * not inherited here, the choice `record:alert` made for its CTA: an
26
+ * object in this array would widen what a related list can run past what
27
+ * the object declares.
28
+ * - PLACEMENT is each action's own `locations`, read by `actionRendersAt`
29
+ * — the platform's one placement rule, and the rule the host bridge
30
+ * (`RelatedRecordActionsBridge.deriveActions`) already applies to this
31
+ * list: `list_toolbar` draws a header button; `list_item` and
32
+ * `record_related` draw a row-menu item (objectui#11270). This block only
33
+ * ever renders inside a parent record, which is the scope `record_related`
34
+ * names (objectstack-ai/objectstack#20937, triage `5919625056`: row
35
+ * placement, inside a record only). Naming an action here does not bypass
36
+ * location filtering (only a list view's selection bar is placed by
37
+ * naming), so an id whose action declares none of the three is REFUSED
38
+ * rather than drawn somewhere or dropped.
39
+ * - ORDER is the author's, within each surface.
40
+ */
41
+ /**
42
+ * An action definition as the registry holds it — the two members read here,
43
+ * typed `unknown` because the definitions arrive straight off the metadata
44
+ * wire (the reason `actionRendersAt` gives for its own parameter).
45
+ */
46
+ export interface RegisteredRelatedAction {
47
+ readonly name?: unknown;
48
+ readonly locations?: unknown;
49
+ }
50
+ /** One reason an authored entry does not render, stated for the author. */
51
+ export type RelatedListActionRefusal =
52
+ /** The array itself is refused: not an array, or not all action ids. */
53
+ {
54
+ readonly kind: 'array';
55
+ readonly message: string;
56
+ }
57
+ /** An id that names no registered action of the related object. */
58
+ | {
59
+ readonly kind: 'unresolved';
60
+ readonly id: string;
61
+ }
62
+ /** An id that resolves, to an action declaring no location this list renders. */
63
+ | {
64
+ readonly kind: 'unplaced';
65
+ readonly id: string;
66
+ };
67
+ export interface PlacedRelatedListActions<T> {
68
+ /** `list_toolbar` actions, in authored order — the list's header buttons. */
69
+ readonly toolbar: T[];
70
+ /** `list_item` and `record_related` actions, in authored order — each row's menu. */
71
+ readonly row: T[];
72
+ /** Every authored entry that renders nowhere, and why. */
73
+ readonly refused: RelatedListActionRefusal[];
74
+ }
75
+ /**
76
+ * Whether an authored value has any id to look up — the registry-independent
77
+ * half of the shared rule, so the caller requests the metadata read only when
78
+ * it can change the answer (a `[]`, an object array or a refused array is
79
+ * decided without it).
80
+ */
81
+ export declare function relatedListActionsNeedLookup(authored: unknown): boolean;
82
+ /**
83
+ * Resolve and place an authored `record:related_list.actions` value.
84
+ *
85
+ * `registered` is the related object's `actions` once the metadata lookup has
86
+ * ANSWERED. The caller does not call this while the lookup is in flight: every
87
+ * id would come back unresolved and the list would refuse an id it is about to
88
+ * resolve.
89
+ *
90
+ * An id authored twice renders once, at its first position — it is drawn, so
91
+ * nothing the author asked for is dropped.
92
+ */
93
+ export declare function placeAuthoredRelatedListActions<T extends RegisteredRelatedAction>(authored: unknown, registered: readonly T[] | null | undefined): PlacedRelatedListActions<T>;
94
+ /**
95
+ * The refusal notice's text — one sentence per refused entry, naming it and
96
+ * the related object, so the fix is a one-entry edit.
97
+ *
98
+ * `objectKnown` is whether the lookup found the related object at all: an id
99
+ * cannot be "not an action of" an object whose metadata never arrived.
100
+ */
101
+ export declare function describeRelatedListActionRefusals(refused: readonly RelatedListActionRefusal[], objectName: string, objectKnown: boolean): string;
102
+ //# sourceMappingURL=relatedListActions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relatedListActions.d.ts","sourceRoot":"","sources":["../../src/renderers/relatedListActions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,2EAA2E;AAC3E,MAAM,MAAM,wBAAwB;AAClC,wEAAwE;AACtE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AACtD,mEAAmE;GACjE;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE;AACtD,iFAAiF;GAC/E;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;IACtB,qFAAqF;IACrF,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;IAClB,0DAA0D;IAC1D,QAAQ,CAAC,OAAO,EAAE,wBAAwB,EAAE,CAAC;CAC9C;AAQD;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAIvE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,+BAA+B,CAAC,CAAC,SAAS,uBAAuB,EAC/E,QAAQ,EAAE,OAAO,EACjB,UAAU,EAAE,SAAS,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,GAC1C,wBAAwB,CAAC,CAAC,CAAC,CA6C7B;AAED;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,SAAS,wBAAwB,EAAE,EAC5C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,OAAO,GACnB,MAAM,CAeR"}
@@ -0,0 +1,83 @@
1
+ import { PercentScale } from '@object-ui/core';
2
+ /**
3
+ * The ONE rule the `summaryFields` chip scales a stored `percent` by — and it
4
+ * is the repo's, not this file's (objectui#9071).
5
+ *
6
+ * ⭐ Since objectui#9167 this is the chip's BAR path only. The chip's TEXT goes
7
+ * straight to `formatPercent` in `@object-ui/fields` — the list cell's own
8
+ * body, which applies {@link percentDisplayValue} itself and then the locale's
9
+ * percent affix at the field's precision. See "The half objectui#9071 did NOT
10
+ * take" below, which objectui#9167 closed.
11
+ *
12
+ * ## Why this function exists at all (objectui#8728)
13
+ *
14
+ * The chip beside the record H1 draws a percent TWICE — as its text and as a
15
+ * small bar — and the two halves scaled the same stored number by two
16
+ * different rules:
17
+ *
18
+ * display = `${num}%`; // the text: no scaling
19
+ * const normalized = num <= 1 ? num * 100 : num; // the bar: ratio, scaled
20
+ *
21
+ * so a stored `0.123` labelled itself `0.123%` beside a bar filled to 12.3%.
22
+ * One control, two percentages, and neither half told the reader which one was
23
+ * right. Both halves read this function — which is why it is a named function
24
+ * and not the same ternary written twice. The next reader cannot
25
+ * re-desynchronise them without deleting a call site.
26
+ *
27
+ * ## What objectui#9071 DELETED, and why aligning it would not have done
28
+ *
29
+ * objectui#8728 converged the chip's two halves onto the BAR's rule and
30
+ * recorded, rather than decided, that the rule was the chip's own. The census
31
+ * it asked for answered the question: the predicate that stood here —
32
+ * a pass-through spelled `if (raw > 1) return raw;`, i.e. the bar's original
33
+ * `num <= 1` written the other way round — was the only spelling of its kind on
34
+ * this side of the tree, and it disagreed with the declared single source of
35
+ * truth at EXACTLY 1 and at every value AT OR BELOW -1:
36
+ *
37
+ * | stored | the deleted local rule | `percentDisplayValue`, and every other band |
38
+ * |--------|------------------------|---------------------------------------------|
39
+ * | `0.25` | `25%` | `25%` (the band they always agreed on) |
40
+ * | `1` | `100%` | `1%` |
41
+ * | `-1` | `-100%` | `-1%` |
42
+ * | `-5` | `-500%` | `-5%` |
43
+ *
44
+ * ⛔ The repair is NOT to move the local predicate's boundary onto the shared
45
+ * one. That would leave two rules agreeing by coincidence — the same shape
46
+ * objectui#5607 removed one package over, and the shape that produced this
47
+ * drift in the first place. The predicate is gone; the authority is
48
+ * {@link percentDisplayValue} in `@object-ui/core`, whose own doc comment
49
+ * requires exactly this of a third surface, and which the list cell
50
+ * (`formatPercent` / `PercentCellRenderer`), the dashboard measure
51
+ * (`formatMeasure`) and the grid column summary already read.
52
+ *
53
+ * ## The half objectui#9071 did NOT take — closed by objectui#9167
54
+ *
55
+ * That doc comment asks a third surface for BOTH halves — the scaling AND the
56
+ * convention. objectui#9071 moved the SCALING only: the chip stated the full
57
+ * JavaScript number with a bare `%` while the list cell rendered through the
58
+ * locale's percent affix at the field's precision (`0` by default), so a stored
59
+ * `12.3` read `12.3%` beside the H1 and `12%` in the list — two spellings of
60
+ * one magnitude, with the two bars proving the magnitude was one. That card
61
+ * stopped because its acceptance forbade moving values the chip rendered
62
+ * correctly today, and the convention half moves several.
63
+ *
64
+ * objectui#9167 took it, value by value rather than by inspection: both
65
+ * surfaces were driven in the same run, on the same field, for every stored
66
+ * value below, and every row that MOVED moved onto the reading the list cell
67
+ * was already giving. The chip's text is now the cell's own `formatPercent`
68
+ * call, byte for byte, and this function is left
69
+ * holding the BAR alone. The table and its `de-DE` leg are pinned in
70
+ * `__tests__/summaryChip.percentConvention-9167.test.tsx`.
71
+ *
72
+ * ## The storage is the field's, stated by the caller (objectui#11475)
73
+ *
74
+ * `percentScale` is the storage the chip's field declares, which `DetailView`
75
+ * reads with `percentCellScale` (`@object-ui/fields`): the spec's
76
+ * `percentScaleOf`, the answer the list cell reads too. Until objectui#11475
77
+ * the shared source guessed the storage from the value's magnitude, so the
78
+ * table above, whose every row is that guess, no longer describes this
79
+ * function: a fraction-stored `1` now fills the bar to 100%, and a
80
+ * whole-stored `0.5` to half a percent.
81
+ */
82
+ export declare function summaryChipPercentPoints(raw: number, percentScale: PercentScale): number;
83
+ //# sourceMappingURL=summaryChipPercent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summaryChipPercent.d.ts","sourceRoot":"","sources":["../src/summaryChipPercent.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+EG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,MAAM,CAyBxF"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Which field kinds the `summaryFields` chip beside the record H1 may draw
10
+ * with the field's OWN cell renderer, and which it may not (objectui#8464).
11
+ *
12
+ * ## Why this set exists at all
13
+ *
14
+ * The chip's display was `String(val)` with four families special-cased, so an
15
+ * object-valued summary field printed the literal `[object Object]` beside the
16
+ * H1 — and, because the chip's accessible name is built from that same string,
17
+ * in its accessible name too. The fix routes such a value through
18
+ * `getCellRenderer`, the way `HeaderHighlight` one band below already does.
19
+ *
20
+ * ⚠️ That route is NOT free, and this set is the measurement that says so. A
21
+ * Badge is a `whitespace-nowrap rounded-full` pill — a much smaller surface
22
+ * than a cell — and some registered types draw something a pill cannot host.
23
+ * Measured, not assumed: every registered type was rendered through
24
+ * `getCellRenderer` INSIDE the real chip Badge against the object value
25
+ * `{ id: 'acct-1', name: 'Acme Corp' }`, and the DOM inside the pill counted.
26
+ * The instrument is `__tests__/summaryChip.badgeFitCensus-8464.test.tsx`,
27
+ * which re-derives the whole table and fails if this set stops matching it.
28
+ *
29
+ * ⛔ The size of that population is deliberately NOT written down here
30
+ * (objectui#8734, AGENTS.md #9). It used to read "18 of the 53 registered
31
+ * types" — a figure derived once, re-derived never, and still true on the day
32
+ * it went stale. The census now takes its population from
33
+ * `listCellRendererTypes()`, `@object-ui/fields`' live reading of the registry,
34
+ * so the count lives in the instrument that re-derives it and nowhere else.
35
+ *
36
+ * ## The five measured refusals
37
+ *
38
+ * | class | types | what the pill got |
39
+ * |-------------------------------|--------------------------------------------------------|-------------------------------------------------------|
40
+ * | a pill inside a pill | `select` `status` `multiselect` `radio` `checkboxes` `tags` | `SelectCellRenderer`'s own `Badge` — one `rounded-full` node nested in the chip's own |
41
+ * | an avatar composite | `user` | TWO `rounded-full` nodes (Radix `Avatar.Root` + `AvatarFallback`) and the initials glued onto the name: `ACAcme Corp` |
42
+ * | an image and no text | `image` `avatar` `signature` | an `<img>` and `textContent === ''` — a pill with nothing to say, and nothing for the accessible name |
43
+ * | a "No value" face | `boolean` `toggle` `datetime` `repeater` (`EmptyValue`), `date` (`formatDate`'s own em-dash, objectui#8581) | the chip is drawn only AFTER `hasCellValue` called the value FILLED; a renderer answering "empty" one band later re-opens exactly the cross-band contradiction objectui#8394 closed |
44
+ * | an interactive control | `file` `video` `audio` (objectui#9161) | an `<a href>` view/download link per file — a control, and the page-title row hosts text |
45
+ *
46
+ * Every other registered type draws plain inline text inside the pill —
47
+ * `Acme Corp` for
48
+ * the nameable families, the JSON literal for `location` / `geolocation` /
49
+ * `address` / `json` / `object` / `composite` / `record` behind objectui#8481's
50
+ * declared json-literal fence, and the value-independent faces (`password`,
51
+ * `secret`, `vector`, `grid`).
52
+ *
53
+ * ## What the refused kinds get instead — NOT a stringifier written here
54
+ *
55
+ * They fall to `coerceToSafeValue`, `@object-ui/fields`' single documented
56
+ * answer to "what text does a cell draw for a value that is not a string".
57
+ * objectui#8596 ruled `select` / `status` / `multiselect` / `radio` /
58
+ * `checkboxes` / `tags` / `user` onto exactly that text for an object value, so
59
+ * for 7 of these 18 the chip is BYTE-EQUAL to its own cell. ⛔ Nothing here
60
+ * invents a renderer-side format (AGENTS.md #0.1): every kind lands either on
61
+ * its own renderer or on the one coercion this repo already had.
62
+ */
63
+ export declare const CHIP_UNFIT_RENDERER_TYPES: ReadonlySet<string>;
64
+ /**
65
+ * May the chip draw `rendererType` with the field's own cell renderer?
66
+ *
67
+ * Takes the type ALREADY resolved through `resolveCellRendererType`, because
68
+ * that is the key `getCellRenderer` dispatches on — asking this question of the
69
+ * authored spelling would answer for a renderer the chip is not about to use.
70
+ *
71
+ * ⚠️ **The default is PERMISSIVE, and what makes that safe is the census, not
72
+ * this function** (objectui#8734). A type absent from
73
+ * {@link CHIP_UNFIT_RENDERER_TYPES} is admitted, so an UNMEASURED type is
74
+ * admitted too. Inverting that — unknown ⇒ refused — is not the free repair it
75
+ * looks like: it would silently downgrade a legitimately-fitting new type to
76
+ * `coerceToSafeValue`, which is a product decision about the chip and not a
77
+ * defect. What objectui#8734 closed instead is the way an unknown type used to
78
+ * arrive: unnoticed. The census reconciles its table against
79
+ * `listCellRendererTypes()`, so a newly registered type turns CI red and names
80
+ * itself before it can reach this predicate in a release.
81
+ */
82
+ export declare function chipTakesCellRenderer(rendererType: string): boolean;
83
+ //# sourceMappingURL=summaryChipRenderers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summaryChipRenderers.d.ts","sourceRoot":"","sources":["../src/summaryChipRenderers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAgCxD,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAEnE"}
@@ -1,4 +1,4 @@
1
- import { ServiceObject } from '@objectstack/spec/data';
1
+ import { SelectOptionKind, ServiceObject } from '@objectstack/spec/data';
2
2
  import { detectStatusField } from '@object-ui/types';
3
3
  /** Minimal shape of an object definition we read here. We deliberately
4
4
  * duck-type so this helper has zero hard dependency on a specific
@@ -18,13 +18,30 @@ export interface ObjectDefLike {
18
18
  /** Semantic role (ADR-0085): the object's most important fields —
19
19
  * drives the highlight strip (first 4). */
20
20
  highlightFields?: string[];
21
- /** Name of the field that holds the record's display title (e.g. `name`,
22
- * `subject`). When present we exclude it from the auto-derived highlight
23
- * list to avoid duplicating the page H1. */
21
+ /**
22
+ * ACCEPTED BUT NEVER READ — do not "tidy" this away (objectui#7287).
23
+ *
24
+ * `primaryField` is a `DetailViewSchema` key (`@object-ui/types` `views.ts`),
25
+ * and reading it off an OBJECT def is the defect objectui#7287 removed:
26
+ * `resolveTitleField` now delegates to `@object-ui/core`'s `resolveNameField`
27
+ * and no code in this module consults this member. No producer can populate
28
+ * it either — `@objectstack/spec`'s object schema is a `strictObject` that
29
+ * answers `unrecognized_keys: ['primaryField']`.
30
+ *
31
+ * It stays declared because this interface is REACHABLE, which is a
32
+ * different question from what it describes: `ObjectDefLike` is re-exported
33
+ * from this package's index and `@object-ui/plugin-detail` is published, so
34
+ * the member is part of a shipped `.d.ts`. Deleting it narrows a published
35
+ * type — an external `const d: ObjectDefLike = { primaryField: 'x' }`
36
+ * compiles today and would stop compiling — and that is a contract change
37
+ * owed its own card, not a rider on a behaviour fix. Retiring it is a
38
+ * deliberate act for someone to take on purpose.
39
+ */
24
40
  primaryField?: string;
25
- /** Optional section grouping for the details region. */
41
+ /** Optional section grouping for the details region. The heading rides
42
+ * `label` — the one slot `@object-ui/types` declares (objectui#6190). */
26
43
  sections?: Array<{
27
- title?: string;
44
+ label?: string;
28
45
  columns?: number;
29
46
  fields?: any[];
30
47
  }>;
@@ -55,7 +72,7 @@ export interface ObjectDefLike {
55
72
  fieldGroups?: Array<Partial<NonNullable<ServiceObject['fieldGroups']>[number]>>;
56
73
  /**
57
74
  * Spec `enable` capability toggles. Only `files` is read here:
58
- * `enable.files === true` (opt-in, #2727) makes the synthesizer emit an
75
+ * `enable.files === true` (opt-in, objectstack-ai/objectstack#2727) makes the synthesizer emit an
59
76
  * Attachments tab beside Details/Related (objectstack#4358).
60
77
  */
61
78
  enable?: {
@@ -82,9 +99,17 @@ export interface ObjectDefFieldLike {
82
99
  name?: string;
83
100
  label?: string;
84
101
  type?: string;
102
+ /**
103
+ * The picklist. `kind` is the option's declared lifecycle category
104
+ * (`SelectOption.kind` in `@objectstack/spec/data`, objectstack#23809):
105
+ * {@link deriveStages} carries a `won` / `lost` kind onto the derived stage
106
+ * as its `terminal`, and `record:path` lets any declared kind (`open`
107
+ * included) win over its spelling heuristic (objectui#12907).
108
+ */
85
109
  options?: Array<{
86
110
  value: any;
87
111
  label: string;
112
+ kind?: SelectOptionKind;
88
113
  }>;
89
114
  /** Declared group membership — matches a `fieldGroups[].key`. */
90
115
  group?: string;
@@ -100,9 +125,10 @@ export interface BuildPageOptions {
100
125
  value: any;
101
126
  label: string;
102
127
  }>;
103
- /** Override the auto-derived section grouping. */
128
+ /** Override the auto-derived section grouping. The heading rides `label` —
129
+ * the one slot `@object-ui/types` declares (objectui#6190). */
104
130
  sections?: Array<{
105
- title?: string;
131
+ label?: string;
106
132
  columns?: number;
107
133
  fields?: any[];
108
134
  }>;
@@ -118,9 +144,13 @@ export interface BuildPageOptions {
118
144
  * Opt in to the Reference Rail (aside region with
119
145
  * `record:reference_rail`). The rail is OFF by default — it fans out a
120
146
  * collection query per related list, which is wasteful on records the
121
- * author never intended to summarize. Set `showReferenceRail: true`
122
- * (per object via `detail.showReferenceRail`) to surface it; it then
147
+ * author never intended to summarize. Set `showReferenceRail: true` on
148
+ * this options object to surface it — that is the only route: ADR-0085
149
+ * removed the per-object `detail` hints block, so no object definition
150
+ * can switch the rail on, and a per-page rail is an authored
151
+ * `record:reference_rail` node in an assigned Page schema. It then
123
152
  * emits only when `related` has at least 2 entries.
153
+ * Pinned by `__tests__/referenceRailRoute-9816.test.ts`.
124
154
  */
125
155
  showReferenceRail?: boolean;
126
156
  /**
@@ -150,7 +180,7 @@ export interface BuildPageOptions {
150
180
  *
151
181
  * - `objectName` and `relationshipField` are required.
152
182
  * - `title` overrides the default child-object label.
153
- * - `columns` / `limit` / `icon` are forwarded to the renderer.
183
+ * - `columns` / `limit` / `icon` / `sort` are forwarded to the renderer.
154
184
  */
155
185
  related?: Array<{
156
186
  title?: string;
@@ -159,6 +189,40 @@ export interface BuildPageOptions {
159
189
  columns?: any[];
160
190
  limit?: number;
161
191
  icon?: string;
192
+ /**
193
+ * Row order for this list — the spec `record:related_list.sort` union,
194
+ * forwarded verbatim onto the synthesized node.
195
+ *
196
+ * The host supplies it; this synthesizer neither derives nor overrides it.
197
+ * `RecordDetailView` fills it from the child object's DEFAULT LIST VIEW
198
+ * sort (objectui#5795 — ruled direction 1 on objectstack#11345,
199
+ * maintainer 2026-08-23: inherit the child list view's sort, NO new spec
200
+ * key), already lowered to the array arm. Omitted → the node carries no
201
+ * `sort` and the related list falls back to the server's PK order, exactly
202
+ * as before this key had a producer.
203
+ */
204
+ sort?: string | Array<{
205
+ field: string;
206
+ order: 'asc' | 'desc';
207
+ }>;
208
+ /**
209
+ * This list's own declared SCOPE — forwarded verbatim onto the synthesized
210
+ * node's `filter`, the key `record:related_list` has read since
211
+ * objectstack#7118 (`RelatedList` ANDs it with `{ [relationshipField]:
212
+ * parentId }`, never substituting for it).
213
+ *
214
+ * The host supplies it; this synthesizer neither derives nor merges it.
215
+ * `RecordDetailView` fills it from the FK field's `relatedListFilter`
216
+ * (objectui#4664 — the spec key from objectstack#8704 / PR objectstack-ai/objectstack#8955), already
217
+ * in the canonical Query-DSL `FilterCondition` shape that key declares. The
218
+ * component-level authored `filter` and this derived one are therefore the
219
+ * same vocabulary arriving at the same prop, which is the point: one read
220
+ * site, one lowering, no per-producer dialect.
221
+ *
222
+ * Omitted → the node carries no `filter` and the list sends the parent
223
+ * scope alone, exactly as before this key had a producer.
224
+ */
225
+ filter?: Record<string, any> | any[];
162
226
  /**
163
227
  * `relatedList: 'primary'` — a CORE relationship. Under the default
164
228
  * layout this list is promoted to its OWN tab; non-primary lists collapse
@@ -268,24 +332,78 @@ export interface BuildPageOptions {
268
332
  * long-standing plugin-detail import path.
269
333
  */
270
334
  export { detectStatusField };
335
+ /**
336
+ * The `record:path` terminal a declared option `kind` stands for: `won` and
337
+ * `lost` are the spec's `stages[].terminal` values verbatim (the vocabulary
338
+ * objectstack#23809 chose to match), and `open` is no terminal.
339
+ */
340
+ export declare function stageTerminalOfKind(kind: SelectOptionKind | undefined): 'won' | 'lost' | undefined;
271
341
  /**
272
342
  * Derive stage values from an object field's `options` (picklist).
343
+ *
344
+ * A stage carries `terminal` from its option's declared `kind`
345
+ * ({@link stageTerminalOfKind}, objectui#12907), so a `won` option is the
346
+ * path's forward terminus and a `lost` one sits in the separated group
347
+ * whatever it is called. An option that declares no kind, or `open`, gets no
348
+ * `terminal` key: the stage shape is the spec's strict `stages[]` row, which
349
+ * has no way to say "declared open" — `record:path` reads that off the field
350
+ * itself ({@link declaredStageKinds}).
273
351
  */
274
352
  export declare function deriveStages(def: ObjectDefLike | undefined, statusField: string | null): Array<{
275
353
  value: any;
276
354
  label: string;
355
+ terminal?: 'won' | 'lost';
277
356
  }> | null;
278
357
  /**
279
- * Resolve the record's title field — the value the page renders as its H1.
280
- * Declared role first (`primaryField` / `nameField` / deprecated
281
- * `displayNameField`), else the first conventional display-field name present
282
- * on the object. Mirrors `record-details`' titleCandidates so "what the H1
283
- * shows" and "what the strip skips" can never disagree.
358
+ * The lifecycle `kind` each option of `statusField` declares, keyed by option
359
+ * value — only the options that declare one (objectui#12907).
360
+ *
361
+ * ⚠️ Package-internal: exported for `renderers/record-path.tsx`, NOT
362
+ * re-exported from the package entry. `record:path` consults it after a
363
+ * stage's explicit `terminal` and before its spelling heuristic, because the
364
+ * spec's strict `stages[]` row can carry `won` / `lost` but not `open`: a
365
+ * declared `open` on a value spelled `closed_won` has to beat the heuristic
366
+ * from here. Returns `null` when there is nothing to consult.
367
+ */
368
+ export declare function declaredStageKinds(def: ObjectDefLike | undefined, statusField: string | null): Map<unknown, SelectOptionKind> | null;
369
+ /**
370
+ * WHICH FIELD titles this record — the field whose value the page renders as
371
+ * its H1, in NAME space.
372
+ *
373
+ * DELEGATES to `@object-ui/core`'s `resolveNameField`: the ONE shared ADR-0079
374
+ * ladder (`nameField` -> deprecated `displayNameField` / `NAME_FIELD_KEY` ->
375
+ * type-aware derivation), the same spelling `getRecordDisplayName` reads to
376
+ * produce the H1's VALUE and `leadWithNameField` reads to lead a list. It does
377
+ * not re-implement that ladder. ADR-0079 collapsed ~6 divergent title
378
+ * resolvers and this module grew one back (objectui#7287); a second ladder
379
+ * that agrees today diverges again on the next change, which is the whole
380
+ * history of this defect.
381
+ *
382
+ * Two rungs went away with the delegation:
383
+ *
384
+ * - `def.primaryField` — a `DetailViewSchema` key (`@object-ui/types`
385
+ * `views.ts`), read here off an OBJECT def and ranked ABOVE the canonical
386
+ * `nameField` ADR-0079 Phase 2 made the pointer. No producer can put it
387
+ * there: `@objectstack/spec`'s object schema is a `strictObject` that
388
+ * answers `unrecognized_keys: ['primaryField']`, and `ObjectSchema.create()`
389
+ * throws — which is why objectstack#6326 deleted the identical read from
390
+ * two lint rules. A census across both repos found zero object payloads
391
+ * carrying it. Same shape as the undeclared `objectDef.titleField` read
392
+ * objectui#6531 measured and #6557 removed.
393
+ * - the literal `['name','full_name','title','subject','display_name']` walk
394
+ * — a NAME match with no type check. `deriveTitleField` ranks those same
395
+ * name-ish names first AND rejects non-title types, so a `select` named
396
+ * `title` stops being named as the H1 field that the H1 never shows.
397
+ *
398
+ * Returns `null` rather than `undefined` for this module's `!== titleField`
399
+ * filters and app-shell's `?? 'name'` call site.
284
400
  */
285
401
  export declare function resolveTitleField(def: ObjectDefLike | undefined): string | null;
286
402
  /**
287
403
  * Pick up to N "highlight" fields by name. Skips the status field
288
404
  * (already shown in `record:path`) and obvious junk like id / created_at.
405
+ * Members of a group gated by `visibleWhen` are never picked
406
+ * ({@link membersOfGatedFieldGroups}, objectui#12720).
289
407
  */
290
408
  export declare function deriveHighlightFields(def: ObjectDefLike | undefined, statusField: string | null, max?: number): string[];
291
409
  /**
@@ -313,6 +431,40 @@ export declare function buildDefaultActions(_def: ObjectDefLike | undefined, hea
313
431
  * two adjacent nodes.
314
432
  */
315
433
  export declare function buildDefaultHighlights(def: ObjectDefLike | undefined, options?: Pick<BuildPageOptions, 'highlightFields' | 'statusField' | 'stages' | 'hideHighlights' | 'hidePath'>): any[];
434
+ /**
435
+ * One field group, resolved — the section a `record:details` `{ group }`
436
+ * reference stands for — plus the group's own ADR-0089 `visibleWhen`
437
+ * (objectui#11630).
438
+ *
439
+ * ⚠️ Package-internal. It is exported from this module for
440
+ * `renderers/record-details.tsx` and is NOT re-exported from the package entry.
441
+ * The predicate travels BESIDE the section, never on it: `section` is exactly
442
+ * what the public {@link deriveFieldGroupDetailSections} returns, so no caller
443
+ * of the public function can copy a `visibleWhen` onto an enumerated
444
+ * `record:details` section. `@objectstack/spec`'s `RecordDetailsProps.sections[]`
445
+ * is strict and refuses that key on parse; the spec's way to gate a section is
446
+ * to reference the gated group with `{ group }`, which inherits its predicate.
447
+ */
448
+ export interface FieldGroupDetailEntry {
449
+ /** The group key. Absent on the trailing ungrouped bucket. */
450
+ key?: string;
451
+ /**
452
+ * The group's `visibleWhen`, verbatim and unevaluated — a bare CEL string or
453
+ * an Expression envelope, as `deriveFieldGroupLayout` passes it through.
454
+ * Never present on the ungrouped bucket.
455
+ */
456
+ visibleWhen?: unknown;
457
+ /** The resolved section, in DetailSection's shape. */
458
+ section: Record<string, any>;
459
+ }
460
+ /**
461
+ * The derivation behind both {@link deriveFieldGroupDetailSections} and the
462
+ * `{ group }` references {@link resolveDetailSections} emits — one code path,
463
+ * so a reference and the section it resolves to cannot disagree.
464
+ *
465
+ * Package-internal; see {@link FieldGroupDetailEntry}.
466
+ */
467
+ export declare function deriveFieldGroupDetailEntries(def: ObjectDefLike | undefined): FieldGroupDetailEntry[] | null;
316
468
  /**
317
469
  * Derive detail sections from the object's declared `fieldGroups` plus each
318
470
  * field's `group` membership. The grouping SEMANTICS (declared order, empty
@@ -328,14 +480,29 @@ export declare function buildDefaultHighlights(def: ObjectDefLike | undefined, o
328
480
  * Section fields are emitted as rich descriptors (`{ name, label, type,
329
481
  * options }`) so DetailSection renders typed values without re-resolving
330
482
  * the object definition.
483
+ *
484
+ * ⚠️ This is the RESOLVED form of a group — what `record:details` renders for
485
+ * a `{ group }` reference — and not a page section to write into a
486
+ * `record:details` node: the rich `fields` descriptors are refused by
487
+ * `@objectstack/spec`'s `RecordDetailsProps.sections[].fields` (bare names), and
488
+ * a group's `visibleWhen` is deliberately not on it. A page references the group
489
+ * instead, which is what {@link resolveDetailSections} emits (objectui#11630).
331
490
  */
332
491
  export declare function deriveFieldGroupDetailSections(def: ObjectDefLike | undefined): Array<Record<string, any>> | null;
333
492
  /**
334
493
  * Resolve the sections for the Details tab body:
335
494
  * 1) explicit `options.sections` from the caller (programmatic API —
336
495
  * Studio preview / metadata-admin anchors);
337
- * 2) else sections derived from the object's `fieldGroups` semantic role;
338
- * 3) else `undefined` — record:details falls back to its flat layout.
496
+ * 2) else the object's `fieldGroups` semantic role, as `{ group }`
497
+ * references plus the ungrouped bucket's field names
498
+ * (`fieldGroupSectionReferences`, objectui#11630);
499
+ * 3) else `undefined` — a bare `record:details`, which draws the object's
500
+ * `highlightFields` (the spec's declared fallback for a node with
501
+ * neither `sections` nor `fields`, objectui#12433), and an empty body
502
+ * when the object declares none. It does NOT draw every field: a caller
503
+ * that wants the running app's default body passes that body's sections
504
+ * in (1) — app-shell's `RecordDetailView`, and Studio's page-create seed
505
+ * and slotted preview, take them from one derivation (objectui#12442).
339
506
  */
340
507
  export declare function resolveDetailSections(def: ObjectDefLike | undefined, sections?: BuildPageOptions['sections']): BuildPageOptions['sections'] | undefined;
341
508
  /**
@@ -356,7 +523,7 @@ export declare function buildDefaultTabs(def: ObjectDefLike | undefined, options
356
523
  */
357
524
  export declare function buildDefaultDiscussion(): any;
358
525
  /**
359
- * Sub-builder: the `record:attachments` panel (#2727). Emitted only when the
526
+ * Sub-builder: the `record:attachments` panel (objectstack-ai/objectstack#2727). Emitted only when the
360
527
  * object opts in via `enable.files: true` — the same gate the server enforces
361
528
  * on `sys_attachment` rows (403 FILES_DISABLED otherwise).
362
529
  */
@@ -366,7 +533,7 @@ export declare function buildDefaultAttachments(): any;
366
533
  * page.
367
534
  *
368
535
  * Shape:
369
- * { type:'record', template:'full-width', regions:[ { name:'main',
536
+ * { type:'record', template:'default', regions:[ { name:'main',
370
537
  * components: [page:header, record:highlights?, record:path?,
371
538
  * page:tabs, record:discussion?] } ] }
372
539
  * For `enable.files: true` objects the tabs node carries a peer