@rebasepro/admin 0.13.0 → 0.13.1-canary.g394d868

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 (137) hide show
  1. package/dist/{CollectionEditorDialog-CjXkWZ64.js → CollectionEditorDialog-D78xsXgL.js} +3 -3
  2. package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-D78xsXgL.js.map} +1 -1
  3. package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-B88EC-1f.js} +2 -2
  4. package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-B88EC-1f.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-B1Ws7h9l.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-BsLTNoqX.js.map → RouterCollectionsStudioView-B1Ws7h9l.js.map} +1 -1
  7. package/dist/collection_editor/serializable_types.d.ts +1 -0
  8. package/dist/collection_editor_ui.js +4 -4
  9. package/dist/components/AdditionalFieldValue.d.ts +25 -0
  10. package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +16 -0
  11. package/dist/components/CollectionViewBinding/SlotValue.d.ts +63 -0
  12. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -4
  13. package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +12 -0
  14. package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +55 -5
  15. package/dist/components/DetailViewBinding.d.ts +15 -0
  16. package/dist/components/DrawerNavigationGroup.d.ts +9 -6
  17. package/dist/components/DrawerNavigationItem.d.ts +16 -6
  18. package/dist/components/EditViewBinding.d.ts +30 -1
  19. package/dist/components/EntityDisplayHeader.d.ts +53 -0
  20. package/dist/components/EntityIdentityBar.d.ts +43 -1
  21. package/dist/components/EntityViewBinding.d.ts +60 -6
  22. package/dist/components/RebaseLayout.d.ts +1 -1
  23. package/dist/components/RebaseShell.d.ts +1 -1
  24. package/dist/components/RelationSelector.d.ts +8 -1
  25. package/dist/components/UserSelector.d.ts +8 -1
  26. package/dist/components/app/Scaffold.d.ts +9 -3
  27. package/dist/components/index.d.ts +2 -0
  28. package/dist/{export-J79rMoNj.js → export-BZhdWhkB.js} +2 -2
  29. package/dist/{export-J79rMoNj.js.map → export-BZhdWhkB.js.map} +1 -1
  30. package/dist/form/components/FieldBlock.d.ts +58 -4
  31. package/dist/form/components/FormRail.d.ts +13 -0
  32. package/dist/form/components/FormSections.d.ts +15 -1
  33. package/dist/form/components/useRailVisible.d.ts +5 -4
  34. package/dist/form/form_utils.d.ts +38 -1
  35. package/dist/{history-D7L_JlLO.js → history-BPpt68CL.js} +4 -4
  36. package/dist/{history-D7L_JlLO.js.map → history-BPpt68CL.js.map} +1 -1
  37. package/dist/hooks/useEntityDisplay.d.ts +102 -0
  38. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -9
  39. package/dist/{import-BvYl--U4.js → import-gRfemegK.js} +3 -3
  40. package/dist/{import-BvYl--U4.js.map → import-gRfemegK.js.map} +1 -1
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.js +152 -57
  43. package/dist/index.js.map +1 -1
  44. package/dist/preview/compact_text.d.ts +23 -0
  45. package/dist/preview/components/BooleanPreview.d.ts +3 -1
  46. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  47. package/dist/preview/property_previews/ArrayOneOfPreview.d.ts +1 -1
  48. package/dist/preview/property_previews/ArrayPropertyPreview.d.ts +1 -1
  49. package/dist/preview/property_previews/MapPropertyPreview.d.ts +1 -1
  50. package/dist/preview/property_previews/StringPropertyPreview.d.ts +1 -1
  51. package/dist/routes/RebaseRoute.d.ts +1 -1
  52. package/dist/types/components/PropertyPreviewProps.d.ts +19 -0
  53. package/dist/util/number_format.d.ts +14 -0
  54. package/dist/util/previews.d.ts +9 -2
  55. package/dist/{util-GiwPgxnL.js → util-BHq3VOjH.js} +9080 -7861
  56. package/dist/util-BHq3VOjH.js.map +1 -0
  57. package/package.json +9 -9
  58. package/src/collection_editor/serializable_types.ts +1 -0
  59. package/src/components/AdditionalFieldValue.tsx +68 -0
  60. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +7 -8
  61. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
  62. package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
  63. package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
  64. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +26 -27
  65. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +172 -112
  66. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
  67. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +38 -8
  68. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +23 -36
  69. package/src/components/CollectionViewBinding/SlotValue.tsx +156 -0
  70. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -7
  71. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
  72. package/src/components/CollectionViewBinding/SplitListView.tsx +33 -2
  73. package/src/components/CollectionViewBinding/useBoardDataController.tsx +12 -3
  74. package/src/components/CollectionViewBinding/usePreviewSlots.ts +177 -54
  75. package/src/components/DefaultDrawer.tsx +19 -23
  76. package/src/components/DetailViewBinding.tsx +87 -55
  77. package/src/components/DrawerNavigationGroup.tsx +16 -39
  78. package/src/components/DrawerNavigationItem.tsx +44 -16
  79. package/src/components/EditViewBinding.tsx +211 -41
  80. package/src/components/EntityDisplayHeader.tsx +170 -0
  81. package/src/components/EntityIdentityBar.tsx +149 -23
  82. package/src/components/EntityPreviewBinding.tsx +58 -18
  83. package/src/components/EntityViewBinding.tsx +342 -41
  84. package/src/components/InlineEntityPreview.tsx +10 -10
  85. package/src/components/RebaseLayout.tsx +2 -2
  86. package/src/components/RebaseShell.tsx +2 -2
  87. package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
  88. package/src/components/RelationSelector.tsx +15 -4
  89. package/src/components/SideDialogs.tsx +15 -3
  90. package/src/components/SidePanelBinding.tsx +122 -66
  91. package/src/components/UserSelector.tsx +15 -4
  92. package/src/components/app/Scaffold.tsx +112 -10
  93. package/src/components/index.ts +3 -0
  94. package/src/form/EntityForm.tsx +64 -52
  95. package/src/form/EntityFormBinding.tsx +24 -11
  96. package/src/form/PropertyFieldBinding.tsx +2 -2
  97. package/src/form/components/FieldBlock.tsx +80 -10
  98. package/src/form/components/FormRail.tsx +19 -2
  99. package/src/form/components/FormSections.tsx +32 -5
  100. package/src/form/components/StorageUploadProgress.tsx +9 -3
  101. package/src/form/components/useRailVisible.tsx +5 -4
  102. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
  103. package/src/form/field_bindings/BinaryFieldBinding.tsx +10 -5
  104. package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
  105. package/src/form/field_bindings/GeopointFieldBinding.tsx +23 -16
  106. package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
  107. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +13 -7
  108. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +7 -1
  109. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -1
  110. package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
  111. package/src/form/field_bindings/RelationFieldBinding.tsx +5 -2
  112. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
  113. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
  114. package/src/form/field_bindings/UserSelectFieldBinding.tsx +4 -1
  115. package/src/form/field_bindings/VectorFieldBinding.tsx +6 -4
  116. package/src/form/form_utils.ts +78 -0
  117. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
  118. package/src/hooks/useEntityDisplay.tsx +262 -0
  119. package/src/hooks/useEntityDisplayTitle.tsx +27 -38
  120. package/src/index.ts +9 -1
  121. package/src/preview/PropertyPreview.tsx +25 -5
  122. package/src/preview/compact_text.ts +68 -0
  123. package/src/preview/components/BooleanPreview.tsx +5 -2
  124. package/src/preview/components/StorageThumbnail.tsx +5 -2
  125. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
  126. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +26 -1
  127. package/src/preview/property_previews/ArrayOneOfPreview.tsx +10 -1
  128. package/src/preview/property_previews/ArrayPropertyPreview.tsx +10 -1
  129. package/src/preview/property_previews/MapPropertyPreview.tsx +52 -1
  130. package/src/preview/property_previews/NumberPropertyPreview.tsx +9 -1
  131. package/src/preview/property_previews/StringPropertyPreview.tsx +8 -5
  132. package/src/routes/RebaseRoute.tsx +62 -4
  133. package/src/types/components/PropertyPreviewProps.tsx +21 -0
  134. package/src/util/number_format.ts +46 -0
  135. package/src/util/previews.ts +8 -55
  136. package/src/util/property_utils.tsx +6 -1
  137. package/dist/util-GiwPgxnL.js.map +0 -1
