@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
@@ -1,6 +1,9 @@
1
1
  export type { EntityViewBindingProps } from "./EntityViewBinding";
2
2
  export { EntityViewBinding } from "./EntityViewBinding";
3
3
 
4
+ export type { EntityDisplayHeaderProps } from "./EntityDisplayHeader";
5
+ export { EntityDisplayHeader, HEADER_DISPLAY_ROLES } from "./EntityDisplayHeader";
6
+
4
7
  export * from "./DetailViewBinding";
5
8
 
6
9
  export type { SelectionProps } from "./ReferenceTable/SelectionTableBinding";
@@ -13,11 +13,12 @@ import { isReadOnly } from "@rebasepro/app";
13
13
 
14
14
  import { getFormFieldKeys, resolveFormLayout } from "@rebasepro/app";
15
15
  import type { ResolvedFormField } from "@rebasepro/app";
16
- import { Alert, Button, cls, Dialog, DialogActions, DialogContent, DialogTitle, iconSize, paperMixin, Typography } from "@rebasepro/ui";
16
+ import { Alert, Button, cls, defaultBorderMixin, Dialog, DialogActions, DialogContent, DialogTitle, paperMixin, Typography } from "@rebasepro/ui";
17
17
  import { Formex, FormexController, useCreateFormex } from "@rebasepro/forms";
18
18
 
19
- import { FieldBlock, isSelfLabellingProperty } from "./components/FieldBlock";
20
- import { FormRail } from "./components/FormRail";
19
+ import { FieldBlock, isSelfLabellingProperty, LABEL_ICON_SIZE, spanClass } from "./components/FieldBlock";
20
+ import { AdditionalFieldValue } from "../components/AdditionalFieldValue";
21
+ import { FormRail, RecordMeta } from "./components/FormRail";
21
22
  import { FormSections } from "./components/FormSections";
22
23
  import { useRailVisible } from "./components/useRailVisible";
23
24
  import { LabelWithIconAndTooltip } from "./components/LabelWithIconAndTooltip";
