@rebasepro/admin 0.13.0 → 0.13.1-canary.g3660bd5

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 (103) hide show
  1. package/dist/{CollectionEditorDialog-CjXkWZ64.js → CollectionEditorDialog-MODRemil.js} +3 -3
  2. package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-MODRemil.js.map} +1 -1
  3. package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-Bt-VM0zQ.js} +2 -2
  4. package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-Bt-VM0zQ.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-D54P9o8O.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-BsLTNoqX.js.map → RouterCollectionsStudioView-D54P9o8O.js.map} +1 -1
  7. package/dist/collection_editor_ui.js +4 -4
  8. package/dist/components/AdditionalFieldValue.d.ts +25 -0
  9. package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +16 -0
  10. package/dist/components/CollectionViewBinding/SlotValue.d.ts +37 -0
  11. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +5 -3
  12. package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +12 -0
  13. package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +29 -4
  14. package/dist/components/DetailViewBinding.d.ts +6 -0
  15. package/dist/components/DrawerNavigationGroup.d.ts +5 -4
  16. package/dist/components/DrawerNavigationItem.d.ts +4 -2
  17. package/dist/components/EditViewBinding.d.ts +7 -1
  18. package/dist/components/EntityDisplayHeader.d.ts +53 -0
  19. package/dist/components/EntityIdentityBar.d.ts +20 -1
  20. package/dist/components/EntityViewBinding.d.ts +60 -6
  21. package/dist/components/RebaseLayout.d.ts +1 -1
  22. package/dist/components/RebaseShell.d.ts +1 -1
  23. package/dist/components/app/Scaffold.d.ts +9 -3
  24. package/dist/components/index.d.ts +2 -0
  25. package/dist/{export-J79rMoNj.js → export-Bl1V7lKO.js} +2 -2
  26. package/dist/{export-J79rMoNj.js.map → export-Bl1V7lKO.js.map} +1 -1
  27. package/dist/form/components/FieldBlock.d.ts +47 -4
  28. package/dist/form/components/FormRail.d.ts +13 -0
  29. package/dist/form/components/FormSections.d.ts +15 -1
  30. package/dist/form/components/useRailVisible.d.ts +5 -4
  31. package/dist/{history-D7L_JlLO.js → history-DN_dzFAF.js} +4 -4
  32. package/dist/{history-D7L_JlLO.js.map → history-DN_dzFAF.js.map} +1 -1
  33. package/dist/hooks/useEntityDisplay.d.ts +102 -0
  34. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -9
  35. package/dist/{import-BvYl--U4.js → import-BJXrHdFv.js} +3 -3
  36. package/dist/{import-BvYl--U4.js.map → import-BJXrHdFv.js.map} +1 -1
  37. package/dist/index.js +149 -51
  38. package/dist/index.js.map +1 -1
  39. package/dist/preview/compact_text.d.ts +23 -0
  40. package/dist/preview/components/BooleanPreview.d.ts +3 -1
  41. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  42. package/dist/preview/property_previews/ArrayOneOfPreview.d.ts +1 -1
  43. package/dist/preview/property_previews/ArrayPropertyPreview.d.ts +1 -1
  44. package/dist/preview/property_previews/MapPropertyPreview.d.ts +1 -1
  45. package/dist/preview/property_previews/StringPropertyPreview.d.ts +1 -1
  46. package/dist/routes/RebaseRoute.d.ts +1 -1
  47. package/dist/types/components/PropertyPreviewProps.d.ts +19 -0
  48. package/dist/util/number_format.d.ts +14 -0
  49. package/dist/util/previews.d.ts +9 -2
  50. package/dist/{util-GiwPgxnL.js → util-EXfeGzbH.js} +8142 -7353
  51. package/dist/util-EXfeGzbH.js.map +1 -0
  52. package/package.json +9 -9
  53. package/src/components/AdditionalFieldValue.tsx +68 -0
  54. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +5 -6
  55. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +9 -27
  56. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +11 -24
  57. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +26 -6
  58. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +10 -33
  59. package/src/components/CollectionViewBinding/SlotValue.tsx +60 -0
  60. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +7 -5
  61. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
  62. package/src/components/CollectionViewBinding/SplitListView.tsx +14 -1
  63. package/src/components/CollectionViewBinding/useBoardDataController.tsx +12 -3
  64. package/src/components/CollectionViewBinding/usePreviewSlots.ts +115 -48
  65. package/src/components/DefaultDrawer.tsx +19 -23
  66. package/src/components/DetailViewBinding.tsx +74 -54
  67. package/src/components/DrawerNavigationGroup.tsx +4 -7
  68. package/src/components/DrawerNavigationItem.tsx +23 -4
  69. package/src/components/EditViewBinding.tsx +54 -33
  70. package/src/components/EntityDisplayHeader.tsx +170 -0
  71. package/src/components/EntityIdentityBar.tsx +49 -5
  72. package/src/components/EntityPreviewBinding.tsx +54 -17
  73. package/src/components/EntityViewBinding.tsx +341 -41
  74. package/src/components/InlineEntityPreview.tsx +10 -10
  75. package/src/components/RebaseLayout.tsx +2 -2
  76. package/src/components/RebaseShell.tsx +2 -2
  77. package/src/components/app/Scaffold.tsx +112 -10
  78. package/src/components/index.ts +3 -0
  79. package/src/form/EntityForm.tsx +28 -33
  80. package/src/form/components/FieldBlock.tsx +65 -8
  81. package/src/form/components/FormRail.tsx +19 -2
  82. package/src/form/components/FormSections.tsx +32 -5
  83. package/src/form/components/StorageUploadProgress.tsx +9 -3
  84. package/src/form/components/useRailVisible.tsx +5 -4
  85. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -1
  86. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
  87. package/src/hooks/useEntityDisplay.tsx +262 -0
  88. package/src/hooks/useEntityDisplayTitle.tsx +27 -38
  89. package/src/preview/PropertyPreview.tsx +18 -2
  90. package/src/preview/compact_text.ts +68 -0
  91. package/src/preview/components/BooleanPreview.tsx +5 -2
  92. package/src/preview/components/StorageThumbnail.tsx +5 -2
  93. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +26 -1
  94. package/src/preview/property_previews/ArrayOneOfPreview.tsx +10 -1
  95. package/src/preview/property_previews/ArrayPropertyPreview.tsx +10 -1
  96. package/src/preview/property_previews/MapPropertyPreview.tsx +52 -1
  97. package/src/preview/property_previews/NumberPropertyPreview.tsx +9 -1
  98. package/src/preview/property_previews/StringPropertyPreview.tsx +8 -5
  99. package/src/routes/RebaseRoute.tsx +62 -4
  100. package/src/types/components/PropertyPreviewProps.tsx +21 -0
  101. package/src/util/number_format.ts +46 -0
  102. package/src/util/previews.ts +8 -55
  103. package/dist/util-GiwPgxnL.js.map +0 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/admin",