@@ -10,11 +10,11 @@ import { CollectionViewBinding } from "./CollectionViewBinding/CollectionViewBin
10
10
  import { EntityViewBinding } from "./EntityViewBinding";
11
11
  import { EntityIdentityBar } from "./EntityIdentityBar";
12
12
  import { SplitListCloseButton } from "./CollectionViewBinding/SplitListCloseButton";
13
+ import { SplitListShowButton } from "./CollectionViewBinding/SplitListShowButton";
13
14
  import { EntityInspector, InspectorTab } from "./EntityInspector";
14
15
  import { CircularProgressCenter, iconSize } from "@rebasepro/ui";
15
16
  import {
16
17
  Alert,
17
- ArrowLeftIcon,
18
18
  Button,
19
19
  CenteredView,
20
20
  cls,
@@ -34,6 +34,7 @@ import { ErrorBoundary } from "@rebasepro/ui";
34
34
  import { ErrorView, createFormexStub, usePermissions, useTranslation, getIcon } from "@rebasepro/app";
35
35
 
36
36
  import { removeInitialAndTrailingSlashes, resolveDefaultSelectedView } from "@rebasepro/app";
37
+ import { withViewMode } from "../util/view_mode";
37
38
  import { resolvedSelectedEntityView } from "../util/resolutions";
38
39
  import {
39
40
  useCustomizationController,
@@ -45,9 +46,7 @@ import {
45
46
  import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
46
47
  import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
47
48
  import { useNavigate } from "react-router";
48
- import { getValueInPath } from "@rebasepro/utils";
49
- import { getEntityTitlePropertyKeyForEntity, isUserSelectProperty, resolveTitleToString } from "../util/previews";
50
- import { getUserLabel, useResolvedUser } from "../hooks/useResolvedUsers";
49
+ import { useEntityDisplayTitle } from "../hooks/useEntityDisplayTitle";
51
50
 
52
51
 
53
52
  import { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE } from "../util/view_constants";
@@ -57,6 +56,8 @@ export type BarActionsParams = {
57
56
  status: EntityStatus,
58
57
  path: string,
59
58
  entityId?: string | number;
59
+ /** Always false here — this view reads a record, it does not edit one. */
60
+ dirty: boolean;
60
61
  };
61
62
 
62
63
  export type OnTabChangeParams<M extends Record<string, unknown>> = {
@@ -75,6 +76,13 @@ export interface DetailViewBindingProps<M extends Record<string, unknown> = Reco
75
76
  parentEntityIds: string[];
76
77
  onTabChange?: (props: OnTabChangeParams<M>) => void;
77
78
  onEditClick?: () => void;
79
+ /**
80
+ * Dismiss this record — the ✕ at the identity bar's trailing edge.
81
+ *
82
+ * Set by the layouts that own the record's place on screen but are not side
83
+ * dialogs: the split, where dismissing is navigating back to the collection.
84
+ */
85
+ onCloseRequest?: () => void;
78
86
  layout?: "side_panel" | "full_screen" | "split" | "dialog";
79
87
  barActions?: (params: BarActionsParams) => React.ReactNode;
80
88
  /**
@@ -82,6 +90,12 @@ export interface DetailViewBindingProps<M extends Record<string, unknown> = Reco
82
90
  * of the breadcrumb — the side panel's close button.
83
91
  */
84
92
  barActionsStart?: React.ReactNode;
93
+ /**
94
+ * Full screen only: bring the list back beside this record. Set when the
95
+ * record was opened out of a split collection, so folding the list away is
96
+ * reversible; it takes the place of the back arrow when present.
97
+ */
98
+ onShowList?: () => void;
85
99
  }
86
100
 
87
101
  export function DetailViewBinding<M extends Record<string, unknown>>({
@@ -137,11 +151,13 @@ function DetailViewBindingInner<M extends Record<string, unknown>>({
137
151
  parentEntityIds,
138
152
  onTabChange,
139
153
  onEditClick,
154
+ onCloseRequest,
140
155
  entity,
141
156
  dataLoading,
142
157
  layout = "full_screen",
143
158
  barActions,
144
- barActionsStart
159
+ barActionsStart,
160
+ onShowList
145
161
  }: DetailViewBindingProps<M> & {
146
162
  entity?: Entity<M>,
147
163
  dataLoading: boolean,
@@ -283,18 +299,13 @@ entityId }
283
299
  const formViewConfig = (collection as AdminCollection<M> & { formView?: FormViewConfig<M> }).formView;
284
300
  const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef<EntityCustomViewParams>(formViewConfig.Builder as ComponentRef<EntityCustomViewParams>) : null;
285
301
 
286
- // Title resolution
287
- const titlePropertyKey = getEntityTitlePropertyKeyForEntity(collection, usedEntity?.values, usedEntity?.id);
288
- const rawTitle = usedEntity?.values && titlePropertyKey ? getValueInPath(usedEntity.values, titlePropertyKey) : undefined;
289
- // A user picker stores an id: resolve it to the person, like a relation.
290
- const titleUser = useResolvedUser(isUserSelectProperty(collection, titlePropertyKey) && typeof rawTitle === "string"
291
- ? rawTitle
292
- : undefined);
293
- const title = titleUser
294
- ? getUserLabel(titleUser)
295
- : (rawTitle !== undefined && rawTitle !== null
296
- ? resolveTitleToString(rawTitle)
297
- : (collection.singularName ?? collection.name));
302
+ // The heading, resolved the one way — this used to be a copy of
303
+ // `useEntityDisplayTitle`'s body, and the two had already drifted.
304
+ const title = useEntityDisplayTitle({
305
+ collection,
306
+ entity: usedEntity,
307
+ status: "existing"
308
+ });
298
309
 
299
310
  // Non-action custom views
300
311
  const nonActionCustomViews = useMemo(() =>
@@ -380,20 +391,31 @@ entityId }
380
391
  }, [onTabChange, path, entityId, collection]);
381
392
 
382
393
  const propertyDetailView = () => {
383
- // formView.Builder replaces the default property display
394
+ // formView.Builder replaces the default property display. It gets the
395
+ // centred column the record used to be given here; the default display
396
+ // now lays itself out, rail included.
384
397
  if (FormViewBuilder && usedEntity) {
385
- return <ErrorBoundary>
386
- <Suspense fallback={<CircularProgressCenter />}>
387
- <FormViewBuilder
388
- collection={collection}
389
- parentCollectionSlugs={parentCollectionSlugs}
390
- parentEntityIds={parentEntityIds}
391
- entity={usedEntity}
392
- modifiedValues={usedEntity?.values}
393
- formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
394
- />
395
- </Suspense>
396
- </ErrorBoundary>;
398
+ return <div className={"flex-1 min-w-0 overflow-y-auto flex justify-center items-start"}>
399
+ <div className={cls(
400
+ "w-full max-w-3xl 2xl:max-w-4xl flex flex-col",
401
+ layout === "dialog"
402
+ ? "pt-5 pb-12 px-6 sm:px-8"
403
+ : "pt-6 pb-16 px-5 sm:px-8"
404
+ )}>
405
+ <ErrorBoundary>
406
+ <Suspense fallback={<CircularProgressCenter />}>
407
+ <FormViewBuilder
408
+ collection={collection}
409
+ parentCollectionSlugs={parentCollectionSlugs}
410
+ parentEntityIds={parentEntityIds}
411
+ entity={usedEntity}
412
+ modifiedValues={usedEntity?.values}
413
+ formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
414
+ />
415
+ </Suspense>
416
+ </ErrorBoundary>
417
+ </div>
418
+ </div>;
397
419
  }
398
420
 
399
421
  return (
@@ -402,6 +424,9 @@ entityId }
402
424
  entity={usedEntity}
403
425
  collection={collection}
404
426
  path={path}
427
+ asPage
428
+ openEntityMode={layout}
429
+ formContext={readOnlyFormContext}
405
430
  />}
406
431
  </>
407
432
  );
@@ -458,24 +483,27 @@ entityId }
458
483
  );
459
484
  });
