@tumbaland/frontend-core 1.14.1 → 1.16.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/dist/authService.d.ts +10 -0
- package/dist/authService.js +18 -3
- package/dist/components/AppErrorBoundary/AppErrorBoundary.d.ts +26 -0
- package/dist/components/AppErrorBoundary/AppErrorBoundary.js +25 -0
- package/dist/components/AppErrorBoundary/index.d.ts +2 -0
- package/dist/components/AppErrorBoundary/index.js +1 -0
- package/dist/components/AppLayout/AppLayout.d.ts +27 -0
- package/dist/components/AppLayout/AppLayout.js +45 -0
- package/dist/components/AppLayout/index.d.ts +2 -0
- package/dist/components/AppLayout/index.js +1 -0
- package/dist/components/AuthHeader/AuthHeader.d.ts +57 -0
- package/dist/components/AuthHeader/AuthHeader.js +135 -0
- package/dist/components/AuthHeader/index.d.ts +2 -0
- package/dist/components/AuthHeader/index.js +1 -0
- package/dist/components/ErrorPage/ErrorPage.d.ts +17 -0
- package/dist/components/ErrorPage/ErrorPage.js +19 -0
- package/dist/components/ErrorPage/index.d.ts +2 -0
- package/dist/components/ErrorPage/index.js +1 -0
- package/dist/components/Loader/Loader.d.ts +12 -0
- package/dist/components/Loader/Loader.js +15 -0
- package/dist/components/Loader/index.d.ts +2 -0
- package/dist/components/Loader/index.js +1 -0
- package/dist/components/LoginButton/LoginButton.d.ts +5 -0
- package/dist/components/LoginButton/LoginButton.js +15 -0
- package/dist/components/LoginButton/index.d.ts +2 -0
- package/dist/components/LoginButton/index.js +1 -0
- package/dist/components/NotificationsMenu/NotificationsMenu.d.ts +9 -0
- package/dist/components/NotificationsMenu/NotificationsMenu.js +29 -0
- package/dist/components/ProtectedRoute/ProtectedRoute.d.ts +33 -0
- package/dist/components/ProtectedRoute/ProtectedRoute.js +50 -0
- package/dist/components/ProtectedRoute/index.d.ts +2 -0
- package/dist/components/ProtectedRoute/index.js +1 -0
- package/dist/components/SectionBar/SectionBar.d.ts +49 -0
- package/dist/components/SectionBar/SectionBar.js +143 -0
- package/dist/components/SectionBar/index.d.ts +2 -0
- package/dist/components/SectionBar/index.js +1 -0
- package/dist/components/UnauthorizedPage/UnauthorizedPage.d.ts +5 -0
- package/dist/components/UnauthorizedPage/UnauthorizedPage.js +9 -0
- package/dist/components/UnauthorizedPage/index.d.ts +2 -0
- package/dist/components/UnauthorizedPage/index.js +1 -0
- package/dist/components/UsageMeter/UsageMeter.d.ts +35 -0
- package/dist/components/UsageMeter/UsageMeter.js +50 -0
- package/dist/components/UsageMeter/index.d.ts +4 -0
- package/dist/components/UsageMeter/index.js +2 -0
- package/dist/components/UsageMeter/useEntitlements.d.ts +27 -0
- package/dist/components/UsageMeter/useEntitlements.js +56 -0
- package/dist/components/tenant/TenantProvider.d.ts +11 -0
- package/dist/components/tenant/TenantProvider.js +53 -0
- package/dist/components/tenant/TenantSelector.d.ts +33 -0
- package/dist/components/tenant/TenantSelector.js +88 -0
- package/dist/components/tenant/index.d.ts +5 -0
- package/dist/components/tenant/index.js +3 -0
- package/dist/components/tenant/useTenant.d.ts +15 -0
- package/dist/components/tenant/useTenant.js +278 -0
- package/dist/components/tenant/useTenantDataRefresh.d.ts +6 -0
- package/dist/components/tenant/useTenantDataRefresh.js +28 -0
- package/dist/config/createConfigProvider.d.ts +44 -0
- package/dist/config/createConfigProvider.js +92 -0
- package/dist/federation.d.ts +46 -0
- package/dist/federation.js +37 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +35 -1
- package/dist/monitoring/batch.js +3 -3
- package/dist/monitoring/config.js +1 -4
- package/dist/monitoring/console.js +1 -1
- package/dist/plansService.js +1 -1
- package/dist/routing/embedContext.d.ts +31 -0
- package/dist/routing/embedContext.js +63 -0
- package/dist/routing/index.d.ts +12 -0
- package/dist/routing/index.js +12 -0
- package/dist/sessionStorage.d.ts +1 -1
- package/dist/sessionStorage.js +1 -1
- package/dist/theme/createTumbalandTheme.d.ts +38 -0
- package/dist/theme/createTumbalandTheme.js +245 -0
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/tokens.d.ts +186 -0
- package/dist/theme/tokens.js +182 -0
- package/package.json +28 -2
- package/dist/test/setup.d.ts +0 -9
- package/dist/test/setup.js +0 -26
package/dist/index.d.ts
CHANGED
|
@@ -12,3 +12,31 @@ export type { User, AuthResponse, Group, ApiResponse } from './types';
|
|
|
12
12
|
export { TENANT_STORAGE_KEY, clearSessionScopedStorage } from './sessionStorage';
|
|
13
13
|
export { initMonitoring, captureException, captureMessage, setUser, setTag, startTransaction, recordMetric, initWebVitals, flushLogs, initConsoleInterception, generateCorrelationId, getCorrelationId, getSessionId, logger } from './monitoring';
|
|
14
14
|
export type { MonitoringConfig } from './monitoring';
|
|
15
|
+
export { Loader } from './components/Loader';
|
|
16
|
+
export { ErrorPage } from './components/ErrorPage';
|
|
17
|
+
export { AppErrorBoundary } from './components/AppErrorBoundary';
|
|
18
|
+
export { UnauthorizedPage } from './components/UnauthorizedPage';
|
|
19
|
+
export { ProtectedRoute } from './components/ProtectedRoute';
|
|
20
|
+
export { LoginButton } from './components/LoginButton';
|
|
21
|
+
export { NotificationsMenu } from './components/NotificationsMenu/NotificationsMenu';
|
|
22
|
+
export { AuthHeader } from './components/AuthHeader';
|
|
23
|
+
export { AppLayout } from './components/AppLayout';
|
|
24
|
+
export { SectionBar } from './components/SectionBar';
|
|
25
|
+
export { UsageMeter, formatBytes, useEntitlements } from './components/UsageMeter';
|
|
26
|
+
export { TenantSelector, TenantProvider, useTenant, useTenantListener, useTenantContext, useTenantFilter } from './components/tenant';
|
|
27
|
+
export type { LoaderProps } from './components/Loader';
|
|
28
|
+
export type { ErrorPageProps } from './components/ErrorPage';
|
|
29
|
+
export type { AppErrorBoundaryProps } from './components/AppErrorBoundary';
|
|
30
|
+
export type { AppLayoutProps } from './components/AppLayout';
|
|
31
|
+
export type { SectionBarProps, SectionBarItem } from './components/SectionBar';
|
|
32
|
+
export type { UnauthorizedPageProps } from './components/UnauthorizedPage';
|
|
33
|
+
export type { ProtectedRouteProps, ProtectedRouteAuthService } from './components/ProtectedRoute';
|
|
34
|
+
export type { LoginButtonProps } from './components/LoginButton';
|
|
35
|
+
export type { NotificationsMenuProps } from './components/NotificationsMenu/NotificationsMenu';
|
|
36
|
+
export type { AuthHeaderProps, AuthHeaderUser, AuthHeaderNavigationItem, AuthHeaderTenantOption, AuthHeaderTenantSelectorProps } from './components/AuthHeader';
|
|
37
|
+
export type { TenantOption, TenantSelectorProps, UseTenantResult } from './components/tenant';
|
|
38
|
+
export type { UsageMeterProps, UseEntitlementsResult } from './components/UsageMeter';
|
|
39
|
+
export { createConfigProvider } from './config/createConfigProvider';
|
|
40
|
+
export type { CreateConfigProviderOptions } from './config/createConfigProvider';
|
|
41
|
+
export { toOrigin } from './routing';
|
|
42
|
+
export { brand, ink, surface, gradient, moduleAccent, wordmark, glass, glassInteractive, chrome, brandButton, section, gutter, gutterSx, TOUCH_TARGET, PHONE, createTumbalandTheme, tumbalandTheme, TUMBALAND_COLORS } from './theme';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
// @tumbaland/frontend-core
|
|
1
|
+
// @tumbaland/frontend-core — everything the Tumbaland frontends share:
|
|
2
|
+
// the headless auth/group/API-client logic, and the React components and theme
|
|
3
|
+
// built on top of it.
|
|
4
|
+
//
|
|
5
|
+
// These were two packages, @tumbaland/frontend-core and @tumbaland/frontend-core,
|
|
6
|
+
// and every one of the eight frontends depended on both. Splitting them bought
|
|
7
|
+
// nothing a consumer could use and cost a release ordering problem: components
|
|
8
|
+
// pinned frontend-core exactly, so the two had to publish in dependency order
|
|
9
|
+
// or the published tarball pinned a version that was about to be replaced.
|
|
10
|
+
//
|
|
11
|
+
// The one thing the split did guarantee is kept by `./federation`: that entry
|
|
12
|
+
// point imports nothing, so the seven vite configs that read it in Node at
|
|
13
|
+
// build time never pull React in.
|
|
2
14
|
export { createApiClient, ApiError } from './apiClient';
|
|
3
15
|
export { createAuthService, createAppAuthService } from './authService';
|
|
4
16
|
export { createGroupService } from './groupService';
|
|
@@ -7,3 +19,25 @@ export { createPlansService } from './plansService';
|
|
|
7
19
|
export { TENANT_STORAGE_KEY, clearSessionScopedStorage } from './sessionStorage';
|
|
8
20
|
// Monitoring: headless logging, error capture, correlation IDs, and web-vitals
|
|
9
21
|
export { initMonitoring, captureException, captureMessage, setUser, setTag, startTransaction, recordMetric, initWebVitals, flushLogs, initConsoleInterception, generateCorrelationId, getCorrelationId, getSessionId, logger } from './monitoring';
|
|
22
|
+
// --- Components, theme and providers (formerly @tumbaland/frontend-core) ---
|
|
23
|
+
export { Loader } from './components/Loader';
|
|
24
|
+
export { ErrorPage } from './components/ErrorPage';
|
|
25
|
+
export { AppErrorBoundary } from './components/AppErrorBoundary';
|
|
26
|
+
export { UnauthorizedPage } from './components/UnauthorizedPage';
|
|
27
|
+
export { ProtectedRoute } from './components/ProtectedRoute';
|
|
28
|
+
export { LoginButton } from './components/LoginButton';
|
|
29
|
+
export { NotificationsMenu } from './components/NotificationsMenu/NotificationsMenu';
|
|
30
|
+
export { AuthHeader } from './components/AuthHeader';
|
|
31
|
+
export { AppLayout } from './components/AppLayout';
|
|
32
|
+
export { SectionBar } from './components/SectionBar';
|
|
33
|
+
// Entitlements: the usage bar and the hook that feeds it
|
|
34
|
+
export { UsageMeter, formatBytes, useEntitlements } from './components/UsageMeter';
|
|
35
|
+
// Tenant selection: provider, selector, and hooks shared across domain frontends
|
|
36
|
+
export { TenantSelector, TenantProvider, useTenant, useTenantListener, useTenantContext, useTenantFilter } from './components/tenant';
|
|
37
|
+
// Config provider factory
|
|
38
|
+
export { createConfigProvider } from './config/createConfigProvider';
|
|
39
|
+
// A plain URL helper; the iframe bridge it belonged to is gone (see routing/index).
|
|
40
|
+
export { toOrigin } from './routing';
|
|
41
|
+
// Theme: the palette, the MUI theme built from it, and the raw tokens for the
|
|
42
|
+
// places that have to name a colour outside a `sx` prop.
|
|
43
|
+
export { brand, ink, surface, gradient, moduleAccent, wordmark, glass, glassInteractive, chrome, brandButton, section, gutter, gutterSx, TOUCH_TARGET, PHONE, createTumbalandTheme, tumbalandTheme, TUMBALAND_COLORS } from './theme';
|
package/dist/monitoring/batch.js
CHANGED
|
@@ -18,9 +18,9 @@ const sendBatch = async () => {
|
|
|
18
18
|
const response = await fetch(config.elkUrl, {
|
|
19
19
|
method: 'POST',
|
|
20
20
|
headers: {
|
|
21
|
-
'Content-Type': 'application/json'
|
|
21
|
+
'Content-Type': 'application/json'
|
|
22
22
|
},
|
|
23
|
-
body: JSON.stringify(batchToSend)
|
|
23
|
+
body: JSON.stringify(batchToSend)
|
|
24
24
|
});
|
|
25
25
|
if (!response.ok) {
|
|
26
26
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
@@ -39,7 +39,7 @@ const sendBatch = async () => {
|
|
|
39
39
|
try {
|
|
40
40
|
console.warn('Failed to send log batch, dropping logs:', error);
|
|
41
41
|
if (config.isDevelopment) {
|
|
42
|
-
batchToSend.forEach(log => {
|
|
42
|
+
batchToSend.forEach((log) => {
|
|
43
43
|
console.warn('📝 [DROPPED LOG]:', JSON.stringify(log));
|
|
44
44
|
});
|
|
45
45
|
}
|
|
@@ -30,10 +30,7 @@ export const initMonitoring = (environment = 'development', options = {}) => {
|
|
|
30
30
|
console.log(`📝 Console interception enabled for levels: ${captureConsoleLevels.join(', ')}`);
|
|
31
31
|
}
|
|
32
32
|
// Initialize console interception and error handlers after modules are loaded
|
|
33
|
-
Promise.all([
|
|
34
|
-
import('./console'),
|
|
35
|
-
import('./errors')
|
|
36
|
-
]).then(([{ initConsoleInterception }, { captureException, captureMessage }]) => {
|
|
33
|
+
Promise.all([import('./console'), import('./errors')]).then(([{ initConsoleInterception }, { captureException, captureMessage }]) => {
|
|
37
34
|
// Initialize console interception with capture function
|
|
38
35
|
initConsoleInterception(captureConsoleLevels, captureMessage);
|
|
39
36
|
// Set up global error handlers
|
|
@@ -18,7 +18,7 @@ export const initConsoleInterception = (captureConsoleLevels = ['error', 'warn']
|
|
|
18
18
|
return;
|
|
19
19
|
// Send to logging API with recursion protection
|
|
20
20
|
try {
|
|
21
|
-
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
|
|
21
|
+
const message = args.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg))).join(' ');
|
|
22
22
|
// Set interception flag to prevent recursion during capture
|
|
23
23
|
window._monitoringInterceptingConsole = true;
|
|
24
24
|
captureMessageFn(message, level, {
|
package/dist/plansService.js
CHANGED
|
@@ -19,7 +19,7 @@ export function createPlansService(config) {
|
|
|
19
19
|
const plans = body?.plans ?? [];
|
|
20
20
|
// A plan without resolved limits cannot be rendered honestly — every line
|
|
21
21
|
// on its card is a limit — so drop it rather than draw a card with holes.
|
|
22
|
-
return plans.filter(plan => Boolean(plan?.effectiveLimits?.meters));
|
|
22
|
+
return plans.filter((plan) => Boolean(plan?.effectiveLimits?.meters));
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared by both sides of the shell/child iframe bridge.
|
|
3
|
+
*
|
|
4
|
+
* The child's initial path travels in the iframe `src` rather than over
|
|
5
|
+
* postMessage so the app boots straight into the deep-linked page instead of
|
|
6
|
+
* flashing its default route first. Every later navigation goes over
|
|
7
|
+
* postMessage — the `src` must stay stable or the child remounts.
|
|
8
|
+
*/
|
|
9
|
+
export declare const EMBED_FLAG = "embedded";
|
|
10
|
+
export declare const EMBED_PATH = "embedPath";
|
|
11
|
+
export interface EmbedContext {
|
|
12
|
+
embedded: boolean;
|
|
13
|
+
initialPath: string;
|
|
14
|
+
}
|
|
15
|
+
/** Path the child router starts on: always absolute, defaults to the root. */
|
|
16
|
+
export declare const normalizeEmbedPath: (path?: string | null) => string;
|
|
17
|
+
/** `/album` + `/albums/1?x=2` -> `/album/albums/1?x=2` (root path -> `/album`). */
|
|
18
|
+
export declare const joinShellPath: (appRoute: string, childPath: string) => string;
|
|
19
|
+
/** Everything after the app prefix, e.g. `/album/albums/1` -> `/albums/1`. */
|
|
20
|
+
export declare const extractChildPath: (appRoute: string, shellPath: string, search?: string) => string;
|
|
21
|
+
/** Config values are full URLs; postMessage needs a bare origin. */
|
|
22
|
+
export declare const toOrigin: (url?: string) => string | undefined;
|
|
23
|
+
export declare const safeUrl: (url: string, base?: string) => URL | undefined;
|
|
24
|
+
/** Iframe `src` for an app, carrying the embed flag and the deep-linked path. */
|
|
25
|
+
export declare const buildEmbeddedUrl: (appUrl: string, childPath: string) => string;
|
|
26
|
+
/**
|
|
27
|
+
* Read on the child side. Embedded only when actually framed *and* flagged, so
|
|
28
|
+
* a direct visit to album.tumbaland.eu always gets a real BrowserRouter even if
|
|
29
|
+
* someone pastes the query params.
|
|
30
|
+
*/
|
|
31
|
+
export declare const readEmbedContext: (search?: string, isFramed?: boolean) => EmbedContext;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared by both sides of the shell/child iframe bridge.
|
|
3
|
+
*
|
|
4
|
+
* The child's initial path travels in the iframe `src` rather than over
|
|
5
|
+
* postMessage so the app boots straight into the deep-linked page instead of
|
|
6
|
+
* flashing its default route first. Every later navigation goes over
|
|
7
|
+
* postMessage — the `src` must stay stable or the child remounts.
|
|
8
|
+
*/
|
|
9
|
+
export const EMBED_FLAG = 'embedded';
|
|
10
|
+
export const EMBED_PATH = 'embedPath';
|
|
11
|
+
/** Path the child router starts on: always absolute, defaults to the root. */
|
|
12
|
+
export const normalizeEmbedPath = (path) => {
|
|
13
|
+
if (!path)
|
|
14
|
+
return '/';
|
|
15
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
16
|
+
};
|
|
17
|
+
/** `/album` + `/albums/1?x=2` -> `/album/albums/1?x=2` (root path -> `/album`). */
|
|
18
|
+
export const joinShellPath = (appRoute, childPath) => {
|
|
19
|
+
const path = normalizeEmbedPath(childPath);
|
|
20
|
+
return path === '/' ? appRoute : `${appRoute}${path}`;
|
|
21
|
+
};
|
|
22
|
+
/** Everything after the app prefix, e.g. `/album/albums/1` -> `/albums/1`. */
|
|
23
|
+
export const extractChildPath = (appRoute, shellPath, search = '') => normalizeEmbedPath(shellPath.slice(appRoute.length) + search);
|
|
24
|
+
/** Config values are full URLs; postMessage needs a bare origin. */
|
|
25
|
+
export const toOrigin = (url) => {
|
|
26
|
+
if (!url)
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
return new URL(url).origin;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
export const safeUrl = (url, base) => {
|
|
36
|
+
try {
|
|
37
|
+
return new URL(url, base);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/** Iframe `src` for an app, carrying the embed flag and the deep-linked path. */
|
|
44
|
+
export const buildEmbeddedUrl = (appUrl, childPath) => {
|
|
45
|
+
const url = safeUrl(appUrl);
|
|
46
|
+
if (!url)
|
|
47
|
+
return appUrl;
|
|
48
|
+
url.searchParams.set(EMBED_FLAG, '1');
|
|
49
|
+
url.searchParams.set(EMBED_PATH, normalizeEmbedPath(childPath));
|
|
50
|
+
return url.toString();
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Read on the child side. Embedded only when actually framed *and* flagged, so
|
|
54
|
+
* a direct visit to album.tumbaland.eu always gets a real BrowserRouter even if
|
|
55
|
+
* someone pastes the query params.
|
|
56
|
+
*/
|
|
57
|
+
export const readEmbedContext = (search = typeof window === 'undefined' ? '' : window.location.search, isFramed = typeof window !== 'undefined' && window.self !== window.top) => {
|
|
58
|
+
const params = new URLSearchParams(search);
|
|
59
|
+
return {
|
|
60
|
+
embedded: isFramed && params.get(EMBED_FLAG) === '1',
|
|
61
|
+
initialPath: normalizeEmbedPath(params.get(EMBED_PATH))
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What survives of the shell/child bridge.
|
|
3
|
+
*
|
|
4
|
+
* The rest — `EmbeddedRouter`, `useShellBridge`, the postMessage contract and
|
|
5
|
+
* the URL helpers that built an iframe `src` — existed only because each module
|
|
6
|
+
* ran in its own cross-origin document. Modules are module-federation remotes
|
|
7
|
+
* now, rendered in the shell's own React tree by `shell-front/src/remotes.ts`,
|
|
8
|
+
* so there is no frame to sync a location into and no second window to ask.
|
|
9
|
+
*
|
|
10
|
+
* `toOrigin` stays because it is a plain URL helper with callers of its own.
|
|
11
|
+
*/
|
|
12
|
+
export { toOrigin } from './embedContext';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What survives of the shell/child bridge.
|
|
3
|
+
*
|
|
4
|
+
* The rest — `EmbeddedRouter`, `useShellBridge`, the postMessage contract and
|
|
5
|
+
* the URL helpers that built an iframe `src` — existed only because each module
|
|
6
|
+
* ran in its own cross-origin document. Modules are module-federation remotes
|
|
7
|
+
* now, rendered in the shell's own React tree by `shell-front/src/remotes.ts`,
|
|
8
|
+
* so there is no frame to sync a location into and no second window to ask.
|
|
9
|
+
*
|
|
10
|
+
* `toOrigin` stays because it is a plain URL helper with callers of its own.
|
|
11
|
+
*/
|
|
12
|
+
export { toOrigin } from './embedContext';
|
package/dist/sessionStorage.d.ts
CHANGED
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
* clearing it at the call site, so a new front or a second logout button
|
|
13
13
|
* cannot miss it.
|
|
14
14
|
*/
|
|
15
|
-
/** Tenant (personal vs group) context — owned by `useTenant` in @tumbaland/
|
|
15
|
+
/** Tenant (personal vs group) context — owned by `useTenant` in @tumbaland/frontend-core. */
|
|
16
16
|
export declare const TENANT_STORAGE_KEY = "selectedTenant";
|
|
17
17
|
export declare function clearSessionScopedStorage(): void;
|
package/dist/sessionStorage.js
CHANGED
|
@@ -13,7 +13,7 @@ import { logger } from './monitoring';
|
|
|
13
13
|
* clearing it at the call site, so a new front or a second logout button
|
|
14
14
|
* cannot miss it.
|
|
15
15
|
*/
|
|
16
|
-
/** Tenant (personal vs group) context — owned by `useTenant` in @tumbaland/
|
|
16
|
+
/** Tenant (personal vs group) context — owned by `useTenant` in @tumbaland/frontend-core. */
|
|
17
17
|
export const TENANT_STORAGE_KEY = 'selectedTenant';
|
|
18
18
|
const SESSION_SCOPED_KEYS = [TENANT_STORAGE_KEY];
|
|
19
19
|
export function clearSessionScopedStorage() {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Theme, type ThemeOptions } from '@mui/material/styles';
|
|
2
|
+
/**
|
|
3
|
+
* The one theme every Tumbaland front renders with.
|
|
4
|
+
*
|
|
5
|
+
* Before this, `createConfigProvider` offered an opt-in theme that set exactly
|
|
6
|
+
* one value — the page background — so seven of the eight fronts were drawing
|
|
7
|
+
* MUI's factory blue and Roboto underneath a pink product. Crossing from the
|
|
8
|
+
* shell into a framed module changed the typeface, the blue and the shape of
|
|
9
|
+
* every button, which is the seam the iframe layout was otherwise hiding well.
|
|
10
|
+
*
|
|
11
|
+
* `shape.borderRadius` stays MUI's 4px unit deliberately: pages across the
|
|
12
|
+
* apps write radii as theme multiples (`borderRadius: 2`), and a larger unit
|
|
13
|
+
* silently turns all of those into pills. Components get their rounder corners
|
|
14
|
+
* individually below.
|
|
15
|
+
*
|
|
16
|
+
* Most of what follows is there for phones. The fronts are used on one far
|
|
17
|
+
* more than on a desktop, and MUI's defaults are tuned the other way: 36px
|
|
18
|
+
* buttons, 14px inputs that make iOS zoom the page on focus, dialogs that keep
|
|
19
|
+
* desktop margins, tab strips that overflow rather than scroll.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createTumbalandTheme(overrides?: ThemeOptions): Theme;
|
|
22
|
+
/**
|
|
23
|
+
* The colours named outside a React tree: `theme-color` in each index.html,
|
|
24
|
+
* the manifest, the offline page.
|
|
25
|
+
*/
|
|
26
|
+
export declare const TUMBALAND_COLORS: {
|
|
27
|
+
readonly brand: "#d61f92";
|
|
28
|
+
readonly themeColor: "#fdf2fa";
|
|
29
|
+
readonly background: "#fbf9fd";
|
|
30
|
+
};
|
|
31
|
+
/** Built once at module scope — a theme rebuilt per render remounts styles. */
|
|
32
|
+
export declare const tumbalandTheme: Theme;
|
|
33
|
+
export declare const gutterSx: {
|
|
34
|
+
readonly px: {
|
|
35
|
+
readonly xs: 2;
|
|
36
|
+
readonly sm: 3;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { createTheme } from '@mui/material/styles';
|
|
2
|
+
import { brand, gutter, ink, PHONE, surface, TOUCH_TARGET } from './tokens';
|
|
3
|
+
const FONT_STACK = '"Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
|
|
4
|
+
/**
|
|
5
|
+
* The one theme every Tumbaland front renders with.
|
|
6
|
+
*
|
|
7
|
+
* Before this, `createConfigProvider` offered an opt-in theme that set exactly
|
|
8
|
+
* one value — the page background — so seven of the eight fronts were drawing
|
|
9
|
+
* MUI's factory blue and Roboto underneath a pink product. Crossing from the
|
|
10
|
+
* shell into a framed module changed the typeface, the blue and the shape of
|
|
11
|
+
* every button, which is the seam the iframe layout was otherwise hiding well.
|
|
12
|
+
*
|
|
13
|
+
* `shape.borderRadius` stays MUI's 4px unit deliberately: pages across the
|
|
14
|
+
* apps write radii as theme multiples (`borderRadius: 2`), and a larger unit
|
|
15
|
+
* silently turns all of those into pills. Components get their rounder corners
|
|
16
|
+
* individually below.
|
|
17
|
+
*
|
|
18
|
+
* Most of what follows is there for phones. The fronts are used on one far
|
|
19
|
+
* more than on a desktop, and MUI's defaults are tuned the other way: 36px
|
|
20
|
+
* buttons, 14px inputs that make iOS zoom the page on focus, dialogs that keep
|
|
21
|
+
* desktop margins, tab strips that overflow rather than scroll.
|
|
22
|
+
*/
|
|
23
|
+
export function createTumbalandTheme(overrides = {}) {
|
|
24
|
+
return createTheme({
|
|
25
|
+
palette: {
|
|
26
|
+
primary: {
|
|
27
|
+
main: brand.pinkInk,
|
|
28
|
+
light: brand.pink,
|
|
29
|
+
dark: brand.pinkInkDeep,
|
|
30
|
+
contrastText: '#ffffff'
|
|
31
|
+
},
|
|
32
|
+
secondary: {
|
|
33
|
+
main: brand.violetInk,
|
|
34
|
+
light: brand.violet,
|
|
35
|
+
dark: '#6d28d9',
|
|
36
|
+
contrastText: '#ffffff'
|
|
37
|
+
},
|
|
38
|
+
success: { main: '#15803d' },
|
|
39
|
+
warning: { main: '#b45309' },
|
|
40
|
+
error: { main: '#dc2626' },
|
|
41
|
+
info: { main: '#0369a1' },
|
|
42
|
+
background: { default: surface.default, paper: surface.paper },
|
|
43
|
+
text: { primary: ink.strong, secondary: ink.body, disabled: ink.faint },
|
|
44
|
+
divider: surface.divider
|
|
45
|
+
},
|
|
46
|
+
typography: {
|
|
47
|
+
fontFamily: FONT_STACK,
|
|
48
|
+
h1: { fontWeight: 800, letterSpacing: '-0.02em' },
|
|
49
|
+
h2: { fontWeight: 700, letterSpacing: '-0.02em' },
|
|
50
|
+
h3: { fontWeight: 700, letterSpacing: '-0.02em' },
|
|
51
|
+
// Page titles. MUI's 2.125rem h4 filled a phone's width with two words.
|
|
52
|
+
h4: { fontWeight: 700, letterSpacing: '-0.02em', fontSize: '1.75rem', [PHONE]: { fontSize: '1.375rem' } },
|
|
53
|
+
h5: { fontWeight: 700, letterSpacing: '-0.01em', [PHONE]: { fontSize: '1.15rem' } },
|
|
54
|
+
h6: { fontWeight: 600, [PHONE]: { fontSize: '1rem' } },
|
|
55
|
+
subtitle1: { fontWeight: 600 },
|
|
56
|
+
subtitle2: { fontWeight: 600 },
|
|
57
|
+
button: { textTransform: 'none', fontWeight: 600 }
|
|
58
|
+
},
|
|
59
|
+
shape: { borderRadius: 4 },
|
|
60
|
+
components: {
|
|
61
|
+
MuiCssBaseline: {
|
|
62
|
+
styleOverrides: {
|
|
63
|
+
html: {
|
|
64
|
+
// Landscape on iOS otherwise inflates body text on its own.
|
|
65
|
+
WebkitTextSizeAdjust: '100%'
|
|
66
|
+
},
|
|
67
|
+
body: {
|
|
68
|
+
WebkitFontSmoothing: 'antialiased',
|
|
69
|
+
MozOsxFontSmoothing: 'grayscale',
|
|
70
|
+
// One wide row — a table, a chart, a strip of thumbnails — used
|
|
71
|
+
// to make the whole page slide sideways under a thumb. Pages
|
|
72
|
+
// that genuinely scroll horizontally do it in their own box.
|
|
73
|
+
overflowX: 'hidden'
|
|
74
|
+
},
|
|
75
|
+
// Was an inline <style> block in shell-front's Layout, so only the
|
|
76
|
+
// shell had it and the framed modules showed platform scrollbars.
|
|
77
|
+
'*::-webkit-scrollbar': { width: 8, height: 8 },
|
|
78
|
+
'*::-webkit-scrollbar-track': { background: 'transparent' },
|
|
79
|
+
'*::-webkit-scrollbar-thumb': { background: 'rgba(139,92,246,0.22)', borderRadius: 4 },
|
|
80
|
+
'*::-webkit-scrollbar-thumb:hover': { background: 'rgba(139,92,246,0.42)' }
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
MuiContainer: {
|
|
84
|
+
styleOverrides: {
|
|
85
|
+
root: {
|
|
86
|
+
paddingLeft: 16,
|
|
87
|
+
paddingRight: 16,
|
|
88
|
+
'@media (min-width:600px)': { paddingLeft: 24, paddingRight: 24 }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
MuiButton: {
|
|
93
|
+
defaultProps: { disableElevation: true },
|
|
94
|
+
styleOverrides: {
|
|
95
|
+
root: { borderRadius: 12, [PHONE]: { minHeight: TOUCH_TARGET } },
|
|
96
|
+
sizeSmall: { borderRadius: 10, [PHONE]: { minHeight: 36 } },
|
|
97
|
+
sizeLarge: { paddingInline: 24, minHeight: 48 }
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
MuiIconButton: {
|
|
101
|
+
styleOverrides: {
|
|
102
|
+
root: { [PHONE]: { minWidth: TOUCH_TARGET, minHeight: TOUCH_TARGET } },
|
|
103
|
+
sizeSmall: { [PHONE]: { minWidth: 36, minHeight: 36 } }
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
MuiFab: {
|
|
107
|
+
styleOverrides: { root: { textTransform: 'none', fontWeight: 600 } }
|
|
108
|
+
},
|
|
109
|
+
MuiPaper: {
|
|
110
|
+
styleOverrides: {
|
|
111
|
+
root: { backgroundImage: 'none' },
|
|
112
|
+
rounded: { borderRadius: 14 },
|
|
113
|
+
outlined: { borderColor: surface.divider }
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
MuiCard: {
|
|
117
|
+
styleOverrides: {
|
|
118
|
+
root: {
|
|
119
|
+
borderRadius: 18,
|
|
120
|
+
border: `1px solid ${surface.divider}`,
|
|
121
|
+
boxShadow: '0 10px 30px -24px rgba(88,28,135,0.45)'
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
MuiCardContent: {
|
|
126
|
+
// 24px of padding on each side of a 360px screen is a quarter of it.
|
|
127
|
+
styleOverrides: { root: { [PHONE]: { padding: 16, '&:last-child': { paddingBottom: 16 } } } }
|
|
128
|
+
},
|
|
129
|
+
// Near-fullscreen on a phone: MUI keeps a desktop's margins, which
|
|
130
|
+
// leaves a form squeezed into the middle with nothing gained.
|
|
131
|
+
MuiDialog: {
|
|
132
|
+
styleOverrides: {
|
|
133
|
+
paper: {
|
|
134
|
+
borderRadius: 20,
|
|
135
|
+
[PHONE]: {
|
|
136
|
+
margin: 12,
|
|
137
|
+
width: 'calc(100% - 24px)',
|
|
138
|
+
maxWidth: 'calc(100% - 24px)',
|
|
139
|
+
maxHeight: 'calc(100% - 24px)',
|
|
140
|
+
borderRadius: 18
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
MuiDialogTitle: {
|
|
146
|
+
styleOverrides: { root: { fontWeight: 700, [PHONE]: { padding: '16px 16px 8px' } } }
|
|
147
|
+
},
|
|
148
|
+
MuiDialogContent: {
|
|
149
|
+
styleOverrides: { root: { [PHONE]: { padding: '8px 16px' } } }
|
|
150
|
+
},
|
|
151
|
+
MuiDialogActions: {
|
|
152
|
+
styleOverrides: { root: { padding: 16, gap: 8 } }
|
|
153
|
+
},
|
|
154
|
+
MuiMenu: {
|
|
155
|
+
styleOverrides: { paper: { borderRadius: 14 } }
|
|
156
|
+
},
|
|
157
|
+
MuiMenuItem: {
|
|
158
|
+
styleOverrides: { root: { [PHONE]: { minHeight: TOUCH_TARGET } } }
|
|
159
|
+
},
|
|
160
|
+
MuiListItemButton: {
|
|
161
|
+
styleOverrides: { root: { borderRadius: 10, [PHONE]: { minHeight: TOUCH_TARGET } } }
|
|
162
|
+
},
|
|
163
|
+
MuiOutlinedInput: {
|
|
164
|
+
styleOverrides: {
|
|
165
|
+
root: { borderRadius: 12, backgroundColor: surface.paper },
|
|
166
|
+
// 16px is the threshold below which iOS Safari zooms the page in
|
|
167
|
+
// when a field takes focus, and then leaves it zoomed.
|
|
168
|
+
input: { [PHONE]: { fontSize: 16 } }
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
MuiInputBase: {
|
|
172
|
+
styleOverrides: { input: { [PHONE]: { fontSize: 16 } } }
|
|
173
|
+
},
|
|
174
|
+
MuiSelect: {
|
|
175
|
+
styleOverrides: { select: { [PHONE]: { fontSize: 16 } } }
|
|
176
|
+
},
|
|
177
|
+
MuiAlert: {
|
|
178
|
+
styleOverrides: { root: { borderRadius: 12 } }
|
|
179
|
+
},
|
|
180
|
+
MuiChip: {
|
|
181
|
+
styleOverrides: { root: { fontWeight: 500, borderRadius: 8 } }
|
|
182
|
+
},
|
|
183
|
+
// Tab strips are the most common cause of a page wider than the phone
|
|
184
|
+
// it is on. Scrolling is the right default; a page that wants fixed
|
|
185
|
+
// tabs still passes `variant` itself.
|
|
186
|
+
MuiTabs: {
|
|
187
|
+
defaultProps: { variant: 'scrollable', scrollButtons: 'auto', allowScrollButtonsMobile: true },
|
|
188
|
+
styleOverrides: { root: { minHeight: 44 } }
|
|
189
|
+
},
|
|
190
|
+
MuiTab: {
|
|
191
|
+
styleOverrides: {
|
|
192
|
+
root: {
|
|
193
|
+
textTransform: 'none',
|
|
194
|
+
fontWeight: 600,
|
|
195
|
+
minHeight: 44,
|
|
196
|
+
[PHONE]: { minWidth: 'auto', paddingInline: 12, fontSize: '0.875rem' }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
MuiTableCell: {
|
|
201
|
+
styleOverrides: {
|
|
202
|
+
root: {
|
|
203
|
+
borderColor: surface.divider,
|
|
204
|
+
[PHONE]: { padding: '8px 10px', fontSize: '0.8125rem' }
|
|
205
|
+
},
|
|
206
|
+
head: {
|
|
207
|
+
backgroundColor: surface.sunken,
|
|
208
|
+
color: ink.body,
|
|
209
|
+
fontSize: '0.75rem',
|
|
210
|
+
fontWeight: 700,
|
|
211
|
+
letterSpacing: '0.04em',
|
|
212
|
+
textTransform: 'uppercase',
|
|
213
|
+
whiteSpace: 'nowrap'
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
MuiLinearProgress: {
|
|
218
|
+
styleOverrides: { root: { borderRadius: 999 }, bar: { borderRadius: 999 } }
|
|
219
|
+
},
|
|
220
|
+
MuiTooltip: {
|
|
221
|
+
// A tooltip is invisible on a touch screen, so anything that needs
|
|
222
|
+
// one also needs a visible label. Kept legible where it does show.
|
|
223
|
+
styleOverrides: { tooltip: { fontSize: '0.75rem', borderRadius: 8, padding: '6px 10px' } }
|
|
224
|
+
},
|
|
225
|
+
MuiSwitch: {
|
|
226
|
+
styleOverrides: { root: { [PHONE]: { marginInline: 4 } } }
|
|
227
|
+
},
|
|
228
|
+
MuiBottomNavigationAction: {
|
|
229
|
+
styleOverrides: { root: { minWidth: 0, [PHONE]: { minHeight: TOUCH_TARGET } } }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}, overrides);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The colours named outside a React tree: `theme-color` in each index.html,
|
|
236
|
+
* the manifest, the offline page.
|
|
237
|
+
*/
|
|
238
|
+
export const TUMBALAND_COLORS = {
|
|
239
|
+
brand: brand.pinkInk,
|
|
240
|
+
themeColor: '#fdf2fa',
|
|
241
|
+
background: surface.default
|
|
242
|
+
};
|
|
243
|
+
/** Built once at module scope — a theme rebuilt per render remounts styles. */
|
|
244
|
+
export const tumbalandTheme = createTumbalandTheme();
|
|
245
|
+
export const gutterSx = { px: gutter };
|