3
3
  "type": "module",
4
- "version": "0.13.0",
4
+ "version": "0.13.1-canary.g3660bd5",
5
5
  "description": "Rebase CMS — content management views, forms, and routing",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -83,14 +83,14 @@
83
83
  "react-dropzone": "^19.1.1",
84
84
  "react-use-measure": "^2.1.7",
85
85
  "zod": "^4.4.3",
86
- "@rebasepro/admin-types": "0.13.0",
87
- "@rebasepro/app": "0.13.0",
88
- "@rebasepro/common": "0.13.0",
89
- "@rebasepro/inference": "0.13.0",
90
- "@rebasepro/forms": "0.13.0",
91
- "@rebasepro/ui": "0.13.0",
92
- "@rebasepro/types": "0.13.0",
93
- "@rebasepro/utils": "0.13.0"
86
+ "@rebasepro/admin-types": "0.13.1-canary.g3660bd5",
87
+ "@rebasepro/app": "0.13.1-canary.g3660bd5",
88
+ "@rebasepro/common": "0.13.1-canary.g3660bd5",
89
+ "@rebasepro/types": "0.13.1-canary.g3660bd5",
90
+ "@rebasepro/ui": "0.13.1-canary.g3660bd5",
91
+ "@rebasepro/inference": "0.13.1-canary.g3660bd5",
92
+ "@rebasepro/utils": "0.13.1-canary.g3660bd5",
93
+ "@rebasepro/forms": "0.13.1-canary.g3660bd5"
94
94
  },