460
485
 
486
+ // The breadcrumb's destination, and the back arrow's when it renders. Both
487
+ // keep the view mode: a collection reached from one of its own records must
488
+ // come back as the user left it.
489
+ const collectionUrl = withViewMode(urlController.buildUrlCollectionPath(path));
490
+
461
491
  // Full screen is the one layout with no way out of its own: a side panel and a
462
492
  // dialog can be dismissed, and a split keeps the list beside it, but opening an
463
493
  // entity full screen replaces the collection entirely and left browser Back as
464
494
  // the only route back to it — which is not an affordance the page shows, and is
465
495
  // wrong anyway once the user has moved between tabs inside the entity.
466
- const backToCollectionButton = layout === "full_screen" ? (
467
- <Tooltip title={t("back")}>
468
- <IconButton
469
- size="small"
470
- aria-label={t("back")}
471
- onClick={() => navigate(urlController.buildUrlCollectionPath(path))}
472
- >
473
- <ArrowLeftIcon size={iconSize.smallest} />
474
- </IconButton>
475
- </Tooltip>
476
- ) : null;
477
-
478
- const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews || layout === "side_panel" || layout === "dialog" || Boolean(backToCollectionButton);
496
+ //
497
+ // Unless the list can be unfolded back beside the record: that chevron
498
+ // leads to the same collection while keeping the record open, so it stands
499
+ // alone rather than beside an arrow going almost the same place.
500
+ //
501
+ // It goes to the bar's leading edge — where a back control reads as back,
502
+ // and where the edit view has always put the same arrow — rather than to
503
+ // the trailing edge among the record's own actions.
504
+ const backToCollection = layout === "full_screen" && !onShowList
505
+ ? () => navigate(collectionUrl)
506
+ : undefined;
479
507
 
