@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.
- package/README.md +99 -1
- package/dist/client/builder/types/field-types.d.mts +11 -0
- package/dist/client/components/brand-logo.d.mts +25 -0
- package/dist/client/components/brand-logo.mjs +174 -0
- package/dist/client/create-admin-client.d.mts +7 -0
- package/dist/client/create-admin-client.mjs +25 -0
- package/dist/client/hooks/use-brand.d.mts +22 -0
- package/dist/client/hooks/use-brand.mjs +52 -0
- package/dist/client/runtime/index.mjs +1 -1
- package/dist/client/runtime/provider.d.mts +4 -0
- package/dist/client/runtime/provider.mjs +38 -8
- package/dist/client/styles/base.css +4 -0
- package/dist/client/types/admin-config.d.mts +24 -0
- package/dist/client/views/auth/accept-invite-form.d.mts +2 -2
- package/dist/client/views/auth/auth-layout.d.mts +8 -3
- package/dist/client/views/auth/auth-layout.mjs +116 -102
- package/dist/client/views/auth/forgot-password-form.d.mts +2 -2
- package/dist/client/views/auth/login-form.d.mts +2 -2
- package/dist/client/views/auth/reset-password-form.d.mts +2 -2
- package/dist/client/views/auth/setup-form.d.mts +2 -2
- package/dist/client/views/collection/auto-form-fields.mjs +2 -0
- package/dist/client/views/collection/field-renderer.mjs +3 -2
- package/dist/client/views/globals/global-form-view.mjs +908 -863
- package/dist/client/views/layout/admin-sidebar.mjs +153 -141
- package/dist/client/views/pages/accept-invite-page.mjs +122 -144
- package/dist/client/views/pages/forgot-password-page.mjs +22 -30
- package/dist/client/views/pages/invite-page.mjs +24 -33
- package/dist/client/views/pages/login-page.d.mts +2 -2
- package/dist/client/views/pages/login-page.mjs +24 -32
- package/dist/client/views/pages/reset-password-page.d.mts +2 -2
- package/dist/client/views/pages/reset-password-page.mjs +77 -92
- package/dist/client/views/pages/setup-page.mjs +31 -39
- package/dist/client.d.mts +6 -2
- package/dist/client.mjs +5 -2
- package/dist/index.d.mts +6 -2
- package/dist/index.mjs +5 -2
- package/dist/server/augmentation/dashboard.d.mts +23 -5
- package/dist/server/augmentation/form-layout.d.mts +10 -0
- package/dist/server/augmentation/index.d.mts +1 -1
- package/dist/server/augmentation.d.mts +1 -1
- package/dist/server/modules/admin/collections/session.d.mts +42 -42
- package/dist/server/modules/admin/collections/user.d.mts +32 -32
- package/dist/server/modules/admin/collections/verification.d.mts +36 -36
- package/dist/server/modules/admin/dto/admin-config.dto.mjs +19 -1
- package/dist/server/modules/admin/index.d.mts +1 -1
- package/dist/server/modules/admin/routes/admin-config.d.mts +2 -2
- package/dist/server/modules/admin/routes/execute-action.d.mts +9 -9
- package/dist/server/modules/admin/routes/preview.d.mts +11 -11
- package/dist/server/modules/admin/routes/preview.mjs +1 -1
- package/dist/server/modules/admin/routes/reactive.d.mts +9 -9
- package/dist/server/modules/admin/routes/setup.d.mts +7 -7
- package/dist/server/modules/admin/routes/widget-data.d.mts +5 -5
- package/dist/server/modules/admin-preferences/collections/saved-views.d.mts +23 -23
- package/dist/server.d.mts +4 -4
- package/dist/shared/preview-utils.d.mts +34 -1
- package/dist/shared/preview-utils.mjs +79 -1
- package/dist/shared.d.mts +2 -2
- package/dist/shared.mjs +2 -2
- 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: {
|
|
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,
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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,
|
|
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
|
|
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):
|
|
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
|
|
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):
|
|
53
|
+
declare function AuthLayout(props: AuthLayoutProps): react_jsx_runtime1.JSX.Element;
|
|
49
54
|
//#endregion
|
|
50
|
-
export { AuthLayout, AuthLayoutProps };
|
|
55
|
+
export { AuthDefaultLogo, AuthLayout, AuthLayoutProps };
|