@object-ui/components 17.7.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.
package/README.md CHANGED
@@ -140,6 +140,28 @@ function MyComponent() {
140
140
  - `tabs` - Tab navigation
141
141
  - `accordion` - Collapsible sections
142
142
 
143
+ #### Page region posture — `usePageRegionPosture()`
144
+
145
+ The page renderer measures each region of a page against the page body, using
146
+ `ResizeObserver` on the region, not the viewport, and tells the region's blocks
147
+ how much room they have:
148
+
149
+ ```ts
150
+ import { usePageRegionPosture, type PageRegionPosture } from '@object-ui/components';
151
+
152
+ const posture: PageRegionPosture | null = usePageRegionPosture(); // 'compact' | 'full' | null
153
+ ```
154
+
155
+ - `'compact'`: the region is narrower than `(page − 24) / 2`, that is, half
156
+ the page less the 24 px gap between regions side by side. A half-and-half
157
+ cell therefore stays `'full'`.
158
+ - `'full'`: the region is half the page or wider.
159
+ - `null`: outside a page region, or before the width is known (server render,
160
+ no `ResizeObserver`). Read `null` as `'full'`.
161
+
162
+ The posture is measured, never authored, and no schema key sets it. The
163
+ `list-view` block in `@object-ui/plugin-list` reads it to draw a compact list.
164
+
143
165
  ### Data Display
144
166
  - `table` - Data table
145
167
  - `list` - List view
@@ -8,13 +8,14 @@ export interface RecordTitleChipProps {
8
8
  objectLabel?: string;
9
9
  /** Record identifier shown next to the object label with a copy button. */
10
10
  resourceId?: string;
11
- /** Hide the favourite star (default: shown). */
12
- showStar?: boolean;
13
11
  /** Hide the copy-id button (default: shown when `resourceId` is given). */
14
12
  showCopyId?: boolean;
15
13
  /** Controlled favourite state. When omitted, the chip manages its own. */
16
14
  isFavorite?: boolean;
17
- /** Called when the favourite star is toggled. */
15
+ /**
16
+ * Called when the favourite star is toggled. The star draws exactly when
17
+ * this is given: a star with nothing to call would store nothing.
18
+ */
18
19
  onToggleFavorite?: (next: boolean) => void;
19
20
  /** Extra classes for the outer flex column. */
20
21
  className?: string;
@@ -0,0 +1,37 @@
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
+ * The ONE viewport gutter every wide centred dialog reads from `sm` up
10
+ * (objectui#12593, promoted to a shared export by objectui#12603).
11
+ *
12
+ * The `DialogContent` primitive draws `w-full max-w-lg`. A consumer that raises
13
+ * the cap with `sm:max-w-2xl` or wider makes the dialog exactly as wide as any
14
+ * window narrower than that cap, at x = 0, edge to edge. With this class the
15
+ * dialog is the smaller of its max width and the window less 2rem, and the
16
+ * primitive's `left-[50%]` plus `translate-x-[-50%]` centres it with an equal
17
+ * 1rem either side. Above the max width plus 2rem the dialog is exactly as wide
18
+ * as before.
19
+ *
20
+ * `100%`, not `100vw`: the dialog is `fixed`, so `100%` is its containing
21
+ * block, the viewport less any classic scrollbar, the same box `left-[50%]`
22
+ * centres in. `100vw` counts the scrollbar, so each gutter would lose half of
23
+ * the scrollbar's width.
24
+ *
25
+ * `sm:` only: below `sm` a dialog is the phone sheet, which this leaves alone.
26
+ * A dialog that keeps its own viewport-relative cap (`sm:max-w-[95vw]
27
+ * sm:w-full`, the `full` size) does not read it.
28
+ *
29
+ * The class string is literal here so Tailwind's source scan emits it; an app
30
+ * that `@source`-scans this package gets the rule. A site writes its wide cap
31
+ * and this constant together (`` `sm:max-w-5xl ${DIALOG_VIEWPORT_GUTTER}` ``,
32
+ * or the constant as an unconditional `cn()` argument), never a gutter literal
33
+ * of its own. The census
34
+ * `packages/components/src/__tests__/dialogViewportGutterCensus-12603.test.ts`
35
+ * enumerates every wide `DialogContent` and fails one that does not.
36
+ */
37
+ export declare const DIALOG_VIEWPORT_GUTTER = "sm:w-[calc(100%-2rem)]";
@@ -0,0 +1,63 @@
1
+ import { EmptyState } from '@objectstack/spec/ui';
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ /**
10
+ * The one link an empty state names (objectui#12270).
11
+ *
12
+ * `@objectstack/spec`'s `EmptyStateSchema` declares an optional `link`,
13
+ * `{ label, url }`: the one next step an empty list names, such as the how-to
14
+ * page that explains what belongs here. The shape is taken by reference, so the
15
+ * link reaches every empty state that shape reaches, and each of them draws it
16
+ * through this component, ⛔ never a copy of its own:
17
+ *
18
+ * - a list view's `emptyState`, drawn by `@object-ui/plugin-list`'s `ListView`;
19
+ * - a list page's own `interfaceConfig.emptyState`, which that page hands the
20
+ * same `ListView`;
21
+ * - the `object-grid` block's `emptyState`, drawn by `@object-ui/plugin-grid`'s
22
+ * `ObjectGrid`.
23
+ *
24
+ * ## The click is the action runner's
25
+ *
26
+ * The link is followed as a `type: 'url'` action through the ActionRunner, so
27
+ * what a click may open is the runner's `classifyNavigationTarget` answer, the
28
+ * one answer every navigator in the app asks (ruling A+ on objectui#12191).
29
+ * ⛔ No scheme test lives here. An in-app path goes through the host's router,
30
+ * in place. An `http(s)://` address opens as the runner opens any external
31
+ * target. `mailto:` opens the mail client in place. Any other value is refused
32
+ * by the runner at the click, which reports its refusal (an error toast under
33
+ * the console's provider) and navigates nowhere. The spec judges the url at
34
+ * parse, except a template that opens with `${…}`, which it says is judged when
35
+ * it is clicked, after interpolation: the runner's click is that judgement, so
36
+ * nothing is judged here at render.
37
+ *
38
+ * Following a link is not feedback-worthy, so it raises no success toast, as
39
+ * the html tier's anchor and the dashboard's header link raise none.
40
+ *
41
+ * ## Without an ActionProvider
42
+ *
43
+ * `useAction()` then hands over a local runner with no navigation or toast
44
+ * handler. That runner hands an admitted in-app path back as `redirect`
45
+ * instead of navigating, and reports a refusal to nobody, so this component
46
+ * follows the redirect with a page navigation and logs the refusal.
47
+ */
48
+ import * as React from 'react';
49
+ /** The spec's empty-state link, `{ label, url }` (`EmptyStateSchema.link`). */
50
+ export type EmptyStateLinkSpec = NonNullable<EmptyState['link']>;
51
+ export interface EmptyStateLinkProps {
52
+ /** The authored `emptyState.link`. Absent, nothing is drawn. */
53
+ link?: EmptyStateLinkSpec;
54
+ /**
55
+ * The locale the `label` is resolved against when it is an inline locale
56
+ * map: the one the caller resolves the empty state's `title` and `message`
57
+ * against. A label that resolves to no text draws nothing.
58
+ */
59
+ displayLocale: string;
60
+ className?: string;
61
+ 'data-testid'?: string;
62
+ }
63
+ export declare function EmptyStateLink({ link, displayLocale, className, 'data-testid': testId, }: EmptyStateLinkProps): React.JSX.Element | null;
@@ -17,11 +17,10 @@ import * as React from "react";
17
17
  * and `operator` here is the builder's own dropdown vocabulary
18
18
  * (`FILTER_BUILDER_OPERATORS`), not the spec's `$`-tokens.
19
19
  *
20
- * DERIVED from `@object-ui/types`, never declared a second time (objectui#9306,
21
- * which settles objectui#6349's two parked name-authority rows). That declaration is
22
- * the one name authority for the row; this one takes every member from it
23
- * (`id`, required since objectui#8415, and `field`) and restates exactly two,
24
- * each a named extension:
20
+ * DERIVED from `@object-ui/types`' `FilterBuilderCondition` (objectui#9306).
21
+ * That declaration is the one name authority for the row; this one takes every
22
+ * member from it (`id`, required since objectui#8415, and `field`) and restates
23
+ * exactly two, each a named extension:
25
24
  *
26
25
  * - `operator` — {@link FilterBuilderOperator}: the protocol's ids, which is
27
26
  * what the authority declares, plus the opt-in `exists` / `notExists` the
@@ -33,15 +32,25 @@ import * as React from "react";
33
32
  * below that takes a row's value (`reshapeFilterValue`, `retypeFilterValue`,
34
33
  * `narrowFilterValueToOptions`, …) is typed on this member, so `any` here
35
34
  * would switch their checking off in silence.
35
+ *
36
+ * Its own NAME, not the authority's (objectui#6349, batch 12). Under the
37
+ * authority's name this was a second declaration of `FilterBuilderCondition`
38
+ * with a different shape: the two are assignable in neither direction (the
39
+ * operator blocks one way, the value's requiredness the other), so an
40
+ * auto-import picked one of two rows by package. `FilterBuilderCondition` now
41
+ * means the authored row in `@object-ui/types` only, and this specialisation
42
+ * is named for the builder that edits it.
36
43
  */
37
- export interface FilterBuilderCondition extends Omit<AuthoredFilterBuilderCondition, "operator" | "value"> {
44
+ export interface FilterBuilderRow extends Omit<AuthoredFilterBuilderCondition, "operator" | "value"> {
38
45
  operator: FilterBuilderOperator;
39
46
  value: string | number | boolean | (string | number | boolean)[];
40
47
  }
41
48
  /**
42
49
  * The group the FilterBuilder edits: one `logic` over FLAT rows.
43
50
  *
44
- * DERIVED from `@object-ui/types` like the row above (objectui#9306). Only
51
+ * DERIVED from `@object-ui/types`' `FilterGroup` like the row above
52
+ * (objectui#9306), and named apart from it for the same reason
53
+ * (objectui#6349, batch 12): `FilterGroup` means the authored group only. Only
45
54
  * `conditions` is restated, to hold THIS package's row; `id` and `logic` are
46
55
  * the authority's own. So `id` is OPTIONAL here, as it is there: nothing in
47
56
  * this builder reads a group's `id`, `onChange` hands back whatever group the
@@ -53,8 +62,8 @@ export interface FilterBuilderCondition extends Omit<AuthoredFilterBuilderCondit
53
62
  * and has no control that creates a group. The authority retired nested
54
63
  * sub-groups in the same change and its zod mirror refuses one by name.
55
64
  */
56
- export interface FilterGroup extends Omit<AuthoredFilterGroup, "conditions"> {
57
- conditions: FilterBuilderCondition[];
65
+ export interface FilterBuilderRowGroup extends Omit<AuthoredFilterGroup, "conditions"> {
66
+ conditions: FilterBuilderRow[];
58
67
  }
59
68
  export interface FilterBuilderProps {
60
69
  fields?: Array<{
@@ -72,8 +81,8 @@ export interface FilterBuilderProps {
72
81
  /** ID field on the referenced object (defaults to "id") */
73
82
  idField?: string;
74
83
  }>;
75
- value?: FilterGroup;
76
- onChange?: (value: FilterGroup) => void;
84
+ value?: FilterBuilderRowGroup;
85
+ onChange?: (value: FilterBuilderRowGroup) => void;
77
86
  className?: string;
78
87
  showClearAll?: boolean;
79
88
  /**
@@ -298,7 +307,7 @@ export declare function filterValueArity(operator: string): FilterValueArity;
298
307
  *
299
308
  * @internal exported for tests
300
309
  */
301
- export declare function reshapeFilterValue(value: FilterBuilderCondition["value"], nextOperator: string): FilterBuilderCondition["value"];
310
+ export declare function reshapeFilterValue(value: FilterBuilderRow["value"], nextOperator: string): FilterBuilderRow["value"];
302
311
  /**
303
312
  * The operator a row keeps after its FIELD is changed, given the operators the
304
313
  * NEW field offers (objectui#4768).
@@ -428,7 +437,7 @@ export declare function mountedOperatorValue(operator: string, offeredOperators:
428
437
  *
429
438
  * @internal exported for tests
430
439
  */
431
- export declare function retypeFilterValue(value: FilterBuilderCondition["value"], fieldType: string | undefined, operator: string): FilterBuilderCondition["value"];
440
+ export declare function retypeFilterValue(value: FilterBuilderRow["value"], fieldType: string | undefined, operator: string): FilterBuilderRow["value"];
432
441
  /** The shape of a `fields` entry that the option-domain helpers below read. */
433
442
  type OptionBearingField = {
434
443
  type?: string;
@@ -476,7 +485,7 @@ type OptionBearingField = {
476
485
  *
477
486
  * @internal exported for tests
478
487
  */
479
- export declare function narrowFilterValueToOptions(value: FilterBuilderCondition["value"], field: OptionBearingField | undefined, operator: string): FilterBuilderCondition["value"];
488
+ export declare function narrowFilterValueToOptions(value: FilterBuilderRow["value"], field: OptionBearingField | undefined, operator: string): FilterBuilderRow["value"];
480
489
  /**
481
490
  * Is this row's value FINISHED for the operator it carries — the arity-aware
482
491
  * reading of "the user has filled this in" (objectstack#8815).
@@ -528,10 +537,17 @@ export declare function narrowFilterValueToOptions(value: FilterBuilderCondition
528
537
  *
529
538
  * @internal exported for tests
530
539
  */
531
- export declare function isFilterValueComplete(operator: string, value: FilterBuilderCondition["value"] | undefined | null): boolean;
540
+ export declare function isFilterValueComplete(operator: string, value: FilterBuilderRow["value"] | undefined | null): boolean;
532
541
  /**
533
- * The operators the dropdown offers for a field of `fieldType`, given the
534
- * opt-in ids this instance was granted.
542
+ * The operators a row on a field of `fieldType` can HOLD, given the opt-in ids
543
+ * this instance was granted — every operator the dropdown can draw for it.
544
+ *
545
+ * What the dropdown OFFERS a fresh choice is this set minus the nullness pair
546
+ * on a type that cannot tell "empty" from "null" (objectui#11810, see
547
+ * {@link offeredOperatorsForRow}). That narrowing is deliberately NOT applied
548
+ * here: callers outside this file (`app-shell`'s dataset read-back,
549
+ * `plugin-list`'s parity pin) ask "can the builder hold this row", and a
550
+ * stored `is_null` on a `select` column still can.
535
551
  *
536
552
  * A pure function rather than a closure so the selection rule — and above all
537
553
  * the {@link OPT_IN_OPERATORS} gate, whose whole job is to keep an operator OFF
@@ -557,9 +573,9 @@ interface LookupValuePickerProps {
557
573
  displayField?: string;
558
574
  idField?: string;
559
575
  };
560
- value: FilterBuilderCondition["value"];
576
+ value: FilterBuilderRow["value"];
561
577
  multiple: boolean;
562
- onChange: (value: FilterBuilderCondition["value"]) => void;
578
+ onChange: (value: FilterBuilderRow["value"]) => void;
563
579
  }
564
580
  declare function LookupValuePicker({ field, value, multiple, onChange }: LookupValuePickerProps): React.JSX.Element;
565
581
  export { FilterBuilder, LookupValuePicker };
@@ -47,5 +47,9 @@ export interface GroupingEditorProps {
47
47
  * Field options are filtered per row so a field can only appear in one level
48
48
  * at a time. The current row's selected field stays in its own dropdown so the
49
49
  * user sees the active selection.
50
+ *
51
+ * The field selector is the shared `Select` primitive, the control the Filter
52
+ * and Sort panels beside this one pick a field with, so the three panels look
53
+ * and behave alike (objectui#11865). It used to be a browser-native `<select>`.
50
54
  */
51
55
  export declare function GroupingEditor({ value, onChange, fieldOptions, maxLevels, className, labels, }: GroupingEditorProps): React.JSX.Element;
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ export interface HorizontalOverflowBoxProps extends React.HTMLAttributes<HTMLDivElement> {
3
+ /**
4
+ * Where the shadow is drawn: `edge` (default) overlays the box's right edge;
5
+ * `pinned` leaves it to the right-pinned cells inside, which carry
6
+ * `PINNED_RIGHT_SEAM_SHADOW_CLASSES` (`./use-horizontal-overflow`).
7
+ */
8
+ seam?: 'edge' | 'pinned';
9
+ /** Classes for the positioned wrapper around the scrolling box. */
10
+ outerClassName?: string;
11
+ }
12
+ /**
13
+ * A horizontally scrolling box with the shared overflow affordance. The
14
+ * scrolling element is the inner `div` (`className`, the ref and every other
15
+ * prop land on it); the edge shadow is its sibling, so it never scrolls away
16
+ * with the content.
17
+ */
18
+ export declare const HorizontalOverflowBox: React.ForwardRefExoticComponent<HorizontalOverflowBoxProps & React.RefAttributes<HTMLDivElement>>;
@@ -24,4 +24,8 @@ export * from './shimmer-skeleton.js';
24
24
  export * from './RecordTitleChip.js';
25
25
  export * from './refresh-indicator.js';
26
26
  export * from './view-states.js';
27
+ export * from './empty-state-link.js';
27
28
  export * from './mobile-dialog-content.js';
29
+ export * from './dialog-viewport-gutter.js';
30
+ export { HorizontalOverflowBox, type HorizontalOverflowBoxProps } from './horizontal-overflow.js';
31
+ export { PINNED_RIGHT_SEAM_SHADOW_CLASSES } from './use-horizontal-overflow.js';
@@ -26,6 +26,14 @@ export interface SortBuilderProps {
26
26
  fields?: Array<{
27
27
  value: string;
28
28
  label: string;
29
+ /**
30
+ * Listed but not choosable (objectui#11943). A disabled entry still names
31
+ * a row whose current field it is, so that row shows its label and can be
32
+ * changed or removed, but no row's dropdown lets the user pick it and
33
+ * "Add sort" never seeds it. "Add sort" seeds the first entry that is not
34
+ * disabled, and is itself disabled when every entry is.
35
+ */
36
+ disabled?: boolean;
29
37
  }>;
30
38
  value?: SortItem[];
31
39
  onChange?: (value: SortItem[]) => void;
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * The horizontal-overflow affordance every wide table shares (objectui#12708):
4
+ * a box that scrolls sideways says so with a right-edge shadow while columns
5
+ * remain to its right, and shows its scrollbar while the pointer is over it or
6
+ * focus is inside it. Built once, here, and used by `data-table`'s own scroll
7
+ * box, `ObjectGrid`'s grouped scroller and any table a page mounts itself.
8
+ */
9
+ /** Whether `box` can scroll further to the right (one pixel of slack for sub-pixel layouts). */
10
+ export declare function hasOverflowRight(box: Pick<HTMLElement, 'scrollLeft' | 'clientWidth' | 'scrollWidth'>): boolean;
11
+ /**
12
+ * Track {@link hasOverflowRight} for the box `ref` points at: read in a layout
13
+ * effect, so the first painted frame is already right, and again on every
14
+ * scroll and whenever the box or its content changes size. `false` while
15
+ * `disabled` (the box does not scroll on its own).
16
+ */
17
+ export declare function useHorizontalOverflow(ref: React.RefObject<HTMLElement | null>, { disabled }?: {
18
+ disabled?: boolean;
19
+ }): boolean;
20
+ /**
21
+ * Scrollbar colours for a box that scrolls: transparent until the pointer is
22
+ * over the box or focus is inside it, then the theme's muted foreground. The bar
23
+ * keeps its place, so nothing shifts when it appears. Marked important
24
+ * because a host may theme every scrollbar with an unlayered `*` rule (the
25
+ * console does), which outranks any layered utility.
26
+ */
27
+ export declare const HOVER_SCROLLBAR_CLASSES = "[scrollbar-width:thin]! [scrollbar-color:transparent_transparent]! hover:[scrollbar-color:var(--color-muted-foreground)_transparent]! focus-within:[scrollbar-color:var(--color-muted-foreground)_transparent]!";
28
+ /**
29
+ * Classes for a RIGHT-PINNED cell inside a {@link HorizontalOverflowBox} with
30
+ * `seam="pinned"`: the cell draws the shadow on its own left edge, the seam
31
+ * where the scrolling columns slide under it, while the box can scroll further
32
+ * right. Needs no measurement, because the pinned cell is where the seam is.
33
+ * Spelled out in full (as the edge overlay below is) so Tailwind's source scan
34
+ * sees every utility.
35
+ */
36
+ export declare const PINNED_RIGHT_SEAM_SHADOW_CLASSES = "before:pointer-events-none before:absolute before:inset-y-0 before:right-full before:w-6 before:bg-gradient-to-l before:from-foreground/10 before:to-transparent before:opacity-0 group-data-[overflow-right=true]/hscroll:before:opacity-100";
@@ -18,6 +18,11 @@ interface ProbeFn {
18
18
  length?: number;
19
19
  }>;
20
20
  }
21
+ /** What one read of a related list answered: the server total and its rows. */
22
+ interface ReadResult {
23
+ total: number;
24
+ rows: unknown[];
25
+ }
21
26
  /**
22
27
  * A list's own declared scope, as carried on the `record:related_list` node it
23
28
  * badges (objectui#4664 / objectstack#7118). Two probes over the SAME
@@ -32,6 +37,53 @@ declare function setCount(objectName: string, relField: string | undefined, pare
32
37
  * for the same key and caches the resulting number until invalidated.
33
38
  */
34
39
  declare function fetchCount(probe: ProbeFn, objectName: string, relField: string | undefined, parentId: string | undefined, filter?: CountScopeFilter, fields?: FieldContainerLike): Promise<number>;
40
+ /**
41
+ * objectui#13010 — the rail card's read: the first `limit` rows of the
42
+ * unfiltered related list and its total. Takes the rows a badge's read holds
43
+ * for this card's claim, joins a read in flight that asked for enough rows, or
44
+ * reads itself and registers that read in flight for a badge to join.
45
+ * Rejects when the read fails, so the card can show its error state.
46
+ *
47
+ * One exception to the registration: when a read with too FEW rows (a badge's
48
+ * one-row probe) is already in flight, this card's own read is NOT registered
49
+ * in flight. The too-small read stays the one a badge joins, and since only the
50
+ * current in-flight read installs a count, the card's own read installs none:
51
+ * the badge's read does.
52
+ *
53
+ * Contract: held rows are a HAND-OFF, not a cache. They are taken once (the
54
+ * first `fetchRows` that finds them removes them; a second call reads again),
55
+ * and they are dropped without being taken when the child object is
56
+ * invalidated (`invalidate` / `invalidateAll`, and the data-change bus that
57
+ * calls them), when the claim is released (`releaseRows`: the card unmounts or
58
+ * moves to a new scope), and when the read that fetched them was overtaken by
59
+ * either. The total is recorded in the store as `fetch` records it. A caller
60
+ * that must see data newer than the record open (a refresh) does not call this.
61
+ */
62
+ declare function fetchRows(probe: ProbeFn, objectName: string, relField: string, parentId: string, limit: number, fields?: FieldContainerLike): Promise<ReadResult>;
63
+ /**
64
+ * objectui#13010 — a rail card says, while it renders, that it will list the
65
+ * first `limit` rows of this unfiltered related list. Idempotent, so a render
66
+ * React repeats or discards claims nothing new.
67
+ *
68
+ * Contract: a claim reads nothing by itself. While it stands, a `fetch` of the
69
+ * same unfiltered (object, relField, parentId) key asks for the claimed rows
70
+ * (the largest `limit` claimed) with its count and holds them for one
71
+ * `fetchRows` to take. A probe with a declared filter never meets a claim. The
72
+ * claim stands until `releaseRows` (unmount or new scope) or `_reset`; an
73
+ * invalidation drops held rows but leaves the claim.
74
+ */
75
+ declare function claimRows(objectName: string, relField: string, parentId: string, limit: number): void;
76
+ /**
77
+ * objectui#13010 — the rail card is gone: its claim, the rows held for it and
78
+ * the read it started itself are dropped, so nothing outlives the record open.
79
+ *
80
+ * Contract: call it with the key a `claimRows` named, when the card unmounts
81
+ * or moves to a new scope (another parent or other entries). Afterwards a
82
+ * `fetch` of that key asks for one row again, untaken held rows are gone, and
83
+ * a later `fetchRows` does not join a read the card started itself. A read a
84
+ * badge started stays in flight for the badges that joined it.
85
+ */
86
+ declare function releaseRows(objectName: string, relField: string, parentId: string): void;
35
87
  /**
36
88
  * Invalidate every cached count that involves the given object. Called by
37
89
  * mutation paths (e.g. ObjectGrid's onBulkDelete callback, drawer save) so
@@ -70,6 +122,9 @@ export declare const RelatedCountStore: {
70
122
  get: typeof getCount;
71
123
  set: typeof setCount;
72
124
  fetch: typeof fetchCount;
125
+ fetchRows: typeof fetchRows;
126
+ claimRows: typeof claimRows;
127
+ releaseRows: typeof releaseRows;
73
128
  invalidate: typeof invalidate;
74
129
  invalidateAll: typeof invalidateAll;
75
130
  _reset: () => void;