480
508
  const fullScreenButton = !barActions && // Not in split: the list panel beside it carries the close control,
481
509
  // and an expand button there competes with it.
@@ -508,49 +536,53 @@ entityId }
508
536
  // Main content view. The title, the `path/id` chip and the empty band under
509
537
  // them have moved into the identity bar, and the `w-80 2xl:w-96` rail that
510
538
  // held a single Edit button is gone — the bar holds it now.
539
+ //
540
+ // The scroll now lives on the record's own column rather than on this row,
541
+ // so the metadata rail beside it stays put while the record scrolls — the
542
+ // same arrangement the form uses.
511
543
  const mainView = <div
512
544
  className={cls(
513
- "flex-1 flex flex-row w-full overflow-y-auto justify-center",
545
+ "flex-1 flex flex-row w-full min-h-0",
514
546
  !mainViewVisible ? "hidden" : ""
515
547
  )}>
516
- <div className={cls(
517
- "w-full max-w-3xl 2xl:max-w-4xl flex flex-col",
518
- layout === "dialog"
519
- ? "pt-5 pb-12 px-6 sm:px-8"
520
- : "pt-6 pb-16 px-5 sm:px-8"
521
- )}>
522
- {propertyDetailView()}
523
- </div>
548
+ {propertyDetailView()}
524
549
  </div>;
