@object-ui/components 17.6.0 → 17.7.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 (56) hide show
  1. package/CHANGELOG.md +8750 -0
  2. package/README.md +5 -3
  3. package/dist/custom/button-group.d.ts +1 -1
  4. package/dist/custom/combobox.d.ts +2 -4
  5. package/dist/custom/empty.d.ts +16 -4
  6. package/dist/custom/field.d.ts +1 -1
  7. package/dist/custom/filter-builder.d.ts +198 -66
  8. package/dist/custom/index.d.ts +0 -1
  9. package/dist/custom/item.d.ts +1 -1
  10. package/dist/custom/kbd.d.ts +1 -1
  11. package/dist/custom/mobile-dialog-content.d.ts +22 -1
  12. package/dist/custom/navigation-overlay.d.ts +105 -0
  13. package/dist/custom/refresh-indicator.d.ts +8 -2
  14. package/dist/custom/view-states.d.ts +74 -1
  15. package/dist/hooks/related-count-store.d.ts +26 -28
  16. package/dist/index.css +157 -363
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.js +6603 -5987
  19. package/dist/index.umd.cjs +10 -15
  20. package/dist/lib/close-label.d.ts +1 -2
  21. package/dist/lib/date-fns-locale.d.ts +41 -0
  22. package/dist/lib/lazy-icon.d.ts +32 -1
  23. package/dist/lib/lucide-record-icon-names.d.ts +30 -0
  24. package/dist/lib/utils.d.ts +113 -0
  25. package/dist/renderers/action/action-bar.d.ts +8 -36
  26. package/dist/renderers/action/action-button.d.ts +15 -4
  27. package/dist/renderers/action/action-group.d.ts +5 -4
  28. package/dist/renderers/action/action-icon.d.ts +12 -2
  29. package/dist/renderers/action/action-menu.d.ts +5 -2
  30. package/dist/renderers/action/auto-trigger.d.ts +19 -4
  31. package/dist/renderers/action/resolve-icon.d.ts +27 -2
  32. package/dist/renderers/action/static-params.d.ts +79 -0
  33. package/dist/renderers/action/visibility-gate.d.ts +11 -3
  34. package/dist/renderers/basic/elements.d.ts +3 -0
  35. package/dist/renderers/basic/html-elements.d.ts +7 -3
  36. package/dist/renderers/basic/readProps.d.ts +60 -0
  37. package/dist/renderers/complex/data-table.d.ts +31 -14
  38. package/dist/renderers/complex/dataTableBindDiagnostic.d.ts +187 -0
  39. package/dist/renderers/layout/box.d.ts +8 -0
  40. package/dist/renderers/layout/containers.d.ts +1 -10
  41. package/dist/renderers/layout/react-page.d.ts +4 -0
  42. package/dist/renderers/navigation/sidebar.d.ts +31 -0
  43. package/dist/ui/badge.d.ts +1 -1
  44. package/dist/ui/button.d.ts +1 -1
  45. package/dist/ui/calendar.d.ts +2 -1
  46. package/dist/ui/command.d.ts +7 -7
  47. package/dist/ui/index.d.ts +0 -1
  48. package/dist/ui/input-otp.d.ts +2 -2
  49. package/dist/ui/sheet.d.ts +1 -1
  50. package/dist/ui/sidebar.d.ts +2 -1
  51. package/package.json +13 -14
  52. package/dist/SchemaRenderer.d.ts +0 -4
  53. package/dist/custom/export-progress-dialog.d.ts +0 -43
  54. package/dist/hooks/use-export-job.d.ts +0 -34
  55. package/dist/ui/chart.d.ts +0 -56
  56. package/dist/ui/toast.d.ts +0 -22
