@rebasepro/app 0.11.1-canary.gfd39654 → 0.12.0

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/app",
3
3
  "type": "module",
4
- "version": "0.11.1-canary.gfd39654",
4
+ "version": "0.12.0",
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.8",
55
- "@rebasepro/common": "0.11.1-canary.gfd39654",
56
- "@rebasepro/types": "0.11.1-canary.gfd39654",
57
- "@rebasepro/ui": "0.11.1-canary.gfd39654",
58
- "@rebasepro/utils": "0.11.1-canary.gfd39654",
59
- "@rebasepro/admin-types": "0.11.1-canary.gfd39654",
60
- "@rebasepro/forms": "0.11.1-canary.gfd39654"
55
+ "@rebasepro/admin-types": "0.12.0",
56
+ "@rebasepro/types": "0.12.0",
57
+ "@rebasepro/ui": "0.12.0",
58
+ "@rebasepro/forms": "0.12.0",
59
+ "@rebasepro/utils": "0.12.0",
60
+ "@rebasepro/common": "0.12.0"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "react": ">=19.0.0",
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  ALL_WHERE_FILTER_OPS,
3
3
  DataType,
4
+ DEFAULT_FILTERABLE_RELATION_KINDS,
4
5
  getDataSourceCapabilities,
5
6
  Property,
7
+ RelationProperty,
6
8
  WhereFilterOp
7
9
  } from "@rebasepro/types";
8
10
 
@@ -30,6 +32,39 @@ const DEFAULT_OPS_BY_TYPE: Partial<Record<DataType, readonly WhereFilterOp[]>> =
30
32
  /** Operators offered when the property is an *array of* a filterable type. */
31
33
  const ARRAY_OPS: readonly WhereFilterOp[] = ["array-contains", "array-contains-any"];
32
34
 