@@ -440,10 +441,10 @@ export function EntityForm<M extends Record<string, unknown>>({
440
441
  const child = AdditionalFieldBuilder
441
442
  ? <AdditionalFieldBuilder entity={entity} context={additionalFieldContext}/>
442
443
  : <div className={"w-full"}>
443
- {additionalField.value?.({
444
- entity,
445
- context: additionalFieldContext
446
- })?.toString()}
444
+ <AdditionalFieldValue
445
+ field={additionalField}
446
+ entity={entity}
447
+ context={additionalFieldContext}/>
447
448
  </div>;
448
449
 
449
450
  return (
@@ -451,7 +452,7 @@ export function EntityForm<M extends Record<string, unknown>>({
451
452
  className={spanClass(field.span)}>
452
453
  <LabelWithIconAndTooltip
453
454
  propertyKey={field.key}
454
- icon={<AlignLeftIcon size={iconSize.small}/>}
455
+ icon={<AlignLeftIcon size={LABEL_ICON_SIZE}/>}
455
456
  title={additionalField.name}
456
457
  className={"text-text-secondary dark:text-text-secondary-dark"}/>
457
458
  <div className={cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-auto no-scrollbar")}>
@@ -537,12 +538,25 @@ export function EntityForm<M extends Record<string, unknown>>({
537
538
  }
538
539
  ];
539
540
 
540
- return <FormSections
541
- sections={sections}
542
- collapsed={collapsedSections}
543
- onToggle={toggleSection}
544
- errorKeys={errorKeys}
545
- renderField={renderField}/>;
541
+ return <>
542
+ <FormSections
543
+ sections={sections}
544
+ collapsed={collapsedSections}
545
+ onToggle={toggleSection}
546
+ errorKeys={errorKeys}
547
+ renderField={renderField}/>
548
+
549
+ {/* Folded back with the rail's fields, and for the same reason. The
550
+ rail renders only where it has been measured to fit, so on the
551
+ split pane, the side panel and the dialog this block was dropped
552
+ outright and the record's id was nowhere on screen but the
553
+ truncated chip in the bar. */}
554
+ {!showRail && layout.showRecordMeta && status === "existing" && entity && (
555
+ <div className={cls("mt-8 pt-5 border-t max-w-sm", defaultBorderMixin)}>
556
+ <RecordMeta entity={entity as Entity<Record<string, unknown>>}/>
557
+ </div>
558
+ )}
559
+ </>;
546
560
  };
547
561
 
548
562
  const formRef = useRef<HTMLDivElement>(null);
@@ -706,25 +720,6 @@ export function EntityForm<M extends Record<string, unknown>>({
706
720
  );
707
721
  }
708
722
 
709
- /**
710
- * Grid placement for a span.
711
- *
712
- * Written out as literal class strings rather than composed, because Tailwind
713
- * scans source text: a template literal would produce classes that exist at
714
- * runtime and were never generated into the stylesheet.
715
- *
716
- * Below the column breakpoint every field takes the single available column, so
717
- * the side panel, the dialog and mobile all get a plain stack.
718
- */
719
- function spanClass(span: 1 | 2 | 3 | 4): string {
720
- switch (span) {
721
- case 1: return "@2xl/col:col-span-1";
722
- case 2: return "@2xl/col:col-span-2";
723
- case 3: return "@2xl/col:col-span-3";
724
- case 4: return "@2xl/col:col-span-4";
725
- }
726
- }
727
-
728
723
  function useOnAutoSave<M extends Record<string, unknown>>(autoSave: undefined | boolean, formex: FormexController<M>, lastSavedValues: React.MutableRefObject<EntityValues<M> | undefined>, save: (values: EntityValues<M>) => Promise<Entity<M> | void>) {
729
724
  useEffect(() => {
730
725
  if (!autoSave) return;
@@ -1,9 +1,33 @@
1
1
  import type { Property } from "@rebasepro/types";
2
+ import type { PropertySpan } from "@rebasepro/admin-types";
2
3
  import React from "react";
3
4
  import { cls, ErrorBoundary, Typography } from "@rebasepro/ui";
4
5
  import { getIconForProperty } from "../../util/property_utils";
5
6
  import { PropertyIdCopyTooltip } from "../../components/PropertyIdCopyTooltip";
6
7
 
8
+ /**
9
+ * Grid placement for a span.
10
+ *
11
+ * Written out as literal class strings rather than composed, because Tailwind
12
+ * scans source text: a template literal would produce classes that exist at
13
+ * runtime and were never generated into the stylesheet.
14
+ *
15
+ * Below the column breakpoint every field takes the single available column, so
16
+ * the side panel, the dialog and mobile all get a plain stack.
17
+ *
18
+ * Lives here rather than in the form, because the read-only rendering of a
19
+ * record places its fields on the same grid — a record that changes shape when
20
+ * you press Edit is the bug this whole layout exists to avoid.
21
+ */
22
+ export function spanClass(span: PropertySpan): string {
23
+ switch (span) {
24
+ case 1: return "@2xl/col:col-span-1";
25
+ case 2: return "@2xl/col:col-span-2";
26
+ case 3: return "@2xl/col:col-span-3";
27
+ case 4: return "@2xl/col:col-span-4";
28
+ }
29
+ }
30
+
7
31
  /**
8
32
  * Does this property's editor supply its own heading?
9
33
  *
@@ -46,28 +70,58 @@ export function isSelfLabellingProperty(property: Property | undefined): boolean
46
70
  }
47
71
  }
48
72
 
73
+ /**
74
+ * The type icon in a field label, in px.
75
+ *
76
+ * Matched to the label's 13px rather than taken from `iconSize.smallest` (16),
77
+ * which is sized for buttons. Exported so the surfaces that draw a label
78
+ * themselves — the additional-field cards, the rail — can stay in step.
79
+ */
80
+ export const LABEL_ICON_SIZE = 14;
81
+
49
82
  export interface FieldBlockProps {
50
83
  propertyKey: string;
51
84
  property?: Property;
52
85
  /** Rendered by the form rather than by the field, unless self-labelling. */
53
86
  showLabel: boolean;
87
+ /**
88
+ * Which of the two renderings this block frames.
89
+ *
90
+ * The **label is identical in both** — same size, same weight, same colour,
91
+ * same type icon, same required marker. A field is the same field whether or
92
+ * not you are editing it, and a record whose labels restyle themselves under
93
+ * Edit reads as two different screens.
94
+ *
95
+ * What `"read"` drops is the *description*, which is not part of the label:
96
+ * it is input help. "Sum of all line items before tax and shipping" under a
97
+ * number that is visibly a sum tells a reader nothing, and a line of it
98
+ * under every field is what a record has instead of room.
99
+ */
100
+ mode?: "edit" | "read";
54
101
  children: React.ReactNode;
55
102
  }
56
103
 
57
104
  /**
58
105
  * One field's worth of the form grid: label, control, description.
59
106
  *
60
- * The label carries a small type icon so a number reads differently from a
61
- * relation at a glance, and the description sits under the control where it
62
- * annotates what you just filled in.
107
+ * The same block frames the read-only rendering of a record, so a field sits in
108
+ * the same place, at the same width, under the same label whether or not you are
109
+ * editing it — see {@link EntityViewBinding}.
110
+ *
111
+ * The label is one size down from the control it names (13px against the body's
112
+ * 14) with its icon matched to it at 14px. Labels are scaffolding: you read them
113
+ * to find the value, then read the value. Setting them at body size made a
114
+ * column of records scan as a column of field names with data attached.
63
115
  */
64
116
  export function FieldBlock({
65
117
  propertyKey,
66
118
  property,
67
119
  showLabel,
120
+ mode = "edit",
68
121
  children
69
122
  }: FieldBlockProps) {
70
123
 
124
+ const reading = mode === "read";
71
125
  const description = property?.description?.trim();
72
126
  const required = Boolean(property?.validation?.required);
73
127
 
@@ -82,15 +136,18 @@ export function FieldBlock({
82
136
  {showLabel && (
83
137
  <PropertyIdCopyTooltip propertyKey={propertyKey}>
84
138
  <div className={cls(
85
- "flex items-center gap-1.5 text-sm font-medium leading-tight mb-1.5",
86
- "text-text-secondary dark:text-text-secondary-dark"
139
+ "flex items-center gap-1.5 font-medium leading-tight mb-1.5",
140
+ "text-[13px] text-text-secondary dark:text-text-secondary-dark"
87
141
  )}>
88
142
  {/* Small and quiet: enough to tell a number from a
89
143
  relation at a glance, not enough to compete with the
90
- field name. */}
144
+ field name. Matched to the label's 13px rather than
145
+ left at the 16px `smallest` token, which put the
146
+ heaviest mark on the row on the least important
147
+ thing in it. */}
91
148
  {property && (
92
149
  <span className={"shrink-0 text-text-disabled dark:text-text-disabled-dark"}>
93
- {getIconForProperty(property, "smallest")}
150
+ {getIconForProperty(property, LABEL_ICON_SIZE)}
94
151
  </span>
95
152
  )}
96
153
  <span className={"truncate"}>{property?.name ?? propertyKey}</span>
@@ -107,7 +164,7 @@ export function FieldBlock({
107
164
 
108
165
  {/* Under the control, where it reads as a note about the thing you
109
166
  just filled in rather than a subtitle of the next label. */}
110
- {showLabel && description && (
167
+ {showLabel && description && !reading && (
111
168
  <Typography variant={"caption"}
112
169
  color={"disabled"}
113
170
  className={"mt-1.5 ml-0.5 leading-snug"}>
@@ -32,7 +32,14 @@ export function FormRail({
32
32
  // No responsive hiding here. The caller decides whether the rail is
33
33
  // shown, by measuring; a CSS `hidden` would leave the fields the
34
34
  // resolver moved in here unreachable rather than folding them back.
35
- "flex flex-col gap-6 shrink-0 w-72 border-l overflow-y-auto",
35
+ //
36
+ // `w-76` (304px), between the old 288 and a full `w-80`. The rail
37
+ // holds real controls — a status select, a date picker — plus a
38
+ // record block whose id is a 36-character UUID, so 288 left the
39
+ // select tighter than the identical select in the column. 320 took
40
+ // more than it gave back: past about 300px the extra goes to the
41
+ // gap beside a chip, not to anything that needed it.
42
+ "flex flex-col gap-6 shrink-0 w-76 border-l overflow-y-auto",
36
43
  "px-5 py-6 bg-surface-50 dark:bg-surface-900",
37
44
  defaultBorderMixin
38
45
  )}>
@@ -47,7 +54,17 @@ export function FormRail({
47
54
  );
48
55
  }
49
56
 
50
- function RecordMeta({ entity }: { entity: Entity<Record<string, unknown>> }) {
57
+ /**
58
+ * The record's id and audit timestamps.
59
+ *
60
+ * Exported because the rail is not the only place it renders. The rail is shown
61
+ * only when it has been *measured* to fit, and every layout narrower than that —
62
+ * the split pane, the side panel, the dialog — folds its contents back into the
63
+ * column. The fields folded back but this block did not, so on three of the four
64
+ * layouts a record simply had no id on screen: the only copy of it was the
65
+ * truncated `e166a394…b422` chip in the identity bar.
66
+ */
67
+ export function RecordMeta({ entity }: { entity: Entity<Record<string, unknown>> }) {
51
68
 
52
69
  const createdAt = pickDate(entity.values, ["created_at", "createdAt", "created_on"]);
53
70
  const updatedAt = pickDate(entity.values, ["updated_at", "updatedAt", "modified_at"]);
@@ -14,6 +14,20 @@ export interface FormSectionsProps {
14
14
  */
15
15
  errorKeys: ReadonlySet<string>;
16
16
  renderField: (field: ResolvedFormField) => React.ReactNode;
17
+ /**
18
+ * Which rendering this is. `"read"` opens the gaps up rather than closing
19
+ * them: a control is a bordered 40px box that separates itself from the next
20
+ * one, and a value is bare text that does not. The form can also afford
21
+ * tighter rows because a description usually sits between them; with that
22
+ * gone, the same numbers ran a record's values together into one block.
23
+ */
24
+ mode?: "edit" | "read";
25
+ /**
26
+ * Renders one field as a summary row, for a section that asked for
27
+ * `readVariant: "summary"`. Only the read view supplies it; without it such
28
+ * a section falls back to the grid rather than to nothing.
29
+ */
30
+ renderSummaryField?: (field: ResolvedFormField, index: number, total: number) => React.ReactNode;
17
31
  }
18
32
 
19
33
  export function FormSections({
@@ -21,15 +35,22 @@ export function FormSections({
21
35
  collapsed,
22
36
  onToggle,
23
37
  errorKeys,
24
- renderField
38
+ renderField,
39
+ mode = "edit",
40
+ renderSummaryField
25
41
  }: FormSectionsProps) {
26
42
 
43
+ const reading = mode === "read";
44
+
27
45
  return (
28
- <div className={"flex flex-col gap-8"}>
46
+ <div className={cls("flex flex-col", reading ? "gap-10" : "gap-8")}>
29
47
  {sections.map((section) => {
30
48
 
31
49
  const hasError = section.fields.some(f => errorKeys.has(f.key));
32
50
  const isCollapsed = section.collapsible && collapsed.has(section.key) && !hasError;
51
+ const summary = reading
52
+ && section.readVariant === "summary"
53
+ && Boolean(renderSummaryField);
33
54
 
34
55
  return (
35
56
  <section key={section.key} className={"min-w-0"}>
@@ -43,15 +64,21 @@ export function FormSections({
43
64
  onToggle={() => onToggle(section.key)}/>
44
65
  )}
45
66
 
46
- {!isCollapsed && (
67
+ {!isCollapsed && (summary
68
+ ? <div id={`form_section_${section.key}`}
69
+ className={"flex flex-col min-w-0 @2xl:max-w-md @2xl:ml-auto"}>
70
+ {section.fields.map((field, index) =>
71
+ renderSummaryField!(field, index, section.fields.length))}
72
+ </div>
47
73
  // A container query, not a media query: the form is
48
74
  // rendered at four very different widths (full
49
75
  // screen, side panel, split pane, dialog) inside the
50
76
  // same viewport, so spans have to answer to the
51
77
  // column they are in, not to the window.
52
- <div id={`form_section_${section.key}`}
78
+ : <div id={`form_section_${section.key}`}
53
79
  className={cls(
54
- "grid gap-x-4 gap-y-5 min-w-0",
80
+ "grid min-w-0",
81
+ reading ? "gap-x-8 gap-y-7" : "gap-x-4 gap-y-5",
55
82
  "grid-cols-1 @2xl:grid-cols-4"
56
83
  )}>
57
84
  {section.fields.map(renderField)}
@@ -87,9 +87,15 @@ export function StorageUploadProgress({
87
87
  }, [entry.file, entry.fileName, upload]);
88
88
 
89
89
  if (simple) {
90
- return <div className={`w-${imageSize} h-${imageSize}`}>
91
-
92
- {loading && <Skeleton className={`w-${imageSize} h-${imageSize}`}/>}
90
+ // `imageSize` is a pixel count, so it has to be sized inline: `w-${n}`
91
+ // is neither a real Tailwind scale value nor a class the JIT can see.
92
+ return <div style={{
93
+ width: imageSize,
94
+ height: imageSize
95
+ }}>
96
+
97
+ {loading && <Skeleton width={imageSize}
98
+ height={imageSize}/>}
93
99
 
94
100
  </div>
95
101
  }
@@ -3,10 +3,11 @@ import React, { useLayoutEffect, useState } from "react";
3
3
  /**
4
4
  * Below this the form is a single column and the rail is not shown.
5
5
  *
6
- * Roughly the 288px rail plus a content column still wide enough to hold a
7
- * four-up row. Above it the rail is worth having even in the split view; below
8
- * it — a narrow split pane, the side panel, the dialog, a phone — its fields
9
- * fold back into the column.
6
+ * Roughly the 320px rail plus a content column still wide enough to hold a
7
+ * four-up row — at the threshold that leaves ~680px, which is four columns of
8
+ * ~145px once the gutters are taken out. Above it the rail is worth having even
9
+ * in the split view; below it — a narrow split pane, the side panel, the dialog,
10
+ * a phone — its fields fold back into the column.
10
11
  */
11
12
  export const RAIL_MIN_WIDTH = 1000;
12
13
 
@@ -53,7 +53,13 @@ export function ReadOnlyFieldBinding({
53
53
  "w-full overflow-x-scroll no-scrollbar flex items-center",
54
54
  skipCardWrapper
55
55
  ? ""
56
- : "rounded-lg border border-surface-200 dark:border-surface-700 px-4 md:px-6 min-h-10 opacity-80"
56
+ // `min-h-12` is `TextField`'s own `large` height, which is
57
+ // what every editable field in the form is. At 40px a
58
+ // read-only field sat 8px short of the input beside it,
59
+ // so a row holding one of each had its two boxes'
60
+ // baselines and bottom edges disagree — the field looked
61
+ // misaligned rather than uneditable.
62
+ : "rounded-lg border border-surface-200 dark:border-surface-700 px-4 md:px-6 min-h-12 opacity-80"
57
63
  )}>
58
64
 
59
65
  <ErrorBoundary>
@@ -41,11 +41,35 @@ export function useBuildCollectionRegistryController(props: {
41
41
  if (!collectionPath) return undefined;
42
42
 
43
43
  let collection: AdminCollection | undefined;
44
- try {
45
- collection = registry.resolvePathToCollections(collectionPath).finalCollection;
46
- } catch (e) {
47
- console.debug(`Could not resolve path to collection: ${collectionPath}`, e);
48
- return undefined;
44
+
45
+ // An exact slug match first, because a slug is allowed to contain
46
+ // slashes and several drivers need it to: a Firestore collection
47
+ // partitioned by locale is declared as `content/de-DE/podcasts`, and
48
+ // that string is its *name*, not a path to walk.
49
+ //
50
+ // Without this, such a slug went straight to `resolvePathToCollections`,
51
+ // which reads any `a/b/c` as collection/entity/subcollection, looked for
52
+ // a root collection called `content`, found none and threw — and the
53
+ // catch below turned that into `undefined`. `RebaseRoute` renders
54
+ // `null` for an unresolved collection, so the whole view came out blank
55
+ // with nothing in the console above `debug`. Thirty-five collections in
56
+ // one app were unreachable that way.
57
+ // `collectionPath`, not `cleanedPath`: an even segment count means the
58
+ // path ends at a record, and the id has already been trimmed off above.
59
+ // Matching the raw path instead would only ever hit for a collection
60
+ // route, leaving `content/de-DE/podcasts/abc123` back on the walker.
61
+ collection = registry.get(collectionPath) as AdminCollection | undefined;
62
+
63
+ if (!collection) {
64
+ try {
65
+ collection = registry.resolvePathToCollections(collectionPath).finalCollection;
66
+ } catch (e) {
67
+ // Warn, not debug. An unresolved collection ends as a route with
68
+ // nothing in it, and at `debug` there was no way to tell that
69
+ // from a collection that legitimately has no rows.
70
+ console.warn(`[rebase] Could not resolve a collection for "${collectionPath}"`, e);
71
+ return undefined;
72
+ }
49
73
  }
50
74
 
51
75
  if (!collection) {