@@ -42,8 +42,35 @@ export interface NavigationOverlayProps {
42
42
  mainContent?: React.ReactNode;
43
43
  /**
44
44
  * Popover trigger element (for popover mode).
45
+ *
46
+ * Rendered through Radix's `PopoverTrigger asChild`, so it is mounted HERE,
47
+ * inside the overlay's own subtree. That makes it the right carrier only for
48
+ * a host that has no element of its own to point at — a host wanting the
49
+ * popover to sit on an element it already rendered elsewhere (a grid row, a
50
+ * tree node, a gantt bar, a kanban card, a calendar event) must use
51
+ * {@link NavigationOverlayProps.popoverAnchorRef}, because re-rendering that
52
+ * element here would draw a SECOND copy of it in the overlay rather than
53
+ * anchor to the first.
45
54
  */
46
55
  popoverTrigger?: React.ReactNode;
56
+ /**
57
+ * The already-rendered element the popover should be anchored to
58
+ * (popover mode).
59
+ *
60
+ * ⭐ This is how the five list-type renderers honour `navigation.mode:
61
+ * 'popover'` after objectui#9299: the click handler stores
62
+ * `event.currentTarget` — the row / node / bar / card / event the user
63
+ * actually clicked — on this ref in the same turn that opens the overlay,
64
+ * and the popover is positioned against it through Radix's virtual-anchor
65
+ * shape (`Popper.Anchor virtualRef`). No element is re-rendered, so nothing
66
+ * is duplicated and the renderer keeps ownership of its own DOM.
67
+ *
68
+ * When neither this nor `popoverTrigger` is supplied the overlay falls back
69
+ * to a compact `Dialog`. ⛔ After objectui#9299 none of the five renderers is
70
+ * allowed to reach that fallback — it survives only for a host that genuinely
71
+ * has no anchor to offer.
72
+ */
73
+ popoverAnchorRef?: React.RefObject<HTMLElement | null>;
47
74
  /**
48
75
  * Optional handler invoked when the user clicks the "Expand to full page"
49
76
  * affordance in the drawer/modal header. Mirrors Linear / Notion / Airtable
@@ -72,7 +99,85 @@ export interface NavigationOverlayProps {
72
99
  * by the `width` prop / configured ceiling.
73
100
  */
74
101
  storageKey?: string;
102
+ /**
103
+ * Retired storage key whose value is migrated into `storageKey` once
104
+ * (drawer mode only).
105
+ *
106
+ * ⭐ objectui#9299 item 4: there used to be TWO drag-resize implementations
107
+ * with two different persisted keys and two different floors —
108
+ * `RecordDetailDrawer`'s `objectui.drawerWidth.OBJECT` and this component's
109
+ * `ov:STORAGE_KEY`. They are now one implementation, and a user who had
110
+ * already dragged a gantt / kanban / calendar drawer to their preferred width
111
+ * must keep that width rather than be silently reset to the default. So the
112
+ * first mount that finds no value under `storageKey` adopts the value found
113
+ * under this key, writes it forward, and REMOVES the retired entry — which is
114
+ * what makes the migration one-time rather than a permanent second read: a
115
+ * later double-click reset must not resurrect the old width.
116
+ *
117
+ * Build it with {@link legacyRecordDrawerWidthKey} rather than spelling the
118
+ * prefix again at a call site.
119
+ */
120
+ legacyStorageKey?: string;
75
121
  }
