@rebasepro/admin 0.13.0 → 0.13.1-canary.gcd6689e

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 (60) hide show
  1. package/dist/{CollectionEditorDialog-CjXkWZ64.js → CollectionEditorDialog-CZZZpYOz.js} +3 -3
  2. package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-CZZZpYOz.js.map} +1 -1
  3. package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-CeoHSmUg.js} +2 -2
  4. package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-CeoHSmUg.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-CMKFWM2J.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-BsLTNoqX.js.map → RouterCollectionsStudioView-CMKFWM2J.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/SlotValue.d.ts +29 -0
  10. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +5 -3
  11. package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +12 -0
  12. package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +29 -4
  13. package/dist/components/DetailViewBinding.d.ts +6 -0
  14. package/dist/components/EditViewBinding.d.ts +7 -1
  15. package/dist/components/EntityIdentityBar.d.ts +10 -1
  16. package/dist/components/EntityViewBinding.d.ts +41 -6
  17. package/dist/components/app/Scaffold.d.ts +4 -3
  18. package/dist/{export-J79rMoNj.js → export-l19WevKo.js} +2 -2
  19. package/dist/{export-J79rMoNj.js.map → export-l19WevKo.js.map} +1 -1
  20. package/dist/form/components/FieldBlock.d.ts +16 -0
  21. package/dist/{history-D7L_JlLO.js → history-JL1ZmSKD.js} +4 -4
  22. package/dist/{history-D7L_JlLO.js.map → history-JL1ZmSKD.js.map} +1 -1
  23. package/dist/hooks/useEntityDisplay.d.ts +102 -0
  24. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -9
  25. package/dist/{import-BvYl--U4.js → import-DNrKrFQV.js} +3 -3
  26. package/dist/{import-BvYl--U4.js.map → import-DNrKrFQV.js.map} +1 -1
  27. package/dist/index.js +79 -15
  28. package/dist/index.js.map +1 -1
  29. package/dist/preview/components/BooleanPreview.d.ts +3 -1
  30. package/dist/routes/RebaseRoute.d.ts +1 -1
  31. package/dist/types/components/PropertyPreviewProps.d.ts +7 -0
  32. package/dist/{util-GiwPgxnL.js → util-pQ3YwBve.js} +1022 -641
  33. package/dist/util-pQ3YwBve.js.map +1 -0
  34. package/package.json +9 -9
  35. package/src/components/AdditionalFieldValue.tsx +68 -0
  36. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +5 -6
  37. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +9 -27
  38. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +11 -24
  39. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +10 -33
  40. package/src/components/CollectionViewBinding/SlotValue.tsx +52 -0
  41. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +7 -5
  42. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
  43. package/src/components/CollectionViewBinding/usePreviewSlots.ts +115 -48
  44. package/src/components/DetailViewBinding.tsx +71 -54
  45. package/src/components/EditViewBinding.tsx +51 -33
  46. package/src/components/EntityIdentityBar.tsx +26 -4
  47. package/src/components/EntityPreviewBinding.tsx +24 -12
  48. package/src/components/EntityViewBinding.tsx +237 -39
  49. package/src/components/InlineEntityPreview.tsx +10 -10
  50. package/src/components/app/Scaffold.tsx +48 -7
  51. package/src/form/EntityForm.tsx +6 -24
  52. package/src/form/components/FieldBlock.tsx +24 -0
  53. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
  54. package/src/hooks/useEntityDisplay.tsx +258 -0
  55. package/src/hooks/useEntityDisplayTitle.tsx +27 -38
  56. package/src/preview/PropertyPreview.tsx +1 -1
  57. package/src/preview/components/BooleanPreview.tsx +5 -2
  58. package/src/routes/RebaseRoute.tsx +62 -4
  59. package/src/types/components/PropertyPreviewProps.tsx +8 -0
  60. 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.gcd6689e",
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.gcd6689e",
87
+ "@rebasepro/app": "0.13.1-canary.gcd6689e",
88
+ "@rebasepro/forms": "0.13.1-canary.gcd6689e",
89
+ "@rebasepro/inference": "0.13.1-canary.gcd6689e",
90
+ "@rebasepro/common": "0.13.1-canary.gcd6689e",
91
+ "@rebasepro/types": "0.13.1-canary.gcd6689e",
92
+ "@rebasepro/ui": "0.13.1-canary.gcd6689e",
93
+ "@rebasepro/utils": "0.13.1-canary.gcd6689e"
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" textOnly={true}/>
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" textOnly={true}/>
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
 
@@ -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,52 @@
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
+ /** Relations and references render as plain text rather than as cards. */
10
+ textOnly?: boolean;
11
+ /** Images only: fill the container rather than fitting inside it. */
12
+ fill?: boolean;
13
+ }
14
+
15
+ /**
16
+ * One display slot, rendered.
17
+ *
18
+ * A slot's value arrives one of two ways and they cannot be rendered the same:
19
+ *
20
+ * - **From a property** — render it with that property's own preview, so an
21
+ * enum status keeps its colour, a date keeps its format and a storage path
22
+ * becomes a thumbnail. This is why a declared path beats a resolver whenever
23
+ * the value is actually on the record.
24
+ * - **From a resolver** — there is no property, because the value was computed
25
+ * or fetched. All that can be said about it is that it is text.
26
+ *
27
+ * The branch lives here, once, rather than at each of the fourteen places the
28
+ * list row, the card and the board card render a slot. That count is the reason:
29
+ * the last time a rule like this was inlined per call site, it drifted into
30
+ * seven versions.
31
+ */
32
+ export function SlotValue({ slot, size, textOnly, fill }: SlotValueProps) {
33
+
34
+ if (!slot || slot.value === undefined || slot.value === null) return null;
35
+
36
+ if (!slot.property || !slot.propertyKey) {
37
+ // Computed. Arrays (a `tags` resolver) join rather than rendering as
38
+ // "[object Object]" via String().
39
+ const text = Array.isArray(slot.value)
40
+ ? slot.value.map(item => String(item)).join(", ")
41
+ : String(slot.value);
42
+ return <>{text}</>;
43
+ }
44
+
45
+ return <PropertyPreview
46
+ propertyKey={slot.propertyKey}
47
+ value={slot.value as never}
48
+ property={slot.property}
49
+ size={size}
50
+ textOnly={textOnly}
51
+ fill={fill}/>;
52
+ }
@@ -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
+ }