525
550
 
526
551
  let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-800">
527
552
 
528
553
  <EntityIdentityBar
529
554
  collection={collection as AdminCollection}
555
+ collectionUrl={layout === "full_screen" || layout === "split" ? collectionUrl : undefined}
530
556
  title={title}
531
557
  entityId={entityId}
532
558
  status={"existing"}
533
559
  dirty={false}
534
560
  saving={false}
561
+ onBack={backToCollection}
562
+ onClose={onCloseRequest}
535
563
  onInspect={includeJsonView ? () => setInspectorTab("json") : undefined}
536
564
  onViewHistory={includeHistoryView ? () => setInspectorTab("history") : undefined}
565
+ externalLink={usedEntity
566
+ ? customizationController?.entityLinkBuilder?.({ entity: usedEntity })
567
+ : undefined}
537
568
  leading={<>
538
569
  {barActionsStart}
539
570
  {/* Split view: closing the list is opening this record
540
571
  full screen, and the control for it belongs at this
541
- bar's leading edge. */}
572
+ bar's leading edge. Full screen carries its mirror. */}
542
573
  {layout === "split" && <SplitListCloseButton/>}
574
+ {layout === "full_screen" && onShowList && <SplitListShowButton onClick={onShowList}/>}
543
575
  </>}
544
576
  trailing={<>
545
577
  {editButton}
546
578
  {pluginActionsTop}
547
- {backToCollectionButton}
548
579
  {fullScreenButton}
549
580
  {barActions?.({
550
581
  path,
551
582
  entityId,
552
583
  values: usedEntity?.values ?? {},
553
- status: "existing"
584
+ status: "existing",
585
+ dirty: false
554
586
  })}
555
587
  </>}
