@questpie/admin 3.0.5 → 3.0.7

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 (59) hide show
  1. package/README.md +99 -1
  2. package/dist/client/builder/types/field-types.d.mts +11 -0
  3. package/dist/client/components/brand-logo.d.mts +25 -0
  4. package/dist/client/components/brand-logo.mjs +174 -0
  5. package/dist/client/create-admin-client.d.mts +7 -0
  6. package/dist/client/create-admin-client.mjs +25 -0
  7. package/dist/client/hooks/use-brand.d.mts +22 -0
  8. package/dist/client/hooks/use-brand.mjs +52 -0
  9. package/dist/client/runtime/index.mjs +1 -1
  10. package/dist/client/runtime/provider.d.mts +4 -0
  11. package/dist/client/runtime/provider.mjs +38 -8
  12. package/dist/client/styles/base.css +4 -0
  13. package/dist/client/types/admin-config.d.mts +24 -0
  14. package/dist/client/views/auth/accept-invite-form.d.mts +2 -2
  15. package/dist/client/views/auth/auth-layout.d.mts +8 -3
  16. package/dist/client/views/auth/auth-layout.mjs +116 -102
  17. package/dist/client/views/auth/forgot-password-form.d.mts +2 -2
  18. package/dist/client/views/auth/login-form.d.mts +2 -2
  19. package/dist/client/views/auth/reset-password-form.d.mts +2 -2
  20. package/dist/client/views/auth/setup-form.d.mts +2 -2
  21. package/dist/client/views/collection/auto-form-fields.mjs +2 -0
  22. package/dist/client/views/collection/field-renderer.mjs +3 -2
  23. package/dist/client/views/globals/global-form-view.mjs +908 -863
  24. package/dist/client/views/layout/admin-sidebar.mjs +153 -141
  25. package/dist/client/views/pages/accept-invite-page.mjs +122 -144
  26. package/dist/client/views/pages/forgot-password-page.mjs +22 -30
  27. package/dist/client/views/pages/invite-page.mjs +24 -33
  28. package/dist/client/views/pages/login-page.d.mts +2 -2
  29. package/dist/client/views/pages/login-page.mjs +24 -32
  30. package/dist/client/views/pages/reset-password-page.d.mts +2 -2
  31. package/dist/client/views/pages/reset-password-page.mjs +77 -92
  32. package/dist/client/views/pages/setup-page.mjs +31 -39
  33. package/dist/client.d.mts +6 -2
  34. package/dist/client.mjs +5 -2
  35. package/dist/index.d.mts +6 -2
  36. package/dist/index.mjs +5 -2
  37. package/dist/server/augmentation/dashboard.d.mts +23 -5
  38. package/dist/server/augmentation/form-layout.d.mts +10 -0
  39. package/dist/server/augmentation/index.d.mts +1 -1
  40. package/dist/server/augmentation.d.mts +1 -1
  41. package/dist/server/modules/admin/collections/session.d.mts +42 -42
  42. package/dist/server/modules/admin/collections/user.d.mts +32 -32
  43. package/dist/server/modules/admin/collections/verification.d.mts +36 -36
  44. package/dist/server/modules/admin/dto/admin-config.dto.mjs +19 -1
  45. package/dist/server/modules/admin/index.d.mts +1 -1
  46. package/dist/server/modules/admin/routes/admin-config.d.mts +2 -2
  47. package/dist/server/modules/admin/routes/execute-action.d.mts +9 -9
  48. package/dist/server/modules/admin/routes/preview.d.mts +11 -11
  49. package/dist/server/modules/admin/routes/preview.mjs +1 -1
  50. package/dist/server/modules/admin/routes/reactive.d.mts +9 -9
  51. package/dist/server/modules/admin/routes/setup.d.mts +7 -7
  52. package/dist/server/modules/admin/routes/widget-data.d.mts +5 -5
  53. package/dist/server/modules/admin-preferences/collections/saved-views.d.mts +23 -23
  54. package/dist/server.d.mts +4 -4
  55. package/dist/shared/preview-utils.d.mts +34 -1
  56. package/dist/shared/preview-utils.mjs +79 -1
  57. package/dist/shared.d.mts +2 -2
  58. package/dist/shared.mjs +2 -2
  59. package/package.json +3 -3
