@rebasepro/app 0.16.0 → 0.16.1-canary.g0d7af95

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 (50) hide show
  1. package/dist/auth/api.d.ts +8 -21
  2. package/dist/auth/index.d.ts +4 -4
  3. package/dist/auth/types.d.ts +1 -1
  4. package/dist/auth/useRebaseAuthController.d.ts +1 -1
  5. package/dist/collections/index.d.ts +13 -13
  6. package/dist/collections/title-property.d.ts +1 -1
  7. package/dist/components/Debug/collection-views/index.d.ts +3 -3
  8. package/dist/components/LoginView/LoginView.d.ts +2 -2
  9. package/dist/components/LoginView/index.d.ts +2 -2
  10. package/dist/components/common/index.d.ts +6 -6
  11. package/dist/components/common/useDataTableController.d.ts +1 -1
  12. package/dist/components/index.d.ts +18 -18
  13. package/dist/contexts/AdminModeController.d.ts +1 -1
  14. package/dist/contexts/ModeController.d.ts +1 -1
  15. package/dist/contexts/index.d.ts +18 -18
  16. package/dist/core/Rebase.d.ts +1 -1
  17. package/dist/core/index.d.ts +4 -4
  18. package/dist/hooks/index.d.ts +36 -36
  19. package/dist/hooks/useBridgeRegistration.d.ts +1 -1
  20. package/dist/hooks/useBuildAdminModeController.d.ts +1 -1
  21. package/dist/hooks/useBuildModeController.d.ts +1 -1
  22. package/dist/hooks/useUnsavedChangesDialog.d.ts +1 -1
  23. package/dist/index.d.ts +19 -19
  24. package/dist/index.es.js +129 -79
  25. package/dist/index.es.js.map +1 -1
  26. package/dist/util/index.d.ts +9 -8
  27. package/dist/util/local_storage.d.ts +31 -0
  28. package/package.json +9 -9
  29. package/src/auth/api.ts +8 -21
  30. package/src/auth/useRebaseAuthController.ts +6 -3
  31. package/src/collections/entity-display.ts +6 -34
  32. package/src/collections/form-layout.ts +2 -1
  33. package/src/collections/title-property.ts +1 -1
  34. package/src/components/Debug/UIReferenceView.tsx +53 -16
  35. package/src/components/LoginView/LoginView.tsx +3 -3
  36. package/src/components/common/useDataTableController.tsx +20 -1
  37. package/src/core/Rebase.tsx +3 -24
  38. package/src/hooks/useBuildAdminModeController.tsx +3 -4
  39. package/src/hooks/useBuildEffectiveRoleController.tsx +6 -7
  40. package/src/hooks/useBuildModeController.tsx +8 -6
  41. package/src/i18n/RebaseI18nProvider.tsx +5 -6
  42. package/src/locales/de.ts +0 -1
  43. package/src/locales/en.ts +0 -1
  44. package/src/locales/es.ts +0 -1
  45. package/src/locales/fr.ts +0 -1
  46. package/src/locales/hi.ts +0 -1
  47. package/src/locales/it.ts +0 -1
  48. package/src/locales/pt.ts +0 -1
  49. package/src/util/index.ts +1 -0
  50. package/src/util/local_storage.ts +66 -0
@@ -1,8 +1,9 @@
1
- export * from "./icon_list";
2
- export * from "./icons";
3
- export * from "./createFormexStub";
4
- export * from "./entity_cache";
5
- export * from "./useStorageUploadController";
6
- export * from "./previews";
7
- export * from "./enums";
8
- export * from "./constants";
1
+ export * from "./icon_list.js";
2
+ export * from "./icons.js";
3
+ export * from "./createFormexStub.js";
4
+ export * from "./entity_cache.js";
5
+ export * from "./useStorageUploadController.js";
6
+ export * from "./previews.js";
7
+ export * from "./enums.js";
8
+ export * from "./constants.js";
9
+ export * from "./local_storage.js";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * localStorage that tolerates not being there.
3
+ *
4
+ * `typeof window !== "undefined"` was the guard these call sites used, and it
5
+ * answers the wrong question: it says "am I in a browser", not "can I read
6
+ * storage". Three things make that distinction real rather than theoretical:
7
+ *
8
+ * - A browser can have storage **disabled**. Safari in private mode, a blocked
9
+ * third-party cookie policy, and a sandboxed iframe all give you a `window`
10
+ * whose `localStorage` property *throws on access* — a SecurityError raised
11
+ * before any method is called, which no amount of null-checking the result
12
+ * catches.
13
+ * - A test environment can be **half torn down**. jsdom is disposed at the end
14
+ * of a file while React is still finishing a concurrent render, so `window`
15
+ * survives as a husk and `localStorage.getItem` is no longer a function.
16
+ * That is how this was found: `TypeError: localStorage.getItem is not a
17
+ * function`, thrown out of a render, failing a suite in which every one of
18
+ * 709 tests passed.
19
+ * - Server rendering has no `window` at all, which is the only case the old
20
+ * guard actually covered.
21
+ *
22
+ * Reading a UI preference is never worth a crash. Every function here fails to
23
+ * `null` / no-op, so a user with storage switched off gets the default
24
+ * appearance instead of a blank page.
25
+ */
26
+ /** The stored string for `key`, or null if it is unset or unreadable. */
27
+ export declare function readStoredString(key: string): string | null;
28
+ /** Persist `value` under `key`. A no-op when storage is unavailable. */
29
+ export declare function writeStoredString(key: string, value: string): void;
30
+ /** Remove `key`. A no-op when storage is unavailable. */
31
+ export declare function removeStoredString(key: string): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/app",
3
3
  "type": "module",