556
588
  />
@@ -28,11 +28,12 @@ export interface DrawerNavigationGroupProps {
28
28
  */
29
29
  drawerOpen: boolean;
30
30
  /**
31
- * Whether tooltips should be shown (drawer closed + hovered)
31
+ * @deprecated No longer read. Entry tooltips are uncontrolled — they follow the
32
+ * pointer and focus, and are suppressed wherever the label is already visible.
32
33
  */
33
- tooltipsOpen: boolean;
34
+ tooltipsOpen?: boolean;
34
35
  /**
35
- * Whether admin menu is open (used to control tooltip visibility)
36
+ * @deprecated No longer read. See {@link tooltipsOpen}.
36
37
  */
37
38
  adminMenuOpen?: boolean;
38
39
  /**
@@ -50,8 +51,10 @@ export interface DrawerNavigationGroupProps {
50
51
  /**
51
52
  * Lucide icon name for the group header, from `NavigationGroupMapping.icon`.
52
53
  *
53
- * Supplying one moves the visual anchor from the rows to the group: the header
54
- * gets the icon, and the entries below trade theirs for an indent.
54
+ * It decorates the header and nothing else: the entries below are untouched and
55
+ * keep their own icons. An app that wants the categorised look — rows giving up
56
+ * their icons to indent under the group — builds it in its own drawer, by
57
+ * overriding `Shell.DrawerNavigationItem` and passing `indented`.
55
58
  */
56
59
  icon?: string;
57
60
  }