package/README.md CHANGED
@@ -46,7 +46,13 @@ Branding name, sidebar, dashboard, and admin locale are configured via `config/a
46
46
  import { adminConfig } from "#questpie/factories";
47
47
 
48
48
  export default adminConfig({
49
- branding: { name: "My Admin" },
49
+ branding: {
50
+ name: "My Admin",
51
+ // Optional — see "Whitelabeling" below
52
+ logo: "/brand/logo.svg",
53
+ tagline: "Powered by My Co.",
54
+ favicon: "/brand/favicon.ico",
55
+ },
50
56
  sidebar: {
51
57
  sections: [
52
58
  {
@@ -266,6 +272,98 @@ Import the admin base stylesheet and scan the admin package:
266
272
 
267
273
  `index.css` is an alias for `base.css`; import `base.css` directly when you want explicit control.
268
274
 
275
+ ## Whitelabeling
276
+
277
+ There are two layers, deliberately separated:
278
+
279
+ | Layer | Configured in | Covers |
280
+ | ------------ | ----------------------------------------------- | -------------------------------------------- |
281
+ | **Content** | `config/admin.ts` → `branding` | Name, logo, tagline, favicon |
282
+ | **Theme** | Your app's `admin.css` | Colors, fonts, radii, shadows, motion |
283
+ | **Chrome** | Files in `questpie/admin/components/` (see below) | Sidebar brand, nav item, auth layout |
284
+
285
+ ### Branding (config-driven)
286
+
287
+ ```ts
288
+ // config/admin.ts
289
+ export default adminConfig({
290
+ branding: {
291
+ name: "Acme Studio",
292
+ // String, or { src, srcDark } for separate light/dark images,
293
+ // or a server ComponentReference for an inline SVG.
294
+ logo: { src: "/brand/logo-light.svg", srcDark: "/brand/logo-dark.svg" },
295
+ // Replaces the "Built with QUESTPIE" footer on auth pages.
296
+ // Omit to render no footer text at all.
297
+ tagline: "Studio admin",
298
+ favicon: "/brand/favicon.ico",
299
+ },
300
+ });
301
+ ```
302
+
303
+ The logo also accepts an `I18nText` for `name`/`tagline` (`{ en: "...", sk: "..." }`)
304
+ and the same locale-map / translation-key shape used elsewhere in the admin.
305
+
306
+ ### Theming (CSS override)
307
+
308
+ The admin exposes every visual token as a CSS custom property in `base.css`. To
309
+ rebrand colors, fonts, or shape, override them in your app's `admin.css`
310
+ **after** the base import — source order ensures your overrides win:
311
+
312
+ ```css
313
+ /* admin.css */
314
+ @import "tailwindcss";
315
+ @import "@questpie/admin/client/styles/base.css";
316
+
317
+ /* Optional: load a brand font */
318
+ @import url("https://fonts.googleapis.com/css2?family=Caveat+Brush&display=swap");
319
+
320
+ :root,
321
+ .light {
322
+ --primary: oklch(0.65 0.2 25);
323
+ --ring: oklch(0.65 0.2 25);
324
+ --font-heading: "Caveat Brush", system-ui, sans-serif;
325
+ --surface-radius: 0.5rem;
326
+ }
327
+
328
+ .dark {
329
+ --primary: oklch(0.78 0.18 25); /* lifted L for dark surfaces */
330
+ --ring: oklch(0.78 0.18 25);
331
+ }
332
+ ```
333
+
334
+ `OKLCH` is recommended because the admin's derived hover/focus states use
335
+ `color-mix(in oklch, …)` — values stay perceptually consistent. HEX and `rgb()`
336
+ work too; `hsl()` is discouraged (no P3 gamut). See the full token list in
337
+ `@questpie/admin/client/styles/base.css`; common knobs:
338
+
339
+ - Colors: `--primary`, `--background`, `--foreground`, `--surface`, `--border`,
340
+ `--ring`, `--accent`, `--destructive`, `--success`, …
341
+ - Sidebar: `--sidebar`, `--sidebar-accent`, `--sidebar-active-background`, …
342
+ - Typography: `--font-sans`, `--font-mono`, `--font-chrome`, `--font-heading`
343
+ - Shape: `--control-radius`, `--surface-radius`, `--floating-radius`
344
+
345
+ ### Zero-FOUC favicon (optional, TanStack Start)
346
+
347
+ By default the favicon is applied client-side after the admin config loads.
348
+ For SSR-clean favicons, fetch your config in the route loader and add a link
349
+ yourself:
350
+
351
+ ```tsx
352
+ // routes/admin.tsx
353
+ export const Route = createFileRoute("/admin")({
354
+ loader: async ({ context }) => ({ config: await context.client.routes.getAdminConfig() }),
355
+ head: ({ loaderData }) => ({
356
+ links: [
357
+ { rel: "stylesheet", href: adminCss },
358
+ ...(loaderData?.config?.branding?.favicon
359
+ ? [{ rel: "icon", href: loaderData.config.branding.favicon }]
360
+ : []),
361
+ ],
362
+ }),
363
+ component: AdminLayout,
364
+ });
365
+ ```
366
+
269
367
  ## Chrome Overrides (File-First)
270
368
 
271
369
  Place component files in `questpie/admin/components/` to override specific UI chrome without changing your app shell:
@@ -506,6 +506,17 @@ interface FieldReactiveConfig<TData = any> {
506
506
  interface FieldLayoutItemWithReactive<TData = any> extends FieldReactiveConfig<TData> {
507
507
  field: string;
508
508
  className?: string;
509
+ /**
510
+ * Extra props forwarded to the field component. Use for component-specific
511
+ * configuration that doesn't have a dedicated layout key (e.g. relation
512
+ * field's `filter`, custom render functions).
513
+ *
514
+ * @example
515
+ * ```ts
516
+ * { field: f.counselorId, props: { filter: () => ({ role: "admin" }) } }
517
+ * ```
518
+ */
519
+ props?: Record<string, any>;
509
520
  }
510
521
  /**
511
522
  * Field layout item - can be a simple field name, field with config, section, or tabs
@@ -0,0 +1,25 @@
1
+ import { BrandLogoConfig } from "../types/admin-config.mjs";
2
+ import * as React from "react";
3
+
4
+ //#region src/client/components/brand-logo.d.ts
5
+ interface BrandLogoMarkProps {
6
+ logo: BrandLogoConfig | null | undefined;
7
+ className?: string;
8
+ fallback?: React.ReactNode;
9
+ alt?: string;
10
+ }
11
+ /**
12
+ * Default renderer for `branding.logo`. Accepts:
13
+ * - a URL string (same image both modes)
14
+ * - `{ src, srcDark }` (separate light/dark images, switched by .dark class)
15
+ * - a `ComponentReference` (server-registered component, e.g. an SVG)
16
+ *
17
+ * When `logo` is null/undefined the `fallback` prop is rendered (typically
18
+ * the framework's built-in mark, e.g. `<QuestpieSymbol />`).
19
+ *
20
+ * Light/dark switching uses Tailwind's `dark:` variant against the
21
+ * `.dark` class the admin theme manager toggles on `<html>`.
22
+ */
23
+ declare const BrandLogoMark: React.NamedExoticComponent<BrandLogoMarkProps>;
24
+ //#endregion
25
+ export { BrandLogoMark, BrandLogoMarkProps };
@@ -0,0 +1,174 @@
1
+ import { cn } from "../lib/utils.mjs";
2
+ import { ComponentRenderer } from "./component-renderer.mjs";
3
+ import { c } from "react/compiler-runtime";
4
+ import * as React from "react";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+
7
+ //#region src/client/components/brand-logo.tsx
8
+ /**
9
+ * Default renderer for `branding.logo`. Accepts:
10
+ * - a URL string (same image both modes)
11
+ * - `{ src, srcDark }` (separate light/dark images, switched by .dark class)
12
+ * - a `ComponentReference` (server-registered component, e.g. an SVG)
13
+ *
14
+ * When `logo` is null/undefined the `fallback` prop is rendered (typically
15
+ * the framework's built-in mark, e.g. `<QuestpieSymbol />`).
16
+ *
17
+ * Light/dark switching uses Tailwind's `dark:` variant against the
18
+ * `.dark` class the admin theme manager toggles on `<html>`.
19
+ */
20
+ const BrandLogoMark = React.memo(function BrandLogoMark$1(t0) {
21
+ const $ = c(46);
22
+ const { logo, className, fallback: t1, alt: t2 } = t0;
23
+ const fallback = t1 === void 0 ? null : t1;
24
+ const alt = t2 === void 0 ? "" : t2;
25
+ if (logo == null) {
26
+ let t3$1;
27
+ if ($[0] !== fallback) {
28
+ t3$1 = /* @__PURE__ */ jsx(Fragment, { children: fallback });
29
+ $[0] = fallback;
30
+ $[1] = t3$1;
31
+ } else t3$1 = $[1];
32
+ return t3$1;
33
+ }
34
+ if (typeof logo === "string") {
35
+ let t3$1;
36
+ if ($[2] !== className) {
37
+ t3$1 = cn("size-6 shrink-0 object-contain", className);
38
+ $[2] = className;
39
+ $[3] = t3$1;
40
+ } else t3$1 = $[3];
41
+ let t4$1;
42
+ if ($[4] !== alt || $[5] !== logo || $[6] !== t3$1) {
43
+ t4$1 = /* @__PURE__ */ jsx("img", {
44
+ src: logo,
45
+ alt,
46
+ className: t3$1
47
+ });
48
+ $[4] = alt;
49
+ $[5] = logo;
50
+ $[6] = t3$1;
51
+ $[7] = t4$1;
52
+ } else t4$1 = $[7];
53
+ return t4$1;
54
+ }
55
+ if ("src" in logo) {
56
+ const { src, srcDark, alt: t3$1, width, height } = logo;
57
+ const imgAlt = t3$1 === void 0 ? alt : t3$1;
58
+ if (!srcDark) {
59
+ let t4$2;
60
+ if ($[8] !== className) {
61
+ t4$2 = cn("size-6 shrink-0 object-contain", className);
62
+ $[8] = className;
63
+ $[9] = t4$2;
64
+ } else t4$2 = $[9];
65
+ let t5$2;
66
+ if ($[10] !== height || $[11] !== imgAlt || $[12] !== src || $[13] !== t4$2 || $[14] !== width) {
67
+ t5$2 = /* @__PURE__ */ jsx("img", {
68
+ src,
69
+ alt: imgAlt,
70
+ width,
71
+ height,
72
+ className: t4$2
73
+ });
74
+ $[10] = height;
75
+ $[11] = imgAlt;
76
+ $[12] = src;
77
+ $[13] = t4$2;
78
+ $[14] = width;
79
+ $[15] = t5$2;
80
+ } else t5$2 = $[15];
81
+ return t5$2;
82
+ }
83
+ let t4$1;
84
+ if ($[16] !== className) {
85
+ t4$1 = cn("size-6 shrink-0 object-contain dark:hidden", className);
86
+ $[16] = className;
87
+ $[17] = t4$1;
88
+ } else t4$1 = $[17];
89
+ let t5$1;
90
+ if ($[18] !== height || $[19] !== imgAlt || $[20] !== src || $[21] !== t4$1 || $[22] !== width) {
91
+ t5$1 = /* @__PURE__ */ jsx("img", {
92
+ src,
93
+ alt: imgAlt,
94
+ width,
95
+ height,
96
+ className: t4$1
97
+ });
98
+ $[18] = height;
99
+ $[19] = imgAlt;
100
+ $[20] = src;
101
+ $[21] = t4$1;
102
+ $[22] = width;
103
+ $[23] = t5$1;
104
+ } else t5$1 = $[23];
105
+ let t6$1;
106
+ if ($[24] !== className) {
107
+ t6$1 = cn("hidden size-6 shrink-0 object-contain dark:block", className);
108
+ $[24] = className;
109
+ $[25] = t6$1;
110
+ } else t6$1 = $[25];
111
+ let t7;
112
+ if ($[26] !== height || $[27] !== imgAlt || $[28] !== srcDark || $[29] !== t6$1 || $[30] !== width) {
113
+ t7 = /* @__PURE__ */ jsx("img", {
114
+ src: srcDark,
115
+ alt: imgAlt,
116
+ width,
117
+ height,
118
+ className: t6$1
119
+ });
120
+ $[26] = height;
121
+ $[27] = imgAlt;
122
+ $[28] = srcDark;
123
+ $[29] = t6$1;
124
+ $[30] = width;
125
+ $[31] = t7;
126
+ } else t7 = $[31];
127
+ let t8;
128
+ if ($[32] !== t5$1 || $[33] !== t7) {
129
+ t8 = /* @__PURE__ */ jsxs(Fragment, { children: [t5$1, t7] });
130
+ $[32] = t5$1;
131
+ $[33] = t7;
132
+ $[34] = t8;
133
+ } else t8 = $[34];
134
+ return t8;
135
+ }
136
+ let t3;
137
+ if ($[35] !== logo.props) {
138
+ t3 = logo.props ?? {};
139
+ $[35] = logo.props;
140
+ $[36] = t3;
141
+ } else t3 = $[36];
142
+ let t4;
143
+ if ($[37] !== logo.type || $[38] !== t3) {
144
+ t4 = {
145
+ type: logo.type,
146
+ props: t3
147
+ };
148
+ $[37] = logo.type;
149
+ $[38] = t3;
150
+ $[39] = t4;
151
+ } else t4 = $[39];
152
+ let t5;
153
+ if ($[40] !== className) {
154
+ t5 = className ? { className } : void 0;
155
+ $[40] = className;
156
+ $[41] = t5;
157
+ } else t5 = $[41];
158
+ let t6;
159
+ if ($[42] !== fallback || $[43] !== t4 || $[44] !== t5) {
160
+ t6 = /* @__PURE__ */ jsx(ComponentRenderer, {
161
+ reference: t4,
162
+ fallback,
163
+ additionalProps: t5
164
+ });
165
+ $[42] = fallback;
166
+ $[43] = t4;
167
+ $[44] = t5;
168
+ $[45] = t6;
169
+ } else t6 = $[45];
170
+ return t6;
171
+ });
172
+
173
+ //#endregion
174
+ export { BrandLogoMark };
@@ -0,0 +1,7 @@
1
+ import { QuestpieApp, QuestpieClient, QuestpieClientConfig } from "questpie/client";
2
+
3
+ //#region src/client/create-admin-client.d.ts
4
+
5
+ declare function createAdminClient<TApp extends QuestpieApp>(config: QuestpieClientConfig): QuestpieClient<TApp>;
6
+ //#endregion
7
+ export { createAdminClient };
@@ -0,0 +1,25 @@
1
+ import { withAdminRequestHeader } from "../shared/preview-utils.mjs";
2
+ import { createClient } from "questpie/client";
3
+
4
+ //#region src/client/create-admin-client.ts
5
+ /**
6
+ * createAdminClient — typed CMS client preconfigured for admin SPA usage.
7
+ *
8
+ * Wraps `createClient` from `questpie/client` and auto-injects the
9
+ * `X-Questpie-Admin` request header on every outbound call. Server-side
10
+ * `isAdminRequest()` (and any access rule that uses it) can then branch
11
+ * on the header instead of relying on URL prefix matching.
12
+ *
13
+ * Use this for the client passed to `<AdminLayoutProvider client={...}>`.
14
+ * Keep your public/frontend client as plain `createClient` — that one
15
+ * MUST NOT inject the admin header.
16
+ */
17
+ function createAdminClient(config) {
18
+ return createClient({
19
+ ...config,
20
+ fetch: withAdminRequestHeader(config.fetch)
21
+ });
22
+ }
23
+
24
+ //#endregion
25
+ export { createAdminClient };
@@ -0,0 +1,22 @@
1
+ import { BrandLogoConfig } from "../types/admin-config.mjs";
2
+
3
+ //#region src/client/hooks/use-brand.d.ts
4
+ interface BrandSnapshot {
5
+ name: string;
6
+ logo: BrandLogoConfig | null;
7
+ tagline: string | null;
8
+ favicon: string | null;
9
+ }
10
+ /**
11
+ * Read branding fields from the admin store. Safe to call outside of an
12
+ * `<AdminProvider>` — falls back to the default snapshot when no store is
13
+ * mounted (e.g. on a bare auth page).
14
+ */
15
+ declare function useBrand(): BrandSnapshot;
16
+ /**
17
+ * Imperative variant for code paths that cannot use hooks (callbacks,
18
+ * server-side helpers). Returns null when no provider is mounted.
19
+ */
20
+ declare function useBrandSnapshotRef(): BrandSnapshot | null;
21
+ //#endregion
22
+ export { BrandSnapshot, useBrand, useBrandSnapshotRef };
@@ -0,0 +1,52 @@
1
+ import { useAdminStoreRaw } from "../runtime/provider.mjs";
2
+ import { c } from "react/compiler-runtime";
3
+ import { createStore, useStore } from "zustand";
4
+
5
+ //#region src/client/hooks/use-brand.ts
6
+ const DEFAULT_BRAND = {
7
+ name: "Admin",
8
+ logo: null,
9
+ tagline: null,
10
+ favicon: null
11
+ };
12
+ const FALLBACK_STORE = createStore(() => ({
13
+ brandName: DEFAULT_BRAND.name,
14
+ brandLogo: DEFAULT_BRAND.logo,
15
+ brandTagline: DEFAULT_BRAND.tagline,
16
+ brandFavicon: DEFAULT_BRAND.favicon
17
+ }));
18
+ function selectBrand(state) {
19
+ return {
20
+ name: state.brandName,
21
+ logo: state.brandLogo,
22
+ tagline: state.brandTagline,
23
+ favicon: state.brandFavicon
24
+ };
25
+ }
26
+ /**
27
+ * Read branding fields from the admin store. Safe to call outside of an
28
+ * `<AdminProvider>` — falls back to the default snapshot when no store is
29
+ * mounted (e.g. on a bare auth page).
30
+ */
31
+ function useBrand() {
32
+ return useStore(useAdminStoreRaw() ?? FALLBACK_STORE, selectBrand);
33
+ }
34
+ /**
35
+ * Imperative variant for code paths that cannot use hooks (callbacks,
36
+ * server-side helpers). Returns null when no provider is mounted.
37
+ */
38
+ function useBrandSnapshotRef() {
39
+ const $ = c(2);
40
+ const store = useAdminStoreRaw();
41
+ if (!store) return null;
42
+ let t0;
43
+ if ($[0] !== store) {
44
+ t0 = selectBrand(store.getState());
45
+ $[0] = store;
46
+ $[1] = t0;
47
+ } else t0 = $[1];
48
+ return t0;
49
+ }
50
+
51
+ //#endregion
52
+ export { useBrand, useBrandSnapshotRef };
@@ -1,4 +1,4 @@
1
- import { AdminProvider, selectAdmin, selectAuthClient, selectBasePath, selectBrandName, selectClient, selectContentLocale, selectNavigate, selectRealtime, selectSetContentLocale, useAdminStore } from "./provider.mjs";
1
+ import { AdminProvider, selectAdmin, selectAuthClient, selectBasePath, selectClient, selectContentLocale, selectNavigate, selectRealtime, selectSetContentLocale, useAdminStore } from "./provider.mjs";
2
2
  import { useSafeContentLocales } from "./content-locales-provider.mjs";
3
3
  import { LocaleScopeProvider, useScopedLocale } from "./locale-scope.mjs";
4
4
  import { useShallow } from "zustand/shallow";
@@ -2,6 +2,7 @@ import { I18nAdapter } from "../i18n/types.mjs";
2
2
  import { AdminState as AdminState$1, BlockDefinitionMap, ComponentDefinitionMap, FieldDefinitionMap, PageDefinitionMap, TranslationsMap, ViewDefinitionMap, WidgetDefinitionMap } from "../builder/admin-types.mjs";
3
3
  import { Admin, AdminInput } from "../builder/admin.mjs";
4
4
  import { AnyQuestpieClient } from "../builder/index.mjs";
5
+ import { BrandLogoConfig } from "../types/admin-config.mjs";
5
6
  import { NavigationGroup } from "./routes.mjs";
6
7
  import { ReactElement, ReactNode } from "react";
7
8
  import { QueryClient } from "@tanstack/react-query";
@@ -22,6 +23,9 @@ interface AdminState {
22
23
  setContentLocale: (locale: string) => void;
23
24
  navigation: NavigationGroup[];
24
25
  brandName: string;
26
+ brandLogo: BrandLogoConfig | null;
27
+ brandTagline: string | null;
28
+ brandFavicon: string | null;
25
29
  }
26
30
  type AdminStore = ReturnType<typeof createAdminStore>;
27
31
  interface CreateAdminStoreProps {
@@ -51,7 +51,10 @@ function createAdminStore({ admin, client, authClient, basePath, navigate, realt
51
51
  set({ contentLocale: newLocale });
52
52
  },
53
53
  navigation: buildNavigation(admin, { basePath }),
54
- brandName: "Admin"
54
+ brandName: "Admin",
55
+ brandLogo: null,
56
+ brandTagline: null,
57
+ brandFavicon: null
55
58
  }));
56
59
  }
57
60
  const AdminStoreContext = createContext(null);
@@ -317,6 +320,28 @@ function AdminProvider(t0) {
317
320
  function _temp(s) {
318
321
  return s.contentLocale;
319
322
  }
323
+ function resolveBrandText(value, fallback) {
324
+ if (typeof value === "string") return value;
325
+ if (value && typeof value === "object") {
326
+ const obj = value;
327
+ if (typeof obj.en === "string") return obj.en;
328
+ const first = Object.values(obj).find((v) => typeof v === "string");
329
+ if (typeof first === "string") return first;
330
+ }
331
+ return fallback;
332
+ }
333
+ function applyFavicon(href) {
334
+ if (typeof document === "undefined" || !href) return;
335
+ const FAVICON_ID = "qa-brand-favicon";
336
+ let link = document.getElementById(FAVICON_ID);
337
+ if (!link) {
338
+ link = document.createElement("link");
339
+ link.id = FAVICON_ID;
340
+ link.rel = "icon";
341
+ document.head.appendChild(link);
342
+ }
343
+ link.href = href;
344
+ }
320
345
  function BrandingSync() {
321
346
  const $ = c(3);
322
347
  const store = useContext(AdminStoreContext);
@@ -328,11 +353,18 @@ function BrandingSync() {
328
353
  const client = store.getState().client;
329
354
  if (!client || !client.routes?.getAdminConfig) return;
330
355
  client.routes.getAdminConfig().then((config) => {
331
- if (config?.branding?.name) {
332
- const name = config.branding.name;
333
- const resolved = typeof name === "string" ? name : typeof name === "object" && name !== null ? name.en ?? Object.values(name)[0] ?? "Admin" : "Admin";
334
- store.setState({ brandName: resolved });
356
+ const branding = config?.branding;
357
+ if (!branding) return;
358
+ const next = {};
359
+ if (branding.name !== void 0) next.brandName = resolveBrandText(branding.name, "Admin");
360
+ if (branding.logo !== void 0) next.brandLogo = branding.logo ?? null;
361
+ if (branding.tagline !== void 0) next.brandTagline = branding.tagline ? resolveBrandText(branding.tagline, "") : null;
362
+ if (branding.favicon !== void 0) {
363
+ const favicon = branding.favicon ?? null;
364
+ next.brandFavicon = favicon;
365
+ applyFavicon(favicon);
335
366
  }
367
+ if (Object.keys(next).length > 0) store.setState(next);
336
368
  }).catch(_temp2);
337
369
  };
338
370
  t1 = [store];
@@ -393,8 +425,6 @@ const selectRealtime = (s) => s.realtime;
393
425
  const selectContentLocale = (s) => s.contentLocale;
394
426
  /** Select setContentLocale function */
395
427
  const selectSetContentLocale = (s) => s.setContentLocale;
396
- /** Select brand name */
397
- const selectBrandName = (s) => s.brandName;
398
428
 
399
429
  //#endregion
400
- export { AdminProvider, selectAdmin, selectAuthClient, selectBasePath, selectBrandName, selectClient, selectContentLocale, selectNavigate, selectRealtime, selectSetContentLocale, useAdminStore, useAdminStoreRaw };
430
+ export { AdminProvider, selectAdmin, selectAuthClient, selectBasePath, selectClient, selectContentLocale, selectNavigate, selectRealtime, selectSetContentLocale, useAdminStore, useAdminStoreRaw };
@@ -58,6 +58,7 @@
58
58
  --font-sans Default for all UI body text and content
59
59
  --font-mono Technical content: code, kbd, IDs, timestamps
60
60
  --font-chrome UI chrome (buttons, labels, inputs, tabs, badges)
61
+ --font-heading Headings (h1-h6); defaults to var(--font-sans)
61
62
 
62
63
  -- Spacing ------------------------------------------------
63
64
  --spacing-card Internal card/panel padding (default: 16px)
@@ -139,6 +140,7 @@
139
140
  --font-mono:
140
141
  "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, monospace;
141
142
  --font-chrome: var(--font-sans);
143
+ --font-heading: var(--font-sans);
142
144
 
143
145
  /* Spacing Scale */
144
146
  --spacing-section: 1.5rem;
@@ -236,6 +238,7 @@
236
238
  --font-mono:
237
239
  "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, monospace;
238
240
  --font-chrome: var(--font-sans);
241
+ --font-heading: var(--font-heading);
239
242
 
240
243
  /* Radius: Autopilot neutral-soft ladder */
241
244
  --radius-xs: 4px;
@@ -744,6 +747,7 @@
744
747
  h4,
745
748
  h5,
746
749
  h6 {
750
+ font-family: var(--font-heading);
747
751
  letter-spacing: -0.02em;
748
752
  text-wrap: balance;
749
753
  }
@@ -0,0 +1,24 @@
1
+ import { I18nText } from "../i18n/types.mjs";
2
+ import "../../server/augmentation.mjs";
3
+ import "../../server/block/index.mjs";
4
+
5
+ //#region src/client/types/admin-config.d.ts
6
+
7
+ type BrandLogoConfig = string | {
8
+ src: string;
9
+ srcDark?: string;
10
+ alt?: string;
11
+ width?: number;
12
+ height?: number;
13
+ } | {
14
+ type: string;
15
+ props?: Record<string, unknown>;
16
+ };
17
+ type BrandingConfig = {
18
+ name?: I18nText;
19
+ logo?: BrandLogoConfig;
20
+ tagline?: I18nText;
21
+ favicon?: string;
22
+ };
23
+ //#endregion
24
+ export { BrandLogoConfig, BrandingConfig };
@@ -1,4 +1,4 @@
1
- import * as react_jsx_runtime2 from "react/jsx-runtime";
1
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
2
2
 
3
3
  //#region src/client/views/auth/accept-invite-form.d.ts
4
4
  /**
@@ -67,6 +67,6 @@ declare function AcceptInviteForm({
67
67
  className,
68
68
  error,
69
69
  minPasswordLength
70
- }: AcceptInviteFormProps): react_jsx_runtime2.JSX.Element;
70
+ }: AcceptInviteFormProps): react_jsx_runtime0.JSX.Element;
71
71
  //#endregion
72
72
  export { AcceptInviteForm };
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import * as react_jsx_runtime0 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime1 from "react/jsx-runtime";
3
3
 
4
4
  //#region src/client/views/auth/auth-layout.d.ts
5
5
 
@@ -22,6 +22,11 @@ type AuthLayoutProps = {
22
22
  /** Additional class name for the card */
23
23
  className?: string;
24
24
  };
25
+ declare function AuthDefaultLogo({
26
+ brandName
27
+ }: {
28
+ brandName: string;
29
+ }): react_jsx_runtime1.JSX.Element;
25
30
  /**
26
31
  * Minimal split layout for authentication pages (login, register, forgot password, etc.)
27
32
  *
@@ -45,6 +50,6 @@ type AuthLayoutProps = {
45
50
  * </AuthLayout>
46
51
  * ```
47
52
  */
48
- declare function AuthLayout(props: AuthLayoutProps): react_jsx_runtime0.JSX.Element;
53
+ declare function AuthLayout(props: AuthLayoutProps): react_jsx_runtime1.JSX.Element;
49
54
  //#endregion
50
- export { AuthLayout, AuthLayoutProps };
55
+ export { AuthDefaultLogo, AuthLayout, AuthLayoutProps };