122
+ /**
123
+ * The one `storageKey` spelling every record overlay uses, keyed by object.
124
+ *
125
+ * `NavigationOverlay` prefixes it with `ov:` on the way to `localStorage`, so
126
+ * the stored key is `ov:drawer-width:OBJECT`. The console's own object page
127
+ * (`app-shell`'s `ObjectView`) has always used this spelling; after
128
+ * objectui#9299 the five list-type renderers use it too, so one object has one
129
+ * remembered overlay width no matter which view type the user resized it on.
130
+ */
131
+ export declare function recordOverlayWidthStorageKey(objectName: string): string;
132
+ /**
133
+ * The RETIRED per-object key `RecordDetailDrawer` persisted its width under
134
+ * before objectui#9299. Read once, migrated forward, then removed — see
135
+ * {@link NavigationOverlayProps.legacyStorageKey}. ⛔ Nothing writes it.
136
+ */
137
+ export declare function legacyRecordDrawerWidthKey(objectName: string): string;
138
+ /**
139
+ * Remember the element a click landed on, so `popover` mode can anchor to it.
140
+ *
141
+ * ⭐ objectui#9299 item 3: `popover` is anchored to the clicked element — the
142
+ * row, node, bar, card or event the user actually pressed. That element is
143
+ * rendered by the RENDERER, not by this overlay, so the overlay cannot find it
144
+ * on its own, and `popoverTrigger` (which MOUNTS an element of its own inside
145
+ * the overlay) is the wrong carrier for it — passing the row there would draw
146
+ * a second copy of the row instead of pointing at the first.
147
+ *
148
+ * Two carriers, same recorder. Spread `anchorCaptureProps` on the renderer's
149
+ * own container when the click reaches it without a DOM event (a chart, a
150
+ * calendar grid); call `captureAnchor(event)` directly at a click site that
151
+ * already HAS one (a kanban card, a tree row). Either way hand `anchorRef` to
152
+ * {@link NavigationOverlayProps.popoverAnchorRef}. The container form uses the
153
+ * capture phase deliberately: it runs before the bubbling `onClick` that opens
154
+ * the overlay, so by the time the popover renders the anchor is recorded.
155
+ *
156
+ * It reads `target`, not `currentTarget`, because the handler sits on the
157
+ * CONTAINER: `currentTarget` there is the whole view, and a popover anchored
158
+ * to the whole board is not anchored to anything. `target` is the deepest
159
+ * element under the pointer — the bar, the card, the cell — which is what the
160
+ * user pointed at.
161
+ *
162
+ * ⚠️ Read synchronously, which is what this does: React nulls its event fields
163
+ * out once the handler returns, so stashing the event rather than the element
164
+ * would hand the overlay a dead reference.
165
+ *
166
+ * Clicks that do not open anything (a toolbar button, a filter) also land here
167
+ * and are harmless: `anchorRef` is only ever read while the popover is open,
168
+ * and the click that opened it is by construction the last one recorded.
169
+ */
170
+ export declare function useOverlayAnchor(): {
171
+ anchorRef: React.RefObject<HTMLElement | null>;
172
+ captureAnchor: (event: {
173
+ target?: unknown;
174
+ } | null | undefined) => void;
175
+ anchorCaptureProps: {
176
+ onClickCapture: (event: {
177
+ target?: unknown;
178
+ }) => void;
179
+ };
180
+ };
76
181
  /**
77
182
  * NavigationOverlay — renders record detail in the configured overlay mode.
78
183
  *
@@ -9,8 +9,14 @@ import * as React from "react";
9
9
  export interface RefreshIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {
10
10
  /** Whether the indicator is active. When false, nothing is rendered. */
11
11
  active: boolean;
12
- /** Accessible label. Defaults to "Refreshing". */
13
- ariaLabel?: string;
12
+ /**
13
+ * Accessible name of the progress bar, rendered as its `aria-label`.
14
+ * Required, with no default: the caller passes a string from its own
15
+ * translation layer, so the name a screen reader announces is in the active
16
+ * locale. An English default here reached every non-English screen-reader
17
+ * user through the callers that passed nothing (objectui#10580).
18
+ */
19
+ ariaLabel: string;
14
20
  }
15
21
  /**
16
22
  * Thin, absolutely-positioned indeterminate progress bar that animates across
@@ -44,8 +44,54 @@ interface DataEmptyStateProps extends React.ComponentProps<"div"> {
44
44
  /** Optional action rendered below the description */
45
45
  action?: React.ReactNode;
46
46
  }
47
+ /**
48
+ * `role` defaults to `"status"` (objectui#7132).
49
+ *
50
+ * The sibling states in this file each declare what they are — `DataLoadingState`
51
+ * is `role="status"`, `DataErrorState` is `role="alert"` — and the empty state
52
+ * alone declared nothing, so an empty box and a failed box were the same node
53
+ * shape to a screen reader and to any structural test. That is the exact
54
+ * property both the hotcrm#1212 (objectui#7063) and hotcrm#1247 (objectui#7064)
55
+ * rulings named first: an empty state must be *distinguishable from a load
56
+ * failure at a glance*.
57
+ *
58
+ * With no default, every surface that wanted the property had to type it at its
59
+ * own call site, and four independently did — `plugin-kanban`'s empty board,
60
+ * `plugin-dashboard`'s `WidgetEmptyState`, and `plugin-charts`' `ObjectChart` —
61
+ * while `plugin-list`, `plugin-detail`'s two timelines and the `ui:empty`
62
+ * renderer silently did not. Four hand-copies of one line is the per-app tax
63
+ * objectstack#13848 rules against, paid at the package level.
64
+ *
65
+ * It is a DEFAULT, not a fixed attribute: `role` is spread from `props` below,
66
+ * so a call site keeps the last word. That is what makes this change inert for
67
+ * the two ruled surfaces — both already pass `role="status"` explicitly and
68
+ * receive the identical attribute either way — and it is what lets a call site
69
+ * rendering something that is NOT empty say so.
70
+ *
71
+ * The call site that needed that last word was `plugin-list`'s load-failure
72
+ * panel, which borrowed this component for its layout. Since objectui#7143 it
73
+ * no longer does: `DataErrorState` grew the icon props it was missing and the
74
+ * panel is drawn by the component named for what it is. The default stays a
75
+ * default for the same reason it always was — a borrow can recur, and a fixed
76
+ * attribute would announce the next one as a routine status.
77
+ */
47
78
  declare function DataEmptyState({ className, icon, illustration, showIcon, iconWrapperClassName, title, description, action, children, ...props }: DataEmptyStateProps): React.JSX.Element;