4
- "version": "0.16.0",
4
+ "version": "0.16.1-canary.g0d7af95",
5
5
  "description": "Rebase core — framework-agnostic runtime for data-driven admin panels",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -52,12 +52,12 @@
52
52
  "magic-string": "^0.30.0",
53
53
  "notistack": "^3.0.2",
54
54
  "react-i18next": "^17.0.11",
55
- "@rebasepro/admin-types": "0.16.0",
56
- "@rebasepro/forms": "0.16.0",
57
- "@rebasepro/common": "0.16.0",
58
- "@rebasepro/types": "0.16.0",
59
- "@rebasepro/ui": "0.16.0",
60
- "@rebasepro/utils": "0.16.0"
55
+ "@rebasepro/common": "0.16.1-canary.g0d7af95",
56
+ "@rebasepro/types": "0.16.1-canary.g0d7af95",
57
+ "@rebasepro/admin-types": "0.16.1-canary.g0d7af95",
58
+ "@rebasepro/utils": "0.16.1-canary.g0d7af95",
59
+ "@rebasepro/forms": "0.16.1-canary.g0d7af95",
60
+ "@rebasepro/ui": "0.16.1-canary.g0d7af95"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "react": ">=19.2.7",
@@ -104,7 +104,7 @@
104
104
  "jest": {
105
105
  "transform": {
106
106
  "^.+\\.tsx?$": "ts-jest",
107
- "^.+\\.m?jsx?$": "<rootDir>/../../scripts/jest/react-router-esm-transform.cjs"
107
+ "^.+\\.m?jsx?$": "<rootDir>/../../tooling/scripts/jest/react-router-esm-transform.cjs"
108
108
  },
109
109
  "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
110
110
  "moduleFileExtensions": [
@@ -135,7 +135,7 @@
135
135
  },
136
136
  "scripts": {
137
137
  "watch": "vite build --watch",
138
- "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && pnpm exec esbuild src/vitePlugin.ts --platform=node --format=esm --outfile=dist/vitePlugin.js && node ../../scripts/assert-build-output.mjs",
138
+ "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../tooling/scripts/add-dts-extensions.mjs dist && pnpm exec esbuild src/vitePlugin.ts --platform=node --format=esm --outfile=dist/vitePlugin.js && node ../../tooling/scripts/assert-build-output.mjs",
139
139
  "test:lint": "eslint \"src/**\" --quiet",
140
140
  "test": "jest --passWithNoTests",
141
141
  "clean": "rm -rf dist && find ./src \\( -name '*.js' -o -name '*.d.ts' \\) -type f | xargs rm -f",
package/src/auth/api.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { RebaseApiError } from "@rebasepro/types";
13
+ import type { AuthAdapterCapabilities } from "@rebasepro/types";
13
14
  import { DEFAULT_API_PATH } from "../hooks/ApiConfigContext";
14
15
 
15
16
  async function handleResponse<T>(response: Response): Promise<T> {
@@ -59,28 +60,14 @@ async function fetchWithHandling(input: RequestInfo | URL, init?: RequestInit):
59
60
  }
60
61
 
61
62
  /**
62
- * Auth config response from the backend
63
+ * What `GET /api/auth/config` answers.
64
+ *
65
+ * The wire contract is declared once, in `@rebasepro/types`, and re-exported
66
+ * here under the name this module uses. Three near-copies of it used to exist —
67
+ * this one listed an `emailServiceEnabled` flag no backend sends, and marked as
68
+ * optional fields every backend always returns.
63
69
  */
64
- export interface AuthConfigResponse {
65
- /** True when there are no users in the system and first user setup is needed */
66
- needsSetup: boolean;
67
- /** Whether new user registration is enabled */
68
- registrationEnabled: boolean;
69
- /** Whether email service is configured */
70
- emailServiceEnabled?: boolean;
71
- /** Whether the self-service password reset flow (email link) is supported */
72
- passwordReset?: boolean;
73
- /**
74
- * Whether an admin can reset another user's password, i.e. the adapter
75
- * mounts `POST /admin/users/:userId/reset-password`. Absent on backends
76
- * predating this field, which the frontend treats as supported.
77
- */
78
- adminPasswordReset?: boolean;
79
- /** Whether email verification is supported */
80
- emailVerification?: boolean;
81
- /** Complete list of enabled OAuth provider IDs (e.g. ["google", "github", "discord"]) */
82
- enabledProviders: string[];
83
- }
70
+ export type AuthConfigResponse = AuthAdapterCapabilities;
84
71
 
85
72
  /**
86
73
  * Cache container for `fetchAuthConfig` — holds both the inflight promise
@@ -325,13 +325,16 @@ export function useRebaseAuthController(
325
325
  setAuthProviderError,
326
326
  extra,
327
327
  setExtra,
328
+ // `authConfig` is null until `GET /auth/config` lands, so each fallback
329
+ // is what to assume *while it is still in flight* — not a stand-in for a
330
+ // backend that might omit the field. The backend always sends all of them.
328
331
  capabilities: {
329
332
  emailPasswordLogin: true,
330
333
  googleLogin: !!(props.googleClientId),
331
- registration: authConfig?.registrationEnabled ?? false,
334
+ registrationEnabled: authConfig?.registrationEnabled ?? false,
332
335
  passwordReset: authConfig?.passwordReset ?? false,
333
- // Defaults to true so backends predating this field keep showing the
334
- // action. Adapters that don't support it report `false` explicitly.
336
+ // True while loading: the admin UI's "Reset Password" action is worth
337
+ // showing optimistically and hiding once a backend says it cannot.
335
338
  adminPasswordReset: authConfig?.adminPasswordReset ?? true,
336
339
  sessionManagement: true,
337
340
  profileUpdate: true,
@@ -13,50 +13,22 @@ import type {
13
13
  EntityDisplayRole
14
14
  } from "@rebasepro/admin-types";
15
15
 
16
- /**
17
- * Collections that have already warned about a deprecated field, so a list of
18
- * fifty rows produces one line in the console rather than fifty.
19
- */
20
- const deprecationWarned = new Set<string>();
21
-
22
- function collectionId(collection: AdminCollection<never> | AdminCollection<any>): string {
23
- return (collection.slug ?? collection.name ?? "collection") as string;
24
- }
25
-
26
- function warnOnce(id: string, message: string): void {
27
- const key = `${id}:${message}`;
28
- if (deprecationWarned.has(key)) return;
29
- deprecationWarned.add(key);
30
- console.warn(message);
31
- }
32
-
33
16
  /**
34
17
  * What the collection declares for a role, before deciding which form it is.
35
18
  *
36
- * `display.title` wins over the deprecated `titleProperty`: a collection setting
37
- * both is mid-migration, and the new field is the one it means.
19
+ * One field states it: `display[role]`. `admin.titleProperty` used to be a
20
+ * second way to say `display.title` and is gone — a role with two spellings is
21
+ * a role with two readers, and this one had grown seven.
38
22
  */
39
23
  function getDeclaredSource<M extends Record<string, unknown>>(
40
24
  collection: AdminCollection<M>,
41
25
  role: EntityDisplayRole
42
26
  ): string | EntityDisplayResolver<M, unknown> | undefined {
43
-
44
27
  const display = collection.display as Record<string, unknown> | undefined;
45
28
  const declared = display?.[role];
46
- if (declared !== undefined) return declared as string | EntityDisplayResolver<M, unknown>;
47
-
48
- if (role === "title" && collection.titleProperty) {
49
- const id = collectionId(collection);
50
- warnOnce(
51
- id,
52
- `[rebase] Collection "${id}" uses admin.titleProperty, which is deprecated. ` +
53
- `Move it to admin.display.title — the same string works there, and display.title ` +
54
- `also accepts a resolver for a title the record does not carry.`
55
- );
56
- return collection.titleProperty as string;
57
- }
58
-
59
- return undefined;
29
+ return declared === undefined
30
+ ? undefined
31
+ : declared as string | EntityDisplayResolver<M, unknown>;
60
32
  }
61
33
 
62
34
  /**
@@ -18,6 +18,7 @@ import type {
18
18
  PropertySpan
19
19
  } from "@rebasepro/admin-types";
20
20
  import type { Property } from "@rebasepro/types";
21
+ import { getDisplayPropertyKey } from "./entity-display";
21
22
  import { isHidden } from "./property_presentation";
22
23
 
23
24
  /**
@@ -278,7 +279,7 @@ export function resolveFormLayout<M extends Record<string, unknown>>({
278
279
  }: ResolveFormLayoutParams<M>): ResolvedFormLayout {
279
280
 
280
281
  const config = collection.form;
281
- const titlePropertyKey = collection.titleProperty as string | undefined;
282
+ const titlePropertyKey = getDisplayPropertyKey(collection, "title");
282
283
 
283
284
  const available = new Map<string, ResolvedFormField>();
284
285
  for (const key of fieldKeys) {
@@ -224,7 +224,7 @@ index });
224
224
  *
225
225
  * Returns `undefined` when that first property is anything but a
226
226
  * single-cardinality relation, or when the collection states its own
227
- * `titleProperty`.
227
+ * `display.title`.
228
228
  *
229
229
  * @group Collections
230
230
  */
@@ -118,7 +118,12 @@ export function UIReferenceView() {
118
118
  <div className="flex w-full">
119
119
 
120
120
  {/* ── Sidebar nav (same structure as DefaultDrawer) ─────────────── */}
121
- <div className={cls("flex flex-col sticky top-0 h-screen grow-0 shrink-0 w-[200px] border-r", defaultBorderMixin)}>
121
+ {/* 232px, not 200px. The icon rail is 56px and the label is uppercase
122
+ at 12px, which left ~128px — one character short of "COLLECTION
123
+ TABLE", so the longest entry in the list rendered as
124
+ "COLLECTION TA…". This is the reference's own nav, not the shared
125
+ DefaultDrawer, so widening it costs no copy-fidelity. */}
126
+ <div className={cls("flex flex-col sticky top-0 h-screen grow-0 shrink-0 w-[232px] border-r", defaultBorderMixin)}>
122
127
  {/* DrawerLogo */}
123
128
  <div className="flex flex-row items-center shrink-0 pt-4 pb-2 px-2">
124
129
  <div className="shrink-0 flex items-center justify-center w-[56px] h-[40px]">
@@ -187,7 +192,14 @@ export function UIReferenceView() {
187
192
  </div>
188
193
 
189
194
  {/* ── Main content area ───────────────────────────────────────────── */}
190
- <div ref={scrollContainerRef} className="flex-1">
195
+ {/* `min-w-0` is required, not cosmetic. A flex item defaults to
196
+ `min-width: auto`, so this column sized itself to its widest
197
+ descendant — the 1451px CRM dashboard — and pushed the whole page
198
+ past the viewport, cutting the collection table's last columns
199
+ off screen. With `min-w-0` the column can shrink to the space
200
+ available and the `overflow-x-auto` on the wide mocks actually
201
+ engages instead of being overruled from above. */}
202
+ <div ref={scrollContainerRef} className="flex-1 min-w-0">
191
203
 
192
204
  {/* ═══════════════════════════════════════════════
193
205
  SECTION: Drawer
@@ -546,7 +558,7 @@ selected: true }, { name: "Tags" }].map(c => (
546
558
  <Typography variant="caption" color="secondary" className="block mb-1">Property Editor (no selection)</Typography>
547
559
  <div className={cls("flex flex-col items-center justify-center h-48 w-[320px] border rounded-lg", defaultBorderMixin)}>
548
560
  <div className="flex flex-col items-center justify-center h-full gap-4">
549
- <Typography variant="label">
561
+ <Typography variant="label" className="text-center px-4">
550
562
  Select a property to edit it
551
563
  </Typography>
552
564
  <Button>
@@ -562,7 +574,7 @@ selected: true }, { name: "Tags" }].map(c => (
562
574
  <Typography variant="caption" color="secondary" className="block mb-1">Property Editor (empty collection)</Typography>
563
575
  <div className={cls("flex flex-col items-center justify-center h-48 w-[320px] border rounded-lg", defaultBorderMixin)}>
564
576
  <div className="flex flex-col items-center justify-center h-full gap-4">
565
- <Typography variant="label">
577
+ <Typography variant="label" className="text-center px-4">
566
578
  Now you can add your first property
567
579
  </Typography>
568
580
  <Button>
@@ -578,7 +590,7 @@ selected: true }, { name: "Tags" }].map(c => (
578
590
  <Typography variant="caption" color="secondary" className="block mb-1">Collection ListIcon (no selection)</Typography>
579
591
  <div className={cls("flex flex-col items-center justify-center h-48 w-[320px] border rounded-lg", defaultBorderMixin)}>
580
592
  <div className="flex flex-col items-center justify-center h-full gap-4">
581
- <Typography variant="label">
593
+ <Typography variant="label" className="text-center px-4">
582
594
  Select a collection or create a new one to start editing
583
595
  </Typography>
584
596
  <Button>
@@ -685,7 +697,15 @@ selected: true }, { name: "Tags" }].map(c => (
685
697
  </div>
686
698
  <div>
687
699
  <Typography variant="caption" color="secondary" className="block mb-2 font-mono">IconButton — sizes</Typography>
688
- <div className="flex flex-wrap gap-3 items-center">
700
+ {/* `items-end`, matching the Button size ramp above.
701
+ Each entry is a column of button-over-label, and the
702
+ buttons step 28→32→40→48px. Centring the columns
703
+ against each other scattered the captions across a
704
+ 10px band, so a row meant to read as an even ramp
705
+ read as ragged. Aligning the ends puts every caption
706
+ on one baseline and lets the size difference show in
707
+ the buttons, which is the point of the row. */}
708
+ <div className="flex flex-wrap gap-3 items-end">
689
709
  {([
690
710
  { s: "smallest" as const, icon: <PencilIcon size={14}/> },
691
711
  { s: "small" as const, icon: <PencilIcon size={16}/> },
@@ -956,11 +976,15 @@ roles: [] }
956
976
  {/* ═══════════════════════════════════════════════
957
977
  SECTION: CRM Dashboard
958
978
  ═══════════════════════════════════════════════ */}
959
- <SectionBlock id="crm-dashboard" title="CRM Dashboard — CrmDashboardDemo">
979
+ <SectionBlock wide id="crm-dashboard" title="CRM Dashboard — CrmDashboardDemo">
960
980
  <Typography variant="body2" color="secondary" className="mb-4">
961
981
  A real-world showcase of a complex dashboard home page incorporating Rebase design language.
962
982
  </Typography>
963
- <div className="w-full">
983
+ {/* The dashboard is intrinsically ~1483px wide — wider than the
984
+ content column at any laptop size. Scrolling it inside its
985
+ own container is what stops it pushing the section, and is
986
+ the same rule wide tables follow everywhere else. */}
987
+ <div className="w-full overflow-x-auto">
964
988
  <CrmDashboardDemo />
965
989
  </div>
966
990
  </SectionBlock>
@@ -968,7 +992,7 @@ roles: [] }
968
992
  {/* ═══════════════════════════════════════════════
969
993
  SECTION: Collection Table
970
994
  ═══════════════════════════════════════════════ */}
971
- <SectionBlock id="collection-table" title="Collection Table">
995
+ <SectionBlock wide id="collection-table" title="Collection Table">
972
996
  <Typography variant="body2" color="secondary" className="mb-4">
973
997
  Sample data collection rendered as a sortable, searchable, filterable table with row selection.
974
998
  Uses <code className="font-mono text-xs">Table</code>, <code className="font-mono text-xs">Chip</code>,{" "}
@@ -981,7 +1005,7 @@ roles: [] }
981
1005
  {/* ═══════════════════════════════════════════════
982
1006
  SECTION: Card View
983
1007
  ═══════════════════════════════════════════════ */}
984
- <SectionBlock id="card-view" title="Card View">
1008
+ <SectionBlock wide id="card-view" title="Card View">
985
1009
  <Typography variant="body2" color="secondary" className="mb-4">
986
1010
  Grid of entity cards with thumbnail area, progress bar, status/priority chips, and assignee.
987
1011
  Uses <code className="font-mono text-xs">Card</code>, <code className="font-mono text-xs">Chip</code>,{" "}
@@ -993,24 +1017,37 @@ roles: [] }
993
1017
  {/* ═══════════════════════════════════════════════
994
1018
  SECTION: Kanban Board
995
1019
  ═══════════════════════════════════════════════ */}
996
- <SectionBlock id="kanban-board" title="Kanban Board">
1020
+ <SectionBlock wide id="kanban-board" title="Kanban Board">
997
1021
  <Typography variant="body2" color="secondary" className="mb-4">
998
1022
  Drag-and-drop kanban board with four status columns.
999
1023
  Uses <code className="font-mono text-xs">KanbanView</code>, <code className="font-mono text-xs">BoardItem</code>,{" "}
1000
1024
  <code className="font-mono text-xs">BoardItemViewProps</code>.
1001
1025
  </Typography>
1002
- <div className="w-full"><KanbanBoardDemo /></div>
1026
+ <div className="w-full overflow-x-auto"><KanbanBoardDemo /></div>
1003
1027
  </SectionBlock>
1004
1028
  </div>
1005
1029
  </div>
1006
1030
  );
1007
1031
  }
1008
1032
 
1009
- function SectionBlock({ id, title, children }: { id: string; title: string; children: React.ReactNode }) {
1033
+ function SectionBlock({ id, title, wide, children }: { id: string; title: string; wide?: boolean; children: React.ReactNode }) {
1010
1034
  return (
1011
- <section id={id} className={cls("px-6 py-8 border-b scroll-mt-0 max-w-5xl", defaultBorderMixin)}>
1012
- <Typography variant="h5" className="mb-1">{title}</Typography>
1013
- <div className="mt-4">{children}</div>
1035
+ // `max-w-5xl` and `border-b` used to share this element, and they cannot.
1036
+ // Several mocks here are app-scale and intrinsically wider than 64rem
1037
+ // (the CRM dashboard reaches 1483px), so the section's own rule stopped
1038
+ // at 1264px while its content ran past it — every such section read as a
1039
+ // broken edge.
1040
+ //
1041
+ // Split instead: the SECTION spans the column, so its rule always
1042
+ // reaches its own edges; the CONTENT keeps a measure, so a form column
1043
+ // never stretches to 700px on a wide monitor. `wide` opts the app-scale
1044
+ // mocks out of the measure — they pair it with `overflow-x-auto` so they
1045
+ // scroll inside themselves rather than pushing the page.
1046
+ <section id={id} className={cls("px-6 py-8 border-b scroll-mt-0", defaultBorderMixin)}>
1047
+ <div className={wide ? undefined : "max-w-5xl"}>
1048
+ <Typography variant="h5" className="mb-1">{title}</Typography>
1049
+ <div className="mt-4">{children}</div>
1050
+ </div>
1014
1051
  </section>
1015
1052
  );
1016
1053
  }
@@ -71,7 +71,7 @@ export interface LoginViewProps {
71
71
 
72
72
  /**
73
73
  * Prevent users from creating new accounts.
74
- * If not set, checks `authController.capabilities.registration`.
74
+ * If not set, checks `authController.capabilities.registrationEnabled`.
75
75
  */
76
76
  disableSignupScreen?: boolean;
77
77
 
@@ -130,7 +130,7 @@ export interface LoginViewProps {
130
130
 
131
131
  /**
132
132
  * Whether registration is enabled.
133
- * If not set, derived from `authController.capabilities.registration`.
133
+ * If not set, derived from `authController.capabilities.registrationEnabled`.
134
134
  */
135
135
  registrationEnabled?: boolean;
136
136
 
@@ -296,7 +296,7 @@ export function LoginView({
296
296
  const isBootstrapMode = needsSetup
297
297
  ?? ("needsSetup" in authController && !!(authController as { needsSetup?: boolean }).needsSetup)
298
298
  ?? false;
299
- const canRegister = registrationEnabled ?? caps.registration ?? false;
299
+ const canRegister = registrationEnabled ?? caps.registrationEnabled ?? false;
300
300
  // Every OAuth button needs BOTH halves: a client id here and the matching
301
301
  // provider configured on the backend. `enabledProviders` is always an array
302
302
  // (the controller defaults it to `[]`), so a missing entry means "the
@@ -210,7 +210,26 @@ export function useDataTableController<M extends Record<string, any> = any, USER
210
210
  const [dataLoadingError, setDataLoadingError] = useState<Error | undefined>();
211
211
  const [noMoreToLoad, setNoMoreToLoad] = useState<boolean>(false);
212
212
 
213
- const clearFilter = useCallback(() => setFilterValues(fixedFilter ?? defaultFilter ?? undefined), [fixedFilter, defaultFilter]);
213
+ /**
214
+ * Clear the user's filters. `fixedFilter` survives — it is the collection
215
+ * forcing a scope, not something the user chose — but `defaultFilter` does
216
+ * not: a default is where the view *opens*, not a floor it can never go
217
+ * below.
218
+ *
219
+ * It used to reset to `defaultFilter`, which made "clear" unable to clear
220
+ * whenever a collection defined one, and every caller here wanted the
221
+ * opposite. `ClearFilterSortButton` is labelled "clear filter" and left one
222
+ * set. `EditorCollectionActionStart` calls this and then re-applies
223
+ * `collection.defaultFilter` on the next line — redundant under the old
224
+ * behaviour, and the clearest statement of what it expected.
225
+ * `FilterPresetsButton` documents "clearing all filters deactivates all
226
+ * chips", which was false: it derives a chip's active state from the
227
+ * filters, so a preset whose values equalled `defaultFilter` switched
228
+ * itself straight back on. That chip could not be turned off at all — click
229
+ * it, the keys are removed, the filter resets to the identical default, and
230
+ * the chip lights up again.
231
+ */
232
+ const clearFilter = useCallback(() => setFilterValues(fixedFilter ?? undefined), [fixedFilter]);
214
233
 
215
234
  const updateFilterValues = useCallback((updatedFilter: FilterValues<Extract<keyof M, string> | (string & {})> | undefined) => {
216
235
  if (fixedFilter) {
@@ -6,7 +6,7 @@ import type { ComponentOverrideMap } from "@rebasepro/admin-types";
6
6
  import React, { useEffect, useMemo, useRef, useState } from "react";
7
7
  import { CenteredView, Typography } from "@rebasepro/ui";
8
8
  import { User, CollectionRegistryController, DataDriver, DataSourceDefinition, RebaseData, DEFAULT_DATA_SOURCE_KEY, StorageSource, StorageSourceDefinition, DEFAULT_STORAGE_SOURCE_KEY } from "@rebasepro/types";
9
- import { RebaseContext, RENAMED_SLOTS, UNRENDERED_SLOTS } from "@rebasepro/admin-types";
9
+ import { RebaseContext, UNRENDERED_SLOTS } from "@rebasepro/admin-types";
10
10
  import { PluginProviderStack } from "./PluginProviderStack";
11
11
  import { PluginLifecycleManager } from "./PluginLifecycleManager";
12
12
  import { AuthControllerContext, CollectionResolverRegistrationContext, CollectionResolver } from "../contexts";
@@ -85,32 +85,11 @@ export function Rebase<USER extends User, DB = unknown>(props: RebaseProps<USER,
85
85
  }
86
86
  }
87
87
 
88
- // Merge direct slots with plugin slots, rewriting any retired slot name to
89
- // the one it was renamed to. `useSlot` matches on string equality, so a
90
- // contribution left on the old name would register cleanly and render
91
- // nothing — the same silent nothing `UNRENDERED_SLOTS` warns about below.
88
+ // Merge direct slots with plugin slots.
92
89
  const resolvedSlots: SlotContribution[] = useMemo(() => [
93
90
  ...directSlots,
94
91
  ...((plugins ?? []).flatMap((p) => p.slots ?? []))
95
- ].map((contribution) => {
96
- const current = RENAMED_SLOTS[contribution.slot];
97
- return current ? { ...contribution, slot: current } : contribution;
98
- }), [directSlots, plugins]);
99
-
100
- // Warn once per retired name actually in use. This reads the *pre-rewrite*
101
- // names off the inputs rather than `resolvedSlots`, which no longer has
102
- // them — the rewrite above is what makes the plugin keep working, and this
103
- // is what tells its author to stop relying on it.
104
- useEffect(() => {
105
- const registered = [...directSlots, ...((plugins ?? []).flatMap((p) => p.slots ?? []))];
106
- for (const legacy of new Set(registered.map(s => s.slot).filter(slot => slot in RENAMED_SLOTS))) {
107
- console.warn(
108
- `[Rebase] The "${legacy}" slot has been renamed to "${RENAMED_SLOTS[legacy]}". ` +
109
- "The contribution was redirected and still renders, but the old name will be " +
110
- "removed in a future major version."
111
- );
112
- }
113
- }, [directSlots, plugins]);
92
+ ], [directSlots, plugins]);
114
93
 
115
94
  // Seven of the twenty-nine declared slots are rendered nowhere. They have
116
95
  // props interfaces and rows in the public slot reference, so registering
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo, useState } from "react";
2
2
  import type { AdminModeController } from "./useAdminModeController";
3
+ import { readStoredString, writeStoredString } from "../util/local_storage";
3
4
 
4
5
  /**
5
6
  * Use this hook to build an admin mode controller that determines
@@ -7,13 +8,11 @@ import type { AdminModeController } from "./useAdminModeController";
7
8
  */
8
9
  export function useBuildAdminModeController(): AdminModeController {
9
10
 
10
- const savedMode = typeof window !== "undefined" ? localStorage.getItem("rebase-admin-mode") as "content" | "studio" | "settings" | null : null;
11
+ const savedMode = readStoredString("rebase-admin-mode") as "content" | "studio" | "settings" | null;
11
12
  const [mode, setMode] = useState<"content" | "studio" | "settings">(savedMode ?? "content");
12
13
 
13
14
  const setModeInternal = useCallback((newMode: "content" | "studio" | "settings") => {
14
- if (typeof window !== "undefined") {
15
- localStorage.setItem("rebase-admin-mode", newMode);
16
- }
15
+ writeStoredString("rebase-admin-mode", newMode);
17
16
  setMode(newMode);
18
17
  }, []);
19
18
 
@@ -1,5 +1,6 @@
1
1
  import type { EffectiveRoleController } from "@rebasepro/types";
2
2
  import { useCallback, useState, useMemo } from "react";
3
+ import { readStoredString, removeStoredString, writeStoredString } from "../util/local_storage";
3
4
  ;
4
5
 
5
6
  /**
@@ -10,16 +11,14 @@ import { useCallback, useState, useMemo } from "react";
10
11
  */
11
12
  export function useBuildEffectiveRoleController(): EffectiveRoleController {
12
13
 
13
- const savedRole = typeof window !== "undefined" ? localStorage.getItem("rebase-effective-role") : null;
14
+ const savedRole = readStoredString("rebase-effective-role");
14
15
  const [effectiveRole, setEffectiveRole] = useState<string | null>(savedRole ?? null);
15
16
 
16
17
  const setRoleInternal = useCallback((newRole: string | null) => {
17
- if (typeof window !== "undefined") {
18
- if (newRole) {
19
- localStorage.setItem("rebase-effective-role", newRole);
20
- } else {
21
- localStorage.removeItem("rebase-effective-role");
22
- }
18
+ if (newRole) {
19
+ writeStoredString("rebase-effective-role", newRole);
20
+ } else {
21
+ removeStoredString("rebase-effective-role");
23
22
  }
24
23
  setEffectiveRole(newRole);
25
24
  }, []);
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useState, useMemo } from "react";
2
2
 
3
3
  import type { ModeController } from "./useModeController";
4
+ import { readStoredString, removeStoredString, writeStoredString } from "../util/local_storage";
4
5
 
5
6
  /**
6
7
  * Use this hook to build a color mode controller that determines
@@ -16,8 +17,9 @@ export function useBuildModeController(): ModeController {
16
17
  }, []);
17
18
 
18
19
  const [mode, setMode] = useState<"light" | "dark">(() => {
19
- const prefersDarkModeStorage = typeof window !== "undefined" && localStorage.getItem("prefers-dark-mode") != null
20
- ? localStorage.getItem("prefers-dark-mode") === "true"
20
+ const storedPrefersDarkMode = readStoredString("prefers-dark-mode");
21
+ const prefersDarkModeStorage = storedPrefersDarkMode != null
22
+ ? storedPrefersDarkMode === "true"
21
23
  : null;
22
24
  const prefersDarkMode = prefersDarkModeStorage ?? prefersDarkModeQuery();
23
25
  return prefersDarkMode ? "dark" : "light";
@@ -26,7 +28,7 @@ export function useBuildModeController(): ModeController {
26
28
  useEffect(() => {
27
29
  const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
28
30
  const handleChange = (e: MediaQueryListEvent) => {
29
- if (localStorage.getItem("prefers-dark-mode") == null) {
31
+ if (readStoredString("prefers-dark-mode") == null) {
30
32
  setMode(e.matches ? "dark" : "light");
31
33
  setDocumentMode(e.matches ? "dark" : "light");
32
34
  }
@@ -50,16 +52,16 @@ export function useBuildModeController(): ModeController {
50
52
  const setModeInternal = useCallback((mode: "light" | "dark" | "system") => {
51
53
  if (mode === "light") {
52
54
  setDocumentMode("light");
53
- localStorage.setItem("prefers-dark-mode", "false");
55
+ writeStoredString("prefers-dark-mode", "false");
54
56
  setMode("light");
55
57
  } else if (mode === "dark") {
56
58
  setDocumentMode("dark");
57
- localStorage.setItem("prefers-dark-mode", "true");
59
+ writeStoredString("prefers-dark-mode", "true");
58
60
  setMode("dark");
59
61
  } else {
60
62
  const preferredMode = prefersDarkModeQuery() ? "dark" : "light";
61
63
  setDocumentMode(preferredMode);
62
- localStorage.removeItem("prefers-dark-mode");
64
+ removeStoredString("prefers-dark-mode");
63
65
  setMode(preferredMode);
64
66
  }
65
67
  }, [prefersDarkModeQuery]);