35
+ /**
36
+ * Whether a relation is one the collection's engine can compile into a
37
+ * `WHERE`.
38
+ *
39
+ * Which kinds those are is the *driver's* answer, not this function's, so it
40
+ * comes from {@link DataSourceCapabilities.filterableRelationKinds}. The admin
41
+ * is one UI over Postgres, MongoDB, Firestore and whatever a developer
42
+ * registers, and "a many-to-many compiles to an `EXISTS` over the junction" is
43
+ * a fact about the Postgres driver — true today, and not the sort of thing to
44
+ * assert on an engine's behalf.
45
+ *
46
+ * Offering an uncompilable filter is not a cosmetic bug. The Postgres driver
47
+ * used to drop a filter key it could not resolve, which *widened* the result
48
+ * set — filtering a many-to-many column returned every row instead of none.
49
+ * That now fails closed with a 400, which is correct for a real schema drift
50
+ * and wrong as the answer to a control the admin itself put on screen.
51
+ *
52
+ * A relation whose kind cannot be determined at all — no inline `relation`
53
+ * block and no stamped `resolvedRelation` — stays filterable. The server
54
+ * resolves relations before the config reaches the admin, so this is the
55
+ * hand-built-property case, and silently dropping a working filter there would
56
+ * be its own regression.
57
+ */
58
+ export function isFilterableRelation(property: Property, engine?: string): boolean {
59
+ if (property.type !== "relation") return true;
60
+ const relationProperty = property as RelationProperty;
61
+ const kind = relationProperty.relation?.kind ?? relationProperty.resolvedRelation?.kind;
62
+ if (!kind) return true;
63
+ const kinds = getDataSourceCapabilities(engine).filterableRelationKinds
64
+ ?? DEFAULT_FILTERABLE_RELATION_KINDS;
65
+ return kinds.includes(kind);
66
+ }
67
+
33
68
  export interface ResolveFilterOperatorsParams {
34
69
  /**
35
70
  * The property to filter on. For array properties, pass the **item**
@@ -56,8 +91,9 @@ export interface ResolveFilterOperatorsParams {
56
91
  * 2. what makes sense for the property type (e.g. no `>` on booleans);
57
92
  * 3. the developer's optional narrowing — `property.admin.filterOperators`.
58
93
  *
59
- * Returns an empty array when the property is not filterable (either by
60
- * type, or because the developer disabled it with `filterOperators: []`).
94
+ * Returns an empty array when the property is not filterable by type, by
95
+ * developer narrowing (`filterOperators: []`), or because it is a relation the
96
+ * query layer has no column for (see {@link isFilterableRelation}).
61
97
  *
62
98
  * @group Models
63
99
  */
@@ -66,6 +102,8 @@ export function resolveFilterOperators({
66
102
  isArray,
67
103
  engine
68
104
  }: ResolveFilterOperatorsParams): WhereFilterOp[] {
105
+ if (!isFilterableRelation(property, engine)) return [];
106
+
69
107
  const typeDefaults: readonly WhereFilterOp[] = isArray
70
108
  ? ARRAY_OPS
71
109
  : DEFAULT_OPS_BY_TYPE[property.type] ?? [];
@@ -14,7 +14,8 @@
14
14
  * declarative conditions feature is authored and stored, never applied. It lives
15
15
  * here now because here is where it would be called from once it is wired up.
16
16
  */
17
- import type { ArrayProperty, ConditionContext, EnumValueConfig, Property, PropertyConditions, ReferenceProperty } from "@rebasepro/types";
17
+ import type { ConditionContext, EnumValueConfig, Property, PropertyConditions, ReferenceProperty } from "@rebasepro/types";
18
+ import type { AdminArrayOptions, AdminReferenceOptions } from "@rebasepro/admin-types";
18
19
  import { evaluateCondition } from "@rebasepro/common";
19
20
 
20
21
  export function isReadOnly(property: Property): boolean {
@@ -126,7 +127,9 @@ export function applyPropertyConditions(
126
127
  (result as ReferenceProperty).path = evaluateCondition(conditions.referencePath, context) as string;
127
128
  }
128
129
  if (conditions.referenceFilter) {
129
- (result as ReferenceProperty).fixedFilter = evaluateCondition(conditions.referenceFilter, context) as ReferenceProperty["fixedFilter"];
130
+ result.admin = result.admin || {};
131
+ (result.admin as AdminReferenceOptions).fixedFilter =
132
+ evaluateCondition(conditions.referenceFilter, context) as AdminReferenceOptions["fixedFilter"];
130
133
  }
131
134
  }
132
135
 
@@ -136,10 +139,12 @@ export function applyPropertyConditions(
136
139
 
137
140
  if (result.type === "array") {
138
141
  if (conditions.canAddElements !== undefined) {
139
- (result as ArrayProperty).canAddElements = evaluateCondition(conditions.canAddElements, context) as boolean;
142
+ result.admin = result.admin || {};
143
+ (result.admin as AdminArrayOptions).canAddElements = evaluateCondition(conditions.canAddElements, context) as boolean;
140
144
  }
141
145
  if (conditions.sortable !== undefined) {
142
- (result as ArrayProperty).sortable = evaluateCondition(conditions.sortable, context) as boolean;
146
+ result.admin = result.admin || {};
147
+ (result.admin as AdminArrayOptions).sortable = evaluateCondition(conditions.sortable, context) as boolean;
143
148
  }
144
149
  }
145
150
 
@@ -1,4 +1,4 @@
1
- import type { Property, Relation } from "@rebasepro/types";
1
+ import { isRelationalCollectionConfig, type Property, type Relation } from "@rebasepro/types";
2
2
  import { generateForeignKeyName } from "@rebasepro/utils";
3
3
  import type { AdminCollection } from "@rebasepro/admin-types";
4
4
  import { getPrimaryKeys, isPropertyBuilder } from "@rebasepro/common";
@@ -104,7 +104,7 @@ function getForeignKeyColumns<M extends Record<string, unknown>>(collection: Adm
104
104
  }
105
105
  };
106
106
 
107
- for (const relation of collection.relations ?? []) {
107
+ for (const relation of (isRelationalCollectionConfig(collection) ? collection.relations : undefined) ?? []) {
108
108
  addRelationKeys(relation);
109
109
  }
110
110
 
@@ -21,6 +21,7 @@ declare global {
21
21
  import {
22
22
  ArrowLeftIcon,
23
23
  Button,
24
+ Checkbox,
24
25
  cls,
25
26
  IconButton,
26
27
  iconSize,
@@ -141,6 +142,15 @@ export interface LoginViewProps {
141
142
  * Pre-fill the password field (e.g. for demo or testing environments).
142
143
  */
143
144
  defaultPassword?: string;
145
+
146
+ /**
147
+ * When set, the email/password forms render a "Join the newsletter"
148
+ * opt-in checkbox. Called with the address that just authenticated —
149
+ * sign-in or registration — if the box was ticked. Wire it to whatever
150
+ * stores the subscription; failures are the callback's problem, the
151
+ * login flow never waits on it.
152
+ */
153
+ onNewsletterOptIn?: (email: string) => void;
144
154
  }
145
155
 
146
156
  type AuthMode = "buttons" | "login" | "register" | "forgot";
@@ -203,7 +213,8 @@ export function LoginView({
203
213
  additionalComponent,
204
214
  topComponent,
205
215
  defaultEmail,
206
- defaultPassword
216
+ defaultPassword,
217
+ onNewsletterOptIn
207
218
  }: LoginViewProps) {
208
219
 
209
220
  const modeState = useModeController();
@@ -213,6 +224,9 @@ export function LoginView({
213
224
  const [mode, setMode] = useState<AuthMode>("buttons");
214
225
  const [fadeIn, setFadeIn] = useState(false);
215
226
  const [viewVisible, setViewVisible] = useState(true);
227
+ // Lives here, not in LoginForm: the form unmounts on every login↔register
228
+ // switch, and losing the tick on a mode change would silently drop opt-ins.
229
+ const [newsletterOptIn, setNewsletterOptIn] = useState(false);
216
230
 
217
231
  const switchMode = (newMode: AuthMode) => {
218
232
  setViewVisible(false);
@@ -360,6 +374,9 @@ export function LoginView({
360
374
  bootstrapMode={true}
361
375
  defaultEmail={defaultEmail}
362
376
  defaultPassword={defaultPassword}
377
+ onNewsletterOptIn={onNewsletterOptIn}
378
+ newsletterOptIn={newsletterOptIn}
379
+ setNewsletterOptIn={setNewsletterOptIn}
363
380
  />
364
381
  )}
365
382
 
@@ -442,6 +459,9 @@ export function LoginView({
442
459
  switchToRegister={showRegistration ? () => switchMode("register") : undefined}
443
460
  defaultEmail={defaultEmail}
444
461
  defaultPassword={defaultPassword}
462
+ onNewsletterOptIn={onNewsletterOptIn}
463
+ newsletterOptIn={newsletterOptIn}
464
+ setNewsletterOptIn={setNewsletterOptIn}
445
465
  />
446
466
  )}
447
467
 
@@ -457,6 +477,9 @@ export function LoginView({
457
477
  switchToLogin={() => switchMode("login")}
458
478
  defaultEmail={defaultEmail}
459
479
  defaultPassword={defaultPassword}
480
+ onNewsletterOptIn={onNewsletterOptIn}
481
+ newsletterOptIn={newsletterOptIn}
482
+ setNewsletterOptIn={setNewsletterOptIn}
460
483
  />
461
484
  )}
462
485
 
@@ -646,7 +669,10 @@ function LoginForm({
646
669
  switchToRegister,
647
670
  switchToLogin,
648
671
  defaultEmail,
649
- defaultPassword
672
+ defaultPassword,
673
+ onNewsletterOptIn,
674
+ newsletterOptIn = false,
675
+ setNewsletterOptIn
650
676
  }: {
651
677
  onClose: () => void,
652
678
  onForgotPassword?: () => void,
@@ -658,9 +684,13 @@ function LoginForm({
658
684
  switchToRegister?: () => void,
659
685
  switchToLogin?: () => void,
660
686
  defaultEmail?: string,
661
- defaultPassword?: string
687
+ defaultPassword?: string,
688
+ onNewsletterOptIn?: (email: string) => void,
689
+ newsletterOptIn?: boolean,
690
+ setNewsletterOptIn?: (checked: boolean) => void
662
691
  }) {
663
692
  const passwordRef = useRef<HTMLInputElement | null>(null);
693
+ const { t } = useTranslation();
664
694
 
665
695
  const [email, setEmail] = useState<string | undefined>(defaultEmail);
666
696
  const [password, setPassword] = useState<string | undefined>(defaultPassword);
@@ -684,15 +714,28 @@ function LoginForm({
684
714
  // is not one of those: without a catch here every rejected sign-in becomes an
685
715
  // unhandled rejection, reported as a crash by anything listening for one,
686
716
  // while the user has already been shown the error.
717
+ // Fires only on the resolution path, i.e. after the controller accepted
718
+ // the credentials — a ticked box on a failed attempt must not subscribe
719
+ // an address its owner never proved they control.
720
+ function subscribeIfOptedIn(email: string) {
721
+ if (newsletterOptIn && onNewsletterOptIn) {
722
+ onNewsletterOptIn(email);
723
+ }
724
+ }
725
+
687
726
  function handleEnterPassword() {
688
727
  if (email && password && authController.emailPasswordLogin) {
689
- void Promise.resolve(authController.emailPasswordLogin(email, password)).catch(() => undefined);
728
+ void Promise.resolve(authController.emailPasswordLogin(email, password))
729
+ .then(() => subscribeIfOptedIn(email))
730
+ .catch(() => undefined);
690
731
  }
691
732
  }
692
733
 
693
734
  function handleRegistration() {
694
735
  if (email && password && authController.register) {
695
- void Promise.resolve(authController.register(email, password, displayName)).catch(() => undefined);
736
+ void Promise.resolve(authController.register(email, password, displayName))
737
+ .then(() => subscribeIfOptedIn(email))
738
+ .catch(() => undefined);
696
739
  }
697
740
  }
698
741
 
@@ -813,6 +856,19 @@ function LoginForm({
813
856
  </div>
814
857
  )}
815
858
 
859
+ {onNewsletterOptIn && (
860
+ <label className="flex items-center gap-2 cursor-pointer mt-1 mb-1">
861
+ <Checkbox
862
+ checked={newsletterOptIn}
863
+ onCheckedChange={(checked) => setNewsletterOptIn?.(checked === true)}
864
+ size="small"
865
+ />
866
+ <Typography variant="caption" color="secondary" className="select-none">
867
+ {t("join_newsletter")}
868
+ </Typography>
869
+ </label>
870
+ )}
871
+
816
872
  <LoadingButton
817
873
  type="submit"
818
874
  variant="filled"
@@ -1,16 +1,16 @@
1
1
  import React, { useLayoutEffect, useRef } from "react";
2
2
  import { useRebaseRegistryDispatch } from "../hooks";
3
- import type { RebaseAuthConfig } from "@rebasepro/admin-types";
3
+ import type { RebaseAuthViewConfig } from "@rebasepro/admin-types";
4
4
 
5
5
  /**
6
6
  * Declarative component to configure authentication in Rebase.
7
7
  * Renders nothing — purely registers config into the RebaseRegistry.
8
8
  *
9
9
  * This is a framework-level component that lives in core since
10
- * authentication is cross-cutting (CMS, Studio, any app area).
10
+ * authentication is cross-cutting (admin, Studio, any app area).
11
11
  * @group Core
12
12
  */
13
- export function RebaseAuth({ loginView }: RebaseAuthConfig) {
13
+ export function RebaseAuth({ loginView }: RebaseAuthViewConfig) {
14
14
  const dispatch = useRebaseRegistryDispatch();
15
15
  const registeredRef = useRef(false);
16
16
 
@@ -6,7 +6,7 @@ import type { AdminCollection } from "@rebasepro/admin-types";
6
6
 
7
7
  /**
8
8
  * Get a property in a property tree from a dot-path like `address.street`.
9
- * Inlined here to avoid importing property-aware utilities from the CMS layer.
9
+ * Inlined here to avoid importing property-aware utilities from the admin layer.
10
10
  */
11
11
  function getPropertyInPath(properties: Properties, path: string): Property | undefined {
12
12
  if (typeof properties === "object") {
@@ -36,7 +36,7 @@ import { EffectiveRoleControllerContext } from "../contexts/EffectiveRoleControl
36
36
  import { useBuildEffectiveRoleController } from "../hooks/useBuildEffectiveRoleController";
37
37
 
38
38
  /**
39
- * If you are using independent components of the CMS
39
+ * If you are using independent components of the admin
40
40
  * you need to wrap them with this main component, so the internal hooks work.
41
41
  *
42
42
  * This is the main component of Rebase. It acts as the provider of all the
@@ -159,7 +159,7 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
159
159
  const resolvedData = useMemo(() => {
160
160
  const registeredDefault = dataSourcesValue.sources[DEFAULT_DATA_SOURCE_KEY];
161
161
  if (registeredDefault) return registeredDefault;
162
- // CMS boundary: the SDK client returns flat rows; wrap them into the
162
+ // admin boundary: the SDK client returns flat rows; wrap them into the
163
163
  // Entity view-model the admin (`useData()`) renders.
164
164
  if (client?.data) return wrapAsEntityData(client.data, entityDataOptions);
165
165
  const built = Object.values(dataSourcesValue.sources);
@@ -204,7 +204,6 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
204
204
  if (!cancelled) console.debug("[Rebase] Could not load storage sources", e);
205
205
  });
206
206
  return () => { cancelled = true; };
207
- // eslint-disable-next-line react-hooks/exhaustive-deps
208
207
  }, [client, authUser, loginSkipped]);
209
208
 
210
209
  // Normalize the prop + discovered definitions into a registry + sources map.
@@ -47,10 +47,8 @@ type DeepPartial<T> = T extends object
47
47
  * Controller to simulate different roles when dev mode is active.
48
48
  * @group Models
49
49
  */
50
- export interface EffectiveRoleController {
51
- effectiveRole: string | null;
52
- setEffectiveRole: (role: string | null) => void;
53
- }
50
+ export type { EffectiveRoleController } from "@rebasepro/types";
51
+ import type { EffectiveRoleController } from "@rebasepro/types";
54
52
 
55
53
 
56
54
  /**
@@ -122,7 +120,7 @@ export type RebaseProps<USER extends User> = {
122
120
  basePath?: string;
123
121
 
124
122
  /**
125
- * Optional base path for the CMS collections.
123
+ * Optional base path for the admin collections.
126
124
  * Defaults to "/c"
127
125
  */
128
126
  baseCollectionPath?: string;
@@ -136,13 +134,13 @@ export type RebaseProps<USER extends User> = {
136
134
 
137
135
 
138
136
  /**
139
- * Format of the dates in the CMS.
137
+ * Format of the dates in the admin.
140
138
  * Defaults to 'MMMM dd, yyyy, HH:mm:ss'
141
139
  */
142
140
  dateTimeFormat?: string;
143
141
 
144
142
  /**
145
- * Locale of the CMS, currently only affecting dates
143
+ * Locale of the admin, currently only affecting dates
146
144
  */
147
145
  locale?: Locale;
148
146
 
@@ -246,7 +244,7 @@ export type RebaseProps<USER extends User> = {
246
244
  userConfigPersistence?: UserConfigurationPersistence;
247
245
 
248
246
  /**
249
- * Callback used to get analytics events from the CMS
247
+ * Callback used to get analytics events from the admin
250
248
  */
251
249
  onAnalyticsEvent?: (event: AnalyticsEvent, data?: object) => void;
252
250
 
@@ -258,12 +256,12 @@ export type RebaseProps<USER extends User> = {
258
256
 
259
257
 
260
258
  /**
261
- * Plugins loaded in the CMS
259
+ * Plugins loaded in the admin
262
260
  */
263
261
  plugins?: RebasePlugin[];
264
262
 
265
263
  /**
266
- * Extra slots for the CMS
264
+ * Extra slots for the admin
267
265
  */
268
266
  slots?: SlotContribution[];
269
267
 
@@ -4,7 +4,7 @@ import type { ModeController } from "./useModeController";
4
4
 
5
5
  /**
6
6
  * Use this hook to build a color mode controller that determines
7
- * the theme of the CMS
7
+ * the theme of the admin
8
8
  */
9
9
  export function useBuildModeController(): ModeController {
10
10
 
@@ -5,7 +5,7 @@ import { CustomizationControllerContext } from "../contexts/CustomizationControl
5
5
  /**
6
6
  * Use this hook to retrieve the customization controller.
7
7
  * This hook includes all the customization options that can be used
8
- * to customize the CMS.
8
+ * to customize the admin.
9
9
  *
10
10
  * You will likely not need to use this hook directly.
11
11
  *
@@ -33,7 +33,7 @@ export const useRebaseContext = <USER extends User = User, AuthControllerType ex
33
33
  const authController = useAuthController<USER, AuthControllerType>();
34
34
  // `context.data` is the flat SDK view — identical in shape to the backend
35
35
  // `context.data` and the frontend SDK client, so callbacks behave the same
36
- // everywhere. The admin CMS reads Entities via `useData()` directly.
36
+ // everywhere. The admin reads Entities via `useData()` directly.
37
37
  const entityData = useData();
38
38
  const data = React.useMemo(() => wrapAsSdkData(entityData), [entityData]);
39
39
  const storageSource = useStorageSource();
@@ -1,5 +1,5 @@
1
1
  import React, { createContext, useContext, useState, useCallback, useMemo, useRef } from "react";
2
- import type { RebaseRegistryController, RebaseAdminConfig, RebaseStudioConfig, RebaseAuthConfig } from "@rebasepro/admin-types";
2
+ import type { RebaseRegistryController, RebaseAdminConfig, RebaseStudioConfig, RebaseAuthViewConfig } from "@rebasepro/admin-types";
3
3
 
4
4
  /**
5
5
  * Split into two contexts to prevent infinite re-render loops:
@@ -12,18 +12,18 @@ import type { RebaseRegistryController, RebaseAdminConfig, RebaseStudioConfig, R
12
12
  */
13
13
 
14
14
  interface RegistryDispatch {
15
- registerCMS: (config: RebaseAdminConfig) => void;
16
- unregisterCMS: () => void;
15
+ registerAdmin: (config: RebaseAdminConfig) => void;
16
+ unregisterAdmin: () => void;
17
17
  registerStudio: (config: RebaseStudioConfig) => void;
18
18
  unregisterStudio: () => void;
19
- registerAuth: (config: RebaseAuthConfig) => void;
19
+ registerAuth: (config: RebaseAuthViewConfig) => void;
20
20
  unregisterAuth: () => void;
21
21
  }
22
22
 
23
23
  interface RegistryState {
24
24
  cmsConfig: RebaseAdminConfig | null;
25
25
  studioConfig: RebaseStudioConfig | null;
26
- authConfig: RebaseAuthConfig | null;
26
+ authConfig: RebaseAuthViewConfig | null;
27
27
  }
28
28
 
29
29
  const RegistryDispatchContext = createContext<RegistryDispatch | undefined>(undefined);
@@ -36,15 +36,15 @@ const RegistryStateContext = createContext<RegistryState>({
36
36
  export function RebaseRegistryProvider({ children }: { children: React.ReactNode }) {
37
37
  const [cmsConfig, setCmsConfig] = useState<RebaseAdminConfig | null>(null);
38
38
  const [studioConfig, setStudioConfig] = useState<RebaseStudioConfig | null>(null);
39
- const [authConfig, setAuthConfig] = useState<RebaseAuthConfig | null>(null);
39
+ const [authConfig, setAuthConfig] = useState<RebaseAuthViewConfig | null>(null);
40
40
 
41
41
  // Dispatch functions are stable — never change identity
42
42
  const dispatch = useMemo<RegistryDispatch>(() => ({
43
- registerCMS: (config: RebaseAdminConfig) => setCmsConfig(config),
44
- unregisterCMS: () => setCmsConfig(null),
43
+ registerAdmin: (config: RebaseAdminConfig) => setCmsConfig(config),
44
+ unregisterAdmin: () => setCmsConfig(null),
45
45
  registerStudio: (config: RebaseStudioConfig) => setStudioConfig(config),
46
46
  unregisterStudio: () => setStudioConfig(null),
47
- registerAuth: (config: RebaseAuthConfig) => setAuthConfig(config),
47
+ registerAuth: (config: RebaseAuthViewConfig) => setAuthConfig(config),
48
48
  unregisterAuth: () => setAuthConfig(null)
49
49
  }), []);
50
50
 
@@ -18,9 +18,9 @@ export type { BreadcrumbEntry, BreadcrumbsController };
18
18
  // ─── Bridge interface ───────────────────────────────────────────────
19
19
 
20
20
  /**
21
- * StudioBridge provides optional CMS capabilities to Studio components.
22
- * When CMS is present, a bridge provider injects real implementations.
23
- * When CMS is absent, noop defaults ensure Studio works standalone.
21
+ * StudioBridge provides optional admin capabilities to Studio components.
22
+ * When the admin is present, a bridge provider injects real implementations.
23
+ * When the admin is absent, noop defaults ensure Studio works standalone.
24
24
  */
25
25
  export interface StudioBridge {
26
26
  collectionRegistry: CollectionRegistryController;
@@ -84,10 +84,10 @@ const NOOP_BRIDGE: StudioBridge = {
84
84
  export const StudioBridgeContext = createContext<StudioBridge>(NOOP_BRIDGE);
85
85
 
86
86
  /**
87
- * Provider that injects CMS capabilities into Studio.
87
+ * Provider that injects admin capabilities into Studio.
88
88
  * Accepts partial overrides — any field not provided falls back to noop.
89
89
  *
90
- * Usage (in app wiring, when CMS is present):
90
+ * Usage (in app wiring, when the admin is present):
91
91
  * ```tsx
92
92
  * <StudioBridgeProvider value={{
93
93
  * collectionRegistry: useCollectionRegistryController(),
@@ -121,27 +121,27 @@ export function StudioBridgeProvider({
121
121
 
122
122
  // ─── Convenience hooks ──────────────────────────────────────────────
123
123
 
124
- /** Collection registry — returns noop if CMS is not present. */
124
+ /** Collection registry — returns noop if the admin is not present. */
125
125
  export function useStudioCollectionRegistry(): CollectionRegistryController {
126
126
  return useContext(StudioBridgeContext).collectionRegistry;
127
127
  }
128
128
 
129
- /** Side panel controller — returns noop if CMS is not present. */
129
+ /** Side panel controller — returns noop if the admin is not present. */
130
130
  export function useStudioSidePanelController(): SidePanelController {
131
131
  return useContext(StudioBridgeContext).sidePanelController;
132
132
  }
133
133
 
134
- /** URL controller — returns noop if CMS is not present. */
134
+ /** URL controller — returns noop if the admin is not present. */
135
135
  export function useStudioUrlController(): UrlController {
136
136
  return useContext(StudioBridgeContext).urlController;
137
137
  }
138
138
 
139
- /** Navigation state — returns noop if CMS is not present. */
139
+ /** Navigation state — returns noop if the admin is not present. */
140
140
  export function useStudioNavigationState(): NavigationStateController {
141
141
  return useContext(StudioBridgeContext).navigationState;
142
142
  }
143
143
 
144
- /** Breadcrumbs controller — returns noop if CMS is not present. */
144
+ /** Breadcrumbs controller — returns noop if the admin is not present. */
145
145
  export function useStudioBreadcrumbs(): BreadcrumbsController {
146
146
  return useContext(StudioBridgeContext).breadcrumbs;
147
147
  }
package/src/index.ts CHANGED
@@ -24,7 +24,7 @@ export * from "./i18n/RebaseI18nProvider";
24
24
  export * from "./locales/en";
25
25
  export * from "./locales/es";
26
26
 
27
- // Studio Bridge — shared context for optional CMS↔Studio integration
27
+ // Studio Bridge — shared context for optional admin↔Studio integration
28
28
  export * from "./hooks/useStudioBridge";
29
29
 
30
30
  // Self-assembling bridge registration hook
package/src/locales/de.ts CHANGED
@@ -58,6 +58,7 @@ export const de: RebaseTranslations = {
58
58
  dark_mode: "Dunkel",
59
59
  light_mode: "Hell",
60
60
  system_mode: "System",
61
+ join_newsletter: "Newsletter abonnieren, kein Spam",
61
62
  ok: "Ok",
62
63
  save_collection_config: "Sammlungsstruktur speichern",
63
64
  search_for_more_icons: "Nach weiteren Symbolen suchen…",
@@ -277,7 +278,7 @@ export const de: RebaseTranslations = {
277
278
  action_defined_in_code: "Diese Aktion ist im Code definiert mit dem Schlüssel",
278
279
  add_custom_entity_action: "Benutzerdefinierte Entitätsaktion hinzufügen",
279
280
  remove_this_action: "Diese Aktion entfernen?",
280
- remove_action_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Aktion im CMS",
281
+ remove_action_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Aktion im Admin-Panel",
281
282
 
282
283
  subcollections_of: "Untersammlungen von",
283
284
  add_subcollection: "Untersammlung hinzufügen",
@@ -286,14 +287,14 @@ export const de: RebaseTranslations = {
286
287
  view_defined_in_code: "Diese Ansicht ist im Code definiert mit dem Schlüssel",
287
288
  add_custom_entity_view: "Benutzerdefinierte Entitätsansicht hinzufügen",
288
289
  delete_this_subcollection: "Diese Untersammlung löschen?",
289
- remove_collection_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Sammlung im CMS",
290
+ remove_collection_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Sammlung im Admin-Panel",
290
291
  remove_this_view: "Diese Ansicht entfernen?",
291
- remove_view_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Ansicht im CMS",
292
+ remove_view_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Ansicht im Admin-Panel",
292
293
 
293
294
  no_collection_selected: "Keine Sammlung ausgewählt",
294
295
  code_for_collection: "Code für",
295
296
  use_config_define_json: "Verwenden Sie diese Konfiguration, um die Sammlung im JSON-Format zu definieren.",
296
- customise_collection_code: "Wenn Sie die Sammlung im Code anpassen möchten, können Sie diesen Sammlungscode zu Ihrer CMS-App-Konfiguration hinzufügen.",
297
+ customise_collection_code: "Wenn Sie die Sammlung im Code anpassen möchten, können Sie diesen Sammlungscode zu Ihrer Admin-App-Konfiguration hinzufügen.",
297
298
  copied: "Kopiert",
298
299
 
299
300
  property_cant_be_edited: "Diese Eigenschaft kann nicht bearbeitet werden",
@@ -464,7 +465,7 @@ export const de: RebaseTranslations = {
464
465
  continue_from_scratch: "Von vorne beginnen",
465
466
 
466
467
  /** Admin views config */
467
- cms_users: "CMS-Benutzer",
468
+ admin_users: "Admin-Benutzer",
468
469
  roles_menu: "Rollen",
469
470
  project_settings: "Projekteinstellungen",
470
471
 
@@ -726,8 +727,8 @@ no_filter: "No filter",
726
727
  studio_sql_error_executing: "An error occurred while executing the query.",
727
728
  studio_sql_error_explaining: "An error occurred while explaining the query.",
728
729
  studio_sql_save_first_to_favorite: "Please save the snippet first before favoriting.",
729
- studio_sql_cms: "Collections:",
730
- studio_sql_cms_collections_tooltip: "Tables in this query that are mapped as CMS collections",
730
+ studio_sql_collections_label: "Collections:",
731
+ studio_sql_admin_collections_tooltip: "Tables in this query that are mapped as admin collections",
731
732
  studio_sql_edit_entity: "Edit {{name}} #{{id}}",
732
733
  studio_sql_sql_not_supported: "SQL execution not supported by this data source",
733
734
  studio_sql_fetch_error: "Failed to fetch connection options: {{message}}",
@@ -832,7 +833,7 @@ no_filter: "No filter",
832
833
  studio_add_kanban_column_add: "Add",
833
834
  studio_add_kanban_column_cancel: "Cancel",
834
835
  studio_collection_view_sql: "SQL Editor",
835
- studio_collection_view_cms: "CMS View",
836
+ studio_collection_view_admin: "Admin View",
836
837
  studio_editor_collection_tooltip: "Edit collection schema",
837
838
  studio_editor_collection_no_permission: "You don't have permission to edit this collection",
838
839
  studio_editor_collection_start_tooltip: "Copy path or edit schema",
@@ -865,7 +866,7 @@ no_filter: "No filter",
865
866
  studio_editor_collection_start_saved: "Default config saved",
866
867
  studio_home_duplicate_collection: "Duplicate",
867
868
  studio_home_delete: "Delete",
868
- studio_home_confirm_delete_no_data: "This will not delete any data, only the collection in the CMS",
869
+ studio_home_confirm_delete_no_data: "This will not delete any data, only the collection in the admin panel",
869
870
  studio_home_collection_deleted: "Collection deleted",
870
871
  studio_kanban_configure: "Configure Kanban",
871
872
  studio_missing_reference_error: "No collection for path: {{path}}",