95
95
  "peerDependencies": {
96
96
  "react": ">=19.2.7",
@@ -0,0 +1,68 @@
1
+ import type { AdditionalFieldDelegate, RebaseContext } from "@rebasepro/admin-types";
2
+ import type { Entity } from "@rebasepro/types";
3
+ import React, { useEffect, useState } from "react";
4
+
5
+ export interface AdditionalFieldValueProps<M extends Record<string, unknown>> {
6
+ field: AdditionalFieldDelegate<M>;
7
+ entity: Entity<M>;
8
+ context: RebaseContext;
9
+ }
10
+
11
+ /**
12
+ * Renders an `additionalFields` entry that supplies a `value` rather than a
13
+ * `Builder`.
14
+ *
15
+ * `value` is typed `string | number | Promise<string | number>`, and the promise
16
+ * arm has been part of that type from the start — the CSV export awaits it
17
+ * correctly. Every *renderer* called `.toString()` on the result instead, so an
18
+ * async value drew the literal text `[object Promise]` in the cell: a shape the
19
+ * type promised, that worked in one of its two consumers, and failed silently in
20
+ * the other. That is what made the obvious port of a FireCMS column — one that
21
+ * fetches a title from a subcollection — look supported and not be.
22
+ *
23
+ * Three call sites had the same three lines copied between them (the table cell,
24
+ * the form and the read-only record), which is why the bug reached all three.
25
+ * Now there is one.
26
+ */
27
+ export function AdditionalFieldValue<M extends Record<string, unknown>>({
28
+ field,
29
+ entity,
30
+ context
31
+ }: AdditionalFieldValueProps<M>) {
32
+
33
+ const [resolved, setResolved] = useState<string | number | undefined>(() => {
34
+ const value = field.value?.({ entity, context });
35
+ // A synchronous value is available on the first render, so it never
36
+ // flashes empty on its way in.
37
+ return isPromise(value) ? undefined : value;
38
+ });
39
+
40
+ useEffect(() => {
41
+ const value = field.value?.({ entity, context });
42
+ if (!isPromise(value)) {
43
+ setResolved(value);
44
+ return;
45
+ }
46
+
47
+ // The row may scroll out, or the record change, before this lands.
48
+ let live = true;
49
+ value.then(
50
+ result => {
51
+ if (live) setResolved(result);
52
+ },
53
+ error => {
54
+ if (live) setResolved(undefined);
55
+ console.warn(`[rebase] Additional field "${field.key}" failed to resolve:`, error);
56
+ }
57
+ );
58
+ return () => {
59
+ live = false;
60
+ };
61
+ }, [field, entity, context]);
62
+
63
+ return <>{resolved === undefined || resolved === null ? null : String(resolved)}</>;
64
+ }
65
+
66
+ function isPromise(value: unknown): value is Promise<string | number | undefined> {
67
+ return typeof (value as Promise<unknown> | undefined)?.then === "function";
68
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AdditionalFieldDelegate, AdminCollection } from "@rebasepro/admin-types";
2
+ import { AdditionalFieldValue } from "../AdditionalFieldValue";
2
3
  import React, { useCallback, useMemo, useRef } from "react";
3
4
  import { Entity, User, Property } from "@rebasepro/types";
4
5
  import { CollectionSize, RebaseContext } from "@rebasepro/admin-types";
@@ -214,12 +215,10 @@ export const CollectionTableBinding = function CollectionTableBinding<M extends
214
215
 
215
216
  const child: React.ReactNode = Builder
216
217
  ? <Builder entity={entity} context={context}/>
217
- : <>
218
- {additionalField.value?.({
219
- entity,
220
- context: context as RebaseContext
221
- })?.toString()}
222
- </>;
218
+ : <AdditionalFieldValue
219
+ field={additionalField}
220
+ entity={entity}
221
+ context={context as RebaseContext}/>;
223
222
 
224
223
  return (
225
224
  <EntityTableCell
@@ -6,7 +6,8 @@ import { PropertyPreview } from "../../preview";
6
6
  import { useAuthController, useCustomizationController } from "@rebasepro/app";
7
7
  import { IconForView } from "@rebasepro/app";
8
8
  import { BoardItemViewProps } from "@rebasepro/ui";
9
- import { useCollectionSlotKeys, resolveEntitySlots } from "./usePreviewSlots";
9
+ import { useCollectionSlotKeys, useEntitySlots } from "./usePreviewSlots";
10
+ import { SlotValue } from "./SlotValue";
10
11
  import type { AdminCollection } from "@rebasepro/admin-types";
11
12
 
12
13
  export type BoardCardBindingProps<M extends Record<string, unknown> = Record<string, unknown>> = BoardItemViewProps<Entity<M>> & {
@@ -46,13 +47,10 @@ function BoardCardBindingInner<M extends Record<string, unknown> = Record<string
46
47
  customizationController.propertyConfigs
47
48
  );
48
49
 
49
- const slots = useMemo(
50
- () => resolveEntitySlots(
51
- entity as Entity<Record<string, unknown>>,
52
- collection as AdminCollection<Record<string, unknown>>,
53
- slotKeys
54
- ),
55
- [entity, collection, slotKeys]
50
+ const slots = useEntitySlots(
51
+ entity as Entity<Record<string, unknown>>,
52
+ collection as AdminCollection<Record<string, unknown>>,
53
+ slotKeys
56
54
  );
57
55
 
58
56
  const handleClick = useCallback((e: React.MouseEvent) => {
@@ -126,13 +124,7 @@ function BoardCardBindingInner<M extends Record<string, unknown> = Record<string
126
124
  selectionEnabled && "group-hover/card:opacity-30",
127
125
  selected && "opacity-0"
128
126
  )}>
129
- <PropertyPreview
130
- property={slots.image.property}
131
- propertyKey={slots.image.propertyKey}
132
- size="small"
133
- value={slots.image.value}
134
- fill={true}
135
- />
127
+ <SlotValue slot={slots.image} size="small" fill={true}/>
136
128
  </div>
137
129
  ) : (
138
130
  <div className={cls(
@@ -177,12 +169,7 @@ function BoardCardBindingInner<M extends Record<string, unknown> = Record<string
177
169
  {/* Title slot */}
178
170
  <div className="line-clamp-2 text-sm font-medium">
179
171
  {slots.title ? (
180
- <PropertyPreview
181
- propertyKey={slots.title.propertyKey}
182
- value={slots.title.value}
183
- property={slots.title.property}
184
- size="small"
185
- />
172
+ <SlotValue slot={slots.title} size="small"/>
186
173
  ) : (
187
174
  <span className="text-surface-500">{entity.id}</span>
188
175
  )}
@@ -193,12 +180,7 @@ function BoardCardBindingInner<M extends Record<string, unknown> = Record<string
193
180
  {typeof slots.subtitle.value === "string" ? (
194
181
  <Markdown source={slots.subtitle.value} size="small" />
195
182
  ) : (
196
- <PropertyPreview
197
- propertyKey={slots.subtitle.propertyKey}
198
- value={slots.subtitle.value}
199
- property={slots.subtitle.property}
200
- size="small"
201
- />
183
+ <SlotValue slot={slots.subtitle} size="small"/>
202
184
  )}
203
185
  </div>
204
186
  ) : (
@@ -24,7 +24,8 @@ import { getEntityPreviewKeys } from "../../util/previews";
24
24
  import { IconForView } from "@rebasepro/app";
25
25
  import { getIcon } from "@rebasepro/app";
26
26
  import { getValueInPath } from "@rebasepro/utils";
27
- import { useCollectionSlotKeys, resolveEntitySlots, type CollectionSlotKeys } from "./usePreviewSlots";
27
+ import { useCollectionSlotKeys, useEntitySlots, type CollectionSlotKeys } from "./usePreviewSlots";
28
+ import { SlotValue } from "./SlotValue";
28
29
  import { useAdminContext } from "../../hooks/useAdminContext";
29
30
  import { resolveEntityAction } from "../../util/resolutions";
30
31
 
@@ -628,8 +629,11 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
628
629
  selectionController?: SelectionController<M>;
629
630
  openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
630
631
  }) {
631
- // ── Resolve slots (pure function, no hooks) ──
632
- const slots = resolveEntitySlots(
632
+ // ── Resolve slots ──
633
+ // A hook now, not a pure call: a `display` resolver may have to fetch what
634
+ // fills a slot, so the row subscribes to the result and re-renders when it
635
+ // lands. `ListRow` is memoized, which is what keeps that to the one row.
636
+ const slots = useEntitySlots(
633
637
  entity as Entity<Record<string, unknown>>,
634
638
  collection as AdminCollection<Record<string, unknown>>,
635
639
  slotKeys
@@ -696,7 +700,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
696
700
  <div className="flex-shrink-0">
697
701
  {slots.image ? (
698
702
  <div className={cls("w-10 h-10 rounded-lg border relative overflow-hidden bg-surface-100 dark:bg-surface-900", defaultBorderMixin)}>
699
- <PropertyPreview propertyKey={slots.image.propertyKey} value={slots.image.value} property={slots.image.property} size="small" fill={true}/>
703
+ <SlotValue slot={slots.image} size="small" fill={true}/>
700
704
  </div>
701
705
  ) : (
702
706
  <div className={cls("w-10 h-10 rounded-lg bg-surface-100 dark:bg-surface-900 flex items-center justify-center border", defaultBorderMixin)}>
@@ -715,13 +719,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
715
719
  <div className="truncate">
716
720
  {slots.title?.value !== undefined ? (
717
721
  <Typography component="div" variant="body2" className="font-semibold text-surface-900 dark:text-surface-50 truncate transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400">
718
- <PropertyPreview
719
- propertyKey={slots.title.propertyKey}
720
- value={slots.title.value}
721
- property={slots.title.property}
722
- size="small"
723
- textOnly={true}
724
- />
722
+ <SlotValue slot={slots.title} size="small"/>
725
723
  </Typography>
726
724
  ) : (
727
725
  <Typography component="div" variant="body2" className="font-semibold text-surface-500 dark:text-surface-400 font-mono text-xs transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400">
@@ -734,13 +732,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
734
732
  {slots.subtitle && (
735
733
  <div className="truncate mt-0.5">
736
734
  <Typography variant="caption" component="div" className="text-surface-500 dark:text-surface-400 truncate">
737
- <PropertyPreview
738
- propertyKey={slots.subtitle.propertyKey}
739
- value={slots.subtitle.value}
740
- property={slots.subtitle.property}
741
- size="small"
742
- textOnly={true}
743
- />
735
+ <SlotValue slot={slots.subtitle} size="small"/>
744
736
  </Typography>
745
737
  </div>
746
738
  )}
@@ -825,12 +817,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
825
817
  {/* STATUS slot — hidden when row is narrow */}
826
818
  {slots.status && (
827
819
  <div className="flex-shrink-0 @max-[400px]:hidden">
828
- <PropertyPreview
829
- propertyKey={slots.status.propertyKey}
830
- value={slots.status.value}
831
- property={slots.status.property}
832
- size="small"
833
- />
820
+ <SlotValue slot={slots.status} size="small"/>
834
821
  </div>
835
822
  )}
836
823
 
@@ -823,6 +823,7 @@ parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
823
823
  collection={collection}
824
824
  filter={tableController.filterValues}
825
825
  sortBy={tableController.sortBy}
826
+ searchString={tableController.searchString}
826
827
  onCountChange={setDocsCount}
827
828
  />;
828
829
 
@@ -1126,13 +1127,18 @@ parentEntityIds,
1126
1127
 
1127
1128
  {countFetcher}
1128
1129
 
1129
- {tableController.dataLoadingError && tableController.data.length > 0 && (
1130
+ {/* A plugin's error view takes priority over the built-in
1131
+ banner, the same precedence the empty state uses. This was
1132
+ computed above and then rendered nowhere, so `collection.error`
1133
+ — a declared, documented slot with its own props interface —
1134
+ did nothing at all, and a plugin author had no way to tell. */}
1135
+ {pluginErrorView ?? (tableController.dataLoadingError && tableController.data.length > 0 && (
1130
1136
  <div className="flex items-center gap-4 px-4 py-2 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800 flex-shrink-0">
1131
1137
  <Typography variant="body2" className="text-red-700 dark:text-red-300 flex-1">
1132
1138
  <strong>Warning:</strong> {tableController.dataLoadingError.message || "Failed to update data."}
1133
1139
  </Typography>
1134
1140
  </div>
1135
- )}
1141
+ ))}
1136
1142
 
1137
1143
  {/* When isSplitLayout, SplitListView is ALWAYS mounted — regardless
1138
1144
  of viewMode. The toolbar + current view live in the left panel;
@@ -1302,17 +1308,26 @@ function DefaultCollectionEmptyState({
1302
1308
  */
1303
1309
  const inflightCountRequests = new Map<string, Promise<number>>();
1304
1310
 
1305
- function EntitiesCount({
1311
+ export function EntitiesCount({
1306
1312
  path,
1307
1313
  collection,
1308
1314
  filter,
1309
1315
  sortBy,
1316
+ searchString,
1310
1317
  onCountChange
1311
1318
  }: {
1312
1319
  path: string,
1313
1320
  collection: AdminCollection,
1314
1321
  filter?: FilterValues<any>,
1315
1322
  sortBy?: [string, "asc" | "desc"],
1323
+ /**
1324
+ * Required, not optional. The term sits in the same scope as the element
1325
+ * that mounts this and is passed to the toolbar and the empty state beside
1326
+ * it — it was simply not passed here, so a searched list showed the count
1327
+ * of the unsearched collection. A required prop is what stops the next
1328
+ * caller doing the same; there is no useful "count without the search".
1329
+ */
1330
+ searchString: string | undefined,
1316
1331
  onCountChange?: (count: number | null | undefined) => void,
1317
1332
  }) {
1318
1333
 
@@ -1344,12 +1359,17 @@ function EntitiesCount({
1344
1359
  const orderByParams: [string, "asc" | "desc"] | undefined = sortByProperty && currentSort ? [String(sortByProperty), currentSort] : undefined;
1345
1360
 
1346
1361
  // Deduplicate inflight count requests (e.g. React StrictMode double-mount)
1347
- const cacheKey = `${path}|${filterKey}|${sortByProperty ?? ""}|${currentSort ?? ""}`;
1362
+ // The search term is part of the query, so it is part of the key. The
1363
+ // cache is module-level and outlives an unmount, so a key that omits it
1364
+ // does not merely lose precision — it answers one search with a
1365
+ // different search's total.
1366
+ const cacheKey = `${path}|${filterKey}|${sortByProperty ?? ""}|${currentSort ?? ""}|${searchString ?? ""}`;
1348
1367
  let countPromise = inflightCountRequests.get(cacheKey);
1349
1368
  if (!countPromise) {
1350
1369
  countPromise = accessor.count({
1351
1370
  where: whereParams,
1352
- orderBy: orderByParams
1371
+ orderBy: orderByParams,
1372
+ searchString
1353
1373
  });
1354
1374
  inflightCountRequests.set(cacheKey, countPromise);
1355
1375
  // Clean up the inflight entry once resolved/rejected
@@ -1364,7 +1384,7 @@ function EntitiesCount({
1364
1384
  });
1365
1385
 
1366
1386
  return () => { cancelled = true; };
1367
- }, [path, filterKey, sortByProperty, currentSort]);
1387
+ }, [path, filterKey, sortByProperty, currentSort, searchString]);
1368
1388
 
1369
1389
  // Count is now displayed in the breadcrumb bar, this component only fetches and reports
1370
1390
  return null;
@@ -16,7 +16,8 @@ import {
16
16
  } from "@rebasepro/app";
17
17
  import { useAnalyticsController } from "@rebasepro/app";
18
18
  import { IconForView } from "@rebasepro/app";
19
- import { useCollectionSlotKeys, resolveEntitySlots } from "./usePreviewSlots";
19
+ import { useCollectionSlotKeys, useEntitySlots } from "./usePreviewSlots";
20
+ import { SlotValue } from "./SlotValue";
20
21
 
21
22
  export type EntityCardBindingProps<M extends Record<string, unknown> = Record<string, unknown>> = {
22
23
  entity: Entity<M>;
@@ -56,13 +57,10 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
56
57
  customizationController.propertyConfigs
57
58
  );
58
59
 
59
- const slots = useMemo(
60
- () => resolveEntitySlots(
61
- entity as Entity<Record<string, unknown>>,
62
- collection as AdminCollection<Record<string, unknown>>,
63
- slotKeys
64
- ),
65
- [entity, collection, slotKeys]
60
+ const slots = useEntitySlots(
61
+ entity as Entity<Record<string, unknown>>,
62
+ collection as AdminCollection<Record<string, unknown>>,
63
+ slotKeys
66
64
  );
67
65
 
68
66
  const handleClick = (e?: React.MouseEvent) => {
@@ -104,13 +102,7 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
104
102
  <div className="aspect-[4/3] relative overflow-hidden bg-surface-100 dark:bg-surface-900">
105
103
  {slots.image ? (
106
104
  <div className="w-full h-full">
107
- <PropertyPreview
108
- property={slots.image.property}
109
- propertyKey={slots.image.propertyKey}
110
- size="medium"
111
- value={slots.image.value}
112
- fill={true}
113
- />
105
+ <SlotValue slot={slots.image} size="medium" fill={true}/>
114
106
  </div>
115
107
  ) : (
116
108
  <div className="w-full h-full flex items-center justify-center">
@@ -160,12 +152,7 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
160
152
  {/* Title slot */}
161
153
  <div className="truncate my-1 text-sm font-medium min-h-[20px]">
162
154
  {slots.title ? (
163
- <PropertyPreview
164
- propertyKey={slots.title.propertyKey}
165
- value={slots.title.value}
166
- property={slots.title.property}
167
- size="small"
168
- />
155
+ <SlotValue slot={slots.title} size="small"/>
169
156
  ) : (
170
157
  <Typography variant="body2" className="text-surface-500">
171
158
  {entity.id}
@@ -176,12 +163,7 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
176
163
  {/* Subtitle slot */}
177
164
  {slots.subtitle && (
178
165
  <div className="line-clamp-3 [&_div]:line-clamp-3 text-xs text-surface-600 dark:text-surface-400 [&_p]:!my-1 [&_p:first-child]:!mt-0 [&_p:last-child]:!mb-0">
179
- <PropertyPreview
180
- propertyKey={slots.subtitle.propertyKey}
181
- value={slots.subtitle.value}
182
- property={slots.subtitle.property}
183
- size="small"
184
- />
166
+ <SlotValue slot={slots.subtitle} size="small"/>
185
167
  </div>
186
168
  )}
187
169
 
@@ -211,12 +193,7 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
211
193
  {/* Status slot */}
212
194
  {slots.status && (
213
195
  <div className="mt-1">
214
- <PropertyPreview
215
- propertyKey={slots.status.propertyKey}
216
- value={slots.status.value}
217
- property={slots.status.property}
218
- size="small"
219
- />
196
+ <SlotValue slot={slots.status} size="small"/>
220
197
  </div>
221
198
  )}
222
199
  </div>
@@ -0,0 +1,60 @@
1
+ import React from "react";
2
+ import type { PreviewSize } from "../../types/components/PropertyPreviewProps";
3
+ import { PropertyPreview } from "../../preview";
4
+ import type { PreviewSlot } from "./usePreviewSlots";
5
+
6
+ export interface SlotValueProps {
7
+ slot: PreviewSlot | undefined;
8
+ size: PreviewSize;
9
+ /**
10
+ * Relations and references render as one line of linked text rather than as
11
+ * a card. Defaults to true: a slot is a line in a row or a card, and a
12
+ * relation that renders its own bordered card there — with its own id line
13
+ * and its own side-panel button — is a card inside a card.
14
+ *
15
+ * Pass `false` only for a slot with room for one, which in practice is
16
+ * none of them.
17
+ */
18
+ textOnly?: boolean;
19
+ /** Images only: fill the container rather than fitting inside it. */
20
+ fill?: boolean;
21
+ }
22
+
23
+ /**
24
+ * One display slot, rendered.
25
+ *
26
+ * A slot's value arrives one of two ways and they cannot be rendered the same:
27
+ *
28
+ * - **From a property** — render it with that property's own preview, so an
29
+ * enum status keeps its colour, a date keeps its format and a storage path
30
+ * becomes a thumbnail. This is why a declared path beats a resolver whenever
31
+ * the value is actually on the record.
32
+ * - **From a resolver** — there is no property, because the value was computed
33
+ * or fetched. All that can be said about it is that it is text.
34
+ *
35
+ * The branch lives here, once, rather than at each of the fourteen places the
36
+ * list row, the card and the board card render a slot. That count is the reason:
37
+ * the last time a rule like this was inlined per call site, it drifted into
38
+ * seven versions.
39
+ */
40
+ export function SlotValue({ slot, size, textOnly = true, fill }: SlotValueProps) {
41
+
42
+ if (!slot || slot.value === undefined || slot.value === null) return null;
43
+
44
+ if (!slot.property || !slot.propertyKey) {
45
+ // Computed. Arrays (a `tags` resolver) join rather than rendering as
46
+ // "[object Object]" via String().
47
+ const text = Array.isArray(slot.value)
48
+ ? slot.value.map(item => String(item)).join(", ")
49
+ : String(slot.value);
50
+ return <>{text}</>;
51
+ }
52
+
53
+ return <PropertyPreview
54
+ propertyKey={slot.propertyKey}
55
+ value={slot.value as never}
56
+ property={slot.property}
57
+ size={size}
58
+ textOnly={textOnly}
59
+ fill={fill}/>;
60
+ }
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
 
3
- import { IconButton, iconSize, Tooltip, XIcon } from "@rebasepro/ui";
3
+ import { ChevronsLeftIcon, IconButton, iconSize, Tooltip } from "@rebasepro/ui";
4
4
  import { useTranslation } from "@rebasepro/app";
5
5
 
6
6
  import { useSplitView } from "./SplitViewContext";
@@ -9,9 +9,11 @@ import { useSplitView } from "./SplitViewContext";
9
9
  * Closes the list of a split view, leaving the open record on its own.
10
10
  *
11
11
  * It leads the record's own app bar, where the breadcrumb starts — this is the
12
- * entity pane's old "Open full screen" button, moved to that edge and wearing an
13
- * X: closing the list and opening the record full screen are the same thing, so
14
- * there is one control and one route (`#full`) rather than two.
12
+ * entity pane's old "Open full screen" button, moved to that edge and wearing a
13
+ * double chevron pointing at the list it folds away: closing the list and
14
+ * opening the record full screen are the same thing, so there is one control
15
+ * and one route (`#full`) rather than two. Full screen carries the mirrored
16
+ * chevron to bring the list back.
15
17
  */
16
18
  export function SplitListCloseButton() {
17
19
 
@@ -26,7 +28,7 @@ export function SplitListCloseButton() {
26
28
  size="small"
27
29
  onClick={splitView.openFullScreen}
28
30
  aria-label={t("hide_list")}>
29
- <XIcon size={iconSize.smallest}/>
31
+ <ChevronsLeftIcon size={iconSize.smallest}/>
30
32
  </IconButton>
31
33
  </Tooltip>
32
34
  );
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+
3
+ import { ChevronsRightIcon, IconButton, iconSize, Tooltip } from "@rebasepro/ui";
4
+ import { useTranslation } from "@rebasepro/app";
5
+
6
+ /**
7
+ * Brings the list back beside a record that was opened full screen.
8
+ *
9
+ * The mirror of {@link SplitListCloseButton}: the same edge of the same bar,
10
+ * the same double chevron pointing the other way, and the same single route —
11
+ * showing the list is dropping the `#full` hash. It replaces the back arrow
12
+ * where it applies, because the list it unfolds already holds the record.
13
+ */
14
+ export function SplitListShowButton({ onClick }: { onClick: () => void }) {
15
+
16
+ const { t } = useTranslation();
17
+
18
+ return (
19
+ <Tooltip title={t("show_list")}>
20
+ <IconButton
21
+ size="small"
22
+ onClick={onClick}
23
+ aria-label={t("show_list")}>
24
+ <ChevronsRightIcon size={iconSize.smallest}/>
25
+ </IconButton>
26
+ </Tooltip>
27
+ );
28
+ }
@@ -79,6 +79,19 @@ function isListShortcutTarget(target: HTMLElement | null, detailPanel: HTMLEleme
79
79
  return !detailPanel?.contains(target);
80
80
  }
81
81
 
82
+ /**
83
+ * The list's share of the split, as a percentage, before anyone drags it.
84
+ *
85
+ * The list only has to identify a record — a title, a subtitle and a status
86
+ * chip — while the detail beside it carries the whole record and its rail. At
87
+ * 30% the list was taking width from the side that had four columns of fields
88
+ * to lay out.
89
+ *
90
+ * A default, not a constraint: {@link savePanelSize} remembers the drag per
91
+ * collection, so anyone who has already sized this list keeps their size.
92
+ */
93
+ const DEFAULT_PANEL_SIZE = 26;
94
+
82
95
  function getSavedPanelSize(path: string): number {
83
96
  try {
84
97
  const saved = localStorage.getItem(`${PANEL_SIZE_KEY}_${path}`);
@@ -87,7 +100,7 @@ function getSavedPanelSize(path: string): number {
87
100
  if (!isNaN(val) && val > 0 && val < 100) return val;
88
101
  }
89
102
  } catch { /* ignore */ }
90
- return 30;
103
+ return DEFAULT_PANEL_SIZE;
91
104
  }
92
105
 
93
106
  function savePanelSize(path: string, size: number) {
@@ -408,7 +408,8 @@ values: { ...e.values,
408
408
  where: whereFilter,
409
409
  limit: itemCount,
410
410
  orderBy: orderByParam,
411
- include: includeParams
411
+ include: includeParams,
412
+ searchString: currentSearchString
412
413
  });
413
414
 
414
415
  const onError = (error: Error) => {
@@ -446,7 +447,12 @@ values: { ...e.values,
446
447
  where: whereFilter,
447
448
  limit: itemCount,
448
449
  orderBy: orderByParam,
449
- include: includeParams
450
+ include: includeParams,
451
+ // Read into a local at the top of this function and then used
452
+ // nowhere, in both places it was read. The board took a search
453
+ // term, re-subscribed every column when it changed, and
454
+ // returned the same rows.
455
+ searchString: currentSearchString
450
456
  }, res => {
451
457
  liveDataReceived = true;
452
458
  onUpdate(res.data as Entity<M>[]);
@@ -539,7 +545,10 @@ values: { ...e.values,
539
545
  };
540
546
 
541
547
  accessor.count({
542
- where: whereFilter
548
+ where: whereFilter,
549
+ // Or the column header counts the unsearched collection
550
+ // while the column beneath it shows searched rows.
551
+ searchString: currentSearchString
543
552
  }).then(count => {
544
553
  if (isCleaningUpRef.current) return;
545
554
  setColumnData(prev => ({