48
79
  interface DataErrorStateProps extends React.ComponentProps<"div"> {
80
+ /** Icon rendered above the title */
81
+ icon?: React.ReactNode;
82
+ /**
83
+ * When false, the icon container is omitted entirely. Useful for
84
+ * banner-style failures that should not draw a glyph at all. Defaults to
85
+ * true.
86
+ */
87
+ showIcon?: boolean;
88
+ /**
89
+ * Override class on the icon wrapper. By default the wrapper renders as a
90
+ * small destructive-tinted rounded square (`size-10 rounded-lg
91
+ * bg-destructive/10`). Pass `""` to strip that styling and render the icon
92
+ * raw, or extend the look (e.g. larger size).
93
+ */
94
+ iconWrapperClassName?: string;
49
95
  title?: string;
50
96
  /** Error message or description */
51
97
  message?: string;
@@ -54,5 +100,32 @@ interface DataErrorStateProps extends React.ComponentProps<"div"> {
54
100
  /** Label for the retry button */
55
101
  retryLabel?: string;
56
102
  }
57
- declare function DataErrorState({ className, title, message, onRetry, retryLabel, children, ...props }: DataErrorStateProps): React.JSX.Element;
103
+ /**
104
+ * `icon` / `showIcon` / `iconWrapperClassName` MIRROR `DataEmptyState` above —
105
+ * same names, same types, same default semantics, including
106
+ * `iconWrapperClassName` REPLACING the wrapper's default class rather than
107
+ * merging with it, so `""` renders the icon raw (objectui#7143). A second
108
+ * spelling of the same three ideas is the defect the mirror exists to prevent;
109
+ * the only intended difference is the default class the `??` falls back to,
110
+ * which stays this component's own destructive square.
111
+ *
112
+ * They exist because `plugin-list` rendered its load FAILURE through
113
+ * `DataEmptyState` — the component named for the *empty* case — for want of
114
+ * them: this one hardcoded its glyph, so the one panel that needed to draw a
115
+ * network failure differently from a permission denial could only get that from
116
+ * the wrong component. objectui#7132 fixed the accessibility half of that
117
+ * collision (`role`) and deliberately left the structural half here.
118
+ *
119
+ * Deliberately NOT mirrored: `illustration` — an empty state's product-feel hero
120
+ * has no failure analogue — and `action`, because this component already spells
121
+ * its affordance as `onRetry` / `retryLabel`, with `children` for a call site
122
+ * that needs to render its own control.
123
+ *
124
+ * The icon wrapper carries `data-slot="data-error-state-icon"`, mirroring the
125
+ * empty state's `data-empty-state-icon`. Not a prop, and not in the ruling's
126
+ * list of three — it is here so the wrapper `iconWrapperClassName` now governs
127
+ * can be named by a test and a stylesheet, and so migrating a call site off
128
+ * `DataEmptyState` renames that identifier instead of dropping it.
129
+ */
130
+ declare function DataErrorState({ className, icon, showIcon, iconWrapperClassName, title, message, onRetry, retryLabel, children, ...props }: DataErrorStateProps): React.JSX.Element;
58
131
  export { DataLoadingState, DataEmptyState, DataEmptyState as EmptyState, DataErrorState, type DataLoadingStateProps, type DataEmptyStateProps, type DataEmptyStateProps as EmptyStateProps, type DataErrorStateProps, };
@@ -1,29 +1,14 @@
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
- * Module-scoped store for related-list counts displayed in tab badges
9
- * (e.g. "Contacts (12)" on an Account detail). The store deduplicates
10
- * concurrent probes for the same key, lets the renderer subscribe with
11
- * `useSyncExternalStore`, and exposes invalidation hooks so other parts
12
- * of the runtime (bulk delete, inline create, optimistic update) can
13
- * keep the badges in sync without a full page refetch.
14
- *
15
- * Design notes:
16
- * - One global Map keyed by `${objectName}::${relField}::${parentId}` so
17
- * a count fetched by one tab strip is reused by every other consumer.
18
- * - Subscribers receive *all* keyspace changes; coarse-grained but
19
- * badges are cheap to re-render and avoids per-key subscription noise.
20
- * - We deliberately avoid Zustand here — the surface area is one Map +
21
- * an emit() — and the React binding uses the built-in
22
- * `useSyncExternalStore` so we don't grow the dependency graph.
23
- */
1
+ import { FieldContainerLike } from '@object-ui/core';
24
2
  interface ProbeFn {
25
3
  (objectName: string, query: {
26
- $filter?: Record<string, unknown>;
4
+ /**
5
+ * The parent scope alone (a MongoDB-style object, as it always was), or
6
+ * — once a list declares its own scope — the ObjectQL AST node
7
+ * `mergeFilterNodes` lowers the pair to. Both arms are what the ROW
8
+ * query on the same list already sends, so this union is not a new
9
+ * dialect: it is the same two shapes `RelatedList` puts on the wire.
10
+ */
11
+ $filter?: Record<string, unknown> | unknown[];
27
12
  $top?: number;
28
13
  $count?: boolean;
29
14
  }): Promise<{
@@ -33,13 +18,20 @@ interface ProbeFn {
33
18
  length?: number;
34
19
  }>;
35
20
  }
36
- declare function getCount(objectName: string, relField: string | undefined, parentId: string | undefined): number | undefined;
37
- declare function setCount(objectName: string, relField: string | undefined, parentId: string | undefined, value: number): void;
21
+ /**
22
+ * A list's own declared scope, as carried on the `record:related_list` node it
23
+ * badges (objectui#4664 / objectstack#7118). Two probes over the SAME
24
+ * (object, relField, parent) triple are different questions when their scopes
25
+ * differ, so it is part of the cache identity below.
26
+ */
27
+ export type CountScopeFilter = Record<string, any> | any[];
28
+ declare function getCount(objectName: string, relField: string | undefined, parentId: string | undefined, filter?: CountScopeFilter): number | undefined;
29
+ declare function setCount(objectName: string, relField: string | undefined, parentId: string | undefined, value: number, filter?: CountScopeFilter): void;
38
30
  /**
39
31
  * Probe a count via the supplied finder. Deduplicates concurrent requests
40
32
  * for the same key and caches the resulting number until invalidated.
41
33
  */
42
- declare function fetchCount(probe: ProbeFn, objectName: string, relField: string | undefined, parentId: string | undefined): Promise<number>;
34
+ declare function fetchCount(probe: ProbeFn, objectName: string, relField: string | undefined, parentId: string | undefined, filter?: CountScopeFilter, fields?: FieldContainerLike): Promise<number>;
43
35
  /**
44
36
  * Invalidate every cached count that involves the given object. Called by
45
37
  * mutation paths (e.g. ObjectGrid's onBulkDelete callback, drawer save) so
@@ -55,7 +47,7 @@ declare function invalidateAll(): void;
55
47
  * (object, relField, parentId) triple. Returns `undefined` while the
56
48
  * probe is in flight or before the first request.
57
49
  */
58
- export declare function useRelatedCount(objectName: string | undefined, relField: string | undefined, parentId: string | undefined): number | undefined;
50
+ export declare function useRelatedCount(objectName: string | undefined, relField: string | undefined, parentId: string | undefined, filter?: CountScopeFilter): number | undefined;
59
51
  /**
60
52
  * Subscribe to the store's monotonic version. Put it in an effect dependency
61
53
  * array to RE-RUN count probes after an invalidation (#2269) — invalidate
@@ -67,6 +59,12 @@ export declare function useRelatedCountVersion(): number;
67
59
  /**
68
60
  * Imperative store API for non-React callers (mutation handlers, tests).
69
61
  * Prefer `useRelatedCount` in components.
62
+ *
63
+ * `fetch`'s last parameter is the CHILD object's field defs. It is optional
64
+ * because a caller that cannot see metadata must still be able to probe — the
65
+ * seam then compiles the historical equality wire — but a caller that CAN see
66
+ * them owes them: that is the difference between a badge and no badge on a
67
+ * multi-value relationship (objectui#8882).
70
68
  */
71
69
  export declare const RelatedCountStore: {
72
70
  get: typeof getCount;