@@ -66,8 +69,6 @@ export function DrawerNavigationGroup({
66
69
  collapsed,
67
70
  onToggleCollapsed,
68
71
  drawerOpen,
69
- tooltipsOpen,
70
- adminMenuOpen,
71
72
  headerActions,
72
73
  onItemClick,
73
74
  hideHeader,
@@ -76,17 +77,13 @@ export function DrawerNavigationGroup({
76
77
  const { t } = useTranslation();
77
78
  const ResolvedDrawerNavigationItem = useComponentOverride("Shell.DrawerNavigationItem", DrawerNavigationItem);
78
79
 
79
- // Opt-in, per group: declaring `icon` on a `NavigationGroupMapping` switches
80
- // this group to the categorised treatment — icon on the header, entries
81
- // indented beneath it without their own. A group that declares no icon renders
82
- // exactly as it did before, so an existing project sees no change at all.
83
- //
84
- // Two cases stay on the flat rendering regardless: with no header there is
85
- // nothing to indent under, and collapsed to a rail the entry icons are the only
86
- // affordance left to click.
80
+ // The icon decorates the header, and stops there. It used to also strip the
81
+ // entries of theirs and indent them, which made a per-app styling choice into
82
+ // framework behaviour: every project that labelled a group with an icon lost the
83
+ // icons on its rows, with nothing to turn it off. An app that wants that look
84
+ // now opts into it in its own drawer — see `indented` on
85
+ // {@link DrawerNavigationItem}.
87
86
  const groupIcon = !hideHeader && drawerOpen ? getIcon(icon, undefined, undefined, "small") : undefined;
88
- const categorised = Boolean(groupIcon);
89
- const indentEntries = categorised;
90
87
  return (
91
88
  <div
92
89
  className={"my-2 mx-2 flex flex-col"}
@@ -103,10 +100,7 @@ export function DrawerNavigationGroup({
103
100
  <ChevronDownIcon
104
101
  size={iconSize.small}
105
102
  className={cls(
106
- categorised
107
- ? "text-surface-500 dark:text-surface-400"
108
- : "text-surface-400 dark:text-surface-400",
109
- "transition-transform duration-200 mr-1",
103
+ "text-surface-400 dark:text-surface-400 transition-transform duration-200 mr-1",
110
104
  collapsed ? "-rotate-90" : "rotate-0"
111
105
  )}
112
106
  />
@@ -118,21 +112,7 @@ export function DrawerNavigationGroup({
118
112
  <Typography
119
113
  variant={"caption"}
120
114
  color={"secondary"}
121
- // Two treatments, and which one applies is decided by the group
122
- // itself. Without an icon this is byte-for-byte the original: the
123
- // drawer is a flat list of same-weight rows, and a louder header
124
- // would just compete with them.
125
- //
126
- // With one, the group becomes the thing you scan — its rows have
127
- // given up their icons to indent beneath it — so the label steps up
128
- // to match. At 11px in surface-400 it sat *below* the contrast of
129
- // the rows it labels, which is backwards once it is the anchor.
130
- className={cls(
131
- "font-semibold uppercase flex-grow line-clamp-1",
132
- categorised
133
- ? "text-[12px] tracking-wide text-surface-600 dark:text-surface-300"
134
- : "text-[11px] tracking-wider text-surface-400 dark:text-surface-400"
135
- )}
115
+ className="font-semibold text-[11px] uppercase tracking-wider flex-grow line-clamp-1 text-surface-400 dark:text-surface-400"
136
116
  >
137
117
  {(group || t("views_group"))}
138
118
  </Typography>
@@ -158,13 +138,10 @@ export function DrawerNavigationGroup({
158
138
  <ResolvedDrawerNavigationItem
159
139
  key={entry.id}
160
140
  icon={<IconForView collectionOrView={entry.collection ?? entry.view} size={"small"}/>}
161
- tooltipsOpen={!collapsed && tooltipsOpen}
162
- adminMenuOpen={adminMenuOpen}
163
141
  drawerOpen={drawerOpen}
164
142
  onClick={() => onItemClick?.(entry)}
165
143
  url={entry.url}
166
144
  name={entry.name}
167
- indented={indentEntries}
168
145
  />
169
146
  ))}
170
147
  </div>
@@ -3,30 +3,39 @@ import React from "react";
3
3
  import { NavLink } from "react-router";
4
4
  import { cls, Tooltip } from "@rebasepro/ui";
5
5
 
6
- export function DrawerNavigationItem({
7
- name,
8
- icon,
9
- drawerOpen,
10
- adminMenuOpen,
11
- tooltipsOpen,
12
- url,
13
- onClick,
14
- indented = false
15
- }: {
6
+ /**
7
+ * Props of a drawer navigation row, named so an app overriding
8
+ * `Shell.DrawerNavigationItem` can type its wrapper against them.
9
+ */
10
+ export type DrawerNavigationItemProps = {
16
11
  icon: React.ReactElement,
17
12
  name: string,
18
- tooltipsOpen: boolean,
13
+ /** @deprecated No longer read: the tooltip is uncontrolled. */
14
+ tooltipsOpen?: boolean,
19
15
  drawerOpen: boolean,
16
+ /** @deprecated No longer read: the tooltip is uncontrolled. */
20
17
  adminMenuOpen?: boolean,
21
18
  url: string,
22
19
  onClick?: () => void,
23
20
  /**
24
- * Render as a child of an icon-bearing group: no icon of its own, indented to
25
- * sit under the group's label. Set by {@link DrawerNavigationGroup} only when
26
- * the group actually has an icon and the drawer is expanded.
21
+ * Drop the row's icon and indent the label into the space it occupied, so the
22
+ * row reads as a child of the group header above it.
23
+ *
24
+ * Nothing in the framework sets this: the stock drawer always passes `false`,
25
+ * and an app that wants the categorised look opts into it by overriding
26
+ * `Shell.DrawerNavigationItem` and deciding for itself which rows get it.
27
27
  */
28
28
  indented?: boolean,
29
- }) {
29
+ };
30
+
31
+ export function DrawerNavigationItem({
32
+ name,
33
+ icon,
34
+ drawerOpen,
35
+ url,
36
+ onClick,
37
+ indented = false
38
+ }: DrawerNavigationItemProps) {
30
39
 
31
40
  // Indented rows give up the icon rather than keeping it *and* indenting: a
32
41
  // per-row icon repeated down a group is what flattens the hierarchy, since
@@ -40,6 +49,15 @@ export function DrawerNavigationItem({
40
49
  {icon}
41
50
  </div>;
42
51
 
52
+ const [wantsTooltip, setWantsTooltip] = React.useState(false);
53
+
54
+ // Radix will not report a close it never noticed: while the tooltip is masked
55
+ // the pointer can leave the row without `onOpenChange(false)` ever firing. So
56
+ // the stale `true` is dropped on the way *into* the masked state — otherwise
57
+ // it surfaces the moment the mask lifts, as a tooltip hanging over a rail the
58
+ // pointer left long ago.
59
+ if (drawerOpen && wantsTooltip) setWantsTooltip(false);
60
+
43
61
  const listItem = <div>
44
62
  <NavLink
45
63
  onClick={onClick}
@@ -70,8 +88,18 @@ export function DrawerNavigationItem({
70
88
  </NavLink>
71
89
  </div>;
72
90
 
91
+ // Per row, and always controlled. The old shared flag could only say "every
92
+ // tooltip" or "none", and the state it needed was unreachable, so no entry
93
+ // tooltip ever opened — leaving a bare rail of unlabelled icons. Radix decides
94
+ // when this one wants to be open, from the pointer and from focus; the drawer
95
+ // only masks it once the label says the same thing.
96
+ //
97
+ // Masked rather than withheld: dropping `open`, or the title, would move the
98
+ // tooltip between controlled and uncontrolled as the drawer opens, which
99
+ // strands the old state — a tooltip left up over a pointer nowhere near it.
73
100
  return <Tooltip
74
- open={drawerOpen || adminMenuOpen ? false : tooltipsOpen}
101
+ open={!drawerOpen && wantsTooltip}
102
+ onOpenChange={setWantsTooltip}
75
103
  side="right"
76
104
  title={name}>
77
105
  {listItem}