@tumbaland/frontend-core 1.14.0 → 1.15.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/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 +24 -0
- package/dist/components/tenant/TenantSelector.js +77 -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 +222 -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 +26 -0
- package/dist/index.js +34 -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 +169 -0
- package/dist/theme/tokens.js +162 -0
- package/package.json +28 -2
- package/dist/test/setup.d.ts +0 -9
- package/dist/test/setup.js +0 -26
|
@@ -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 };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place the look is decided, for all eight fronts.
|
|
3
|
+
*
|
|
4
|
+
* This file used to live in shell-front, which meant the signed-in home page
|
|
5
|
+
* was the only screen that knew what colour Tumbaland is. The other seven
|
|
6
|
+
* frontends fell back to MUI's factory defaults — Roboto and `#1976d2` — and
|
|
7
|
+
* then painted over them with literals, which is how three palettes ended up
|
|
8
|
+
* in the product at once: the pink below, an indigo one in a dozen gradient
|
|
9
|
+
* literals, and a sky-blue one in five copies of `AppLayout`.
|
|
10
|
+
*
|
|
11
|
+
* Naming the palette once, in the package every front already depends on,
|
|
12
|
+
* makes it a knob rather than a search-and-replace.
|
|
13
|
+
*
|
|
14
|
+
* Two pinks, on purpose. `pink` is the identity — bright, and only ever used
|
|
15
|
+
* for gradients and large decorative fills, where its 3.5:1 against white does
|
|
16
|
+
* not matter. `pinkInk` is the one anything readable or clickable uses: the
|
|
17
|
+
* same hue walked down to 4.7:1, which clears WCAG AA as text on white and as
|
|
18
|
+
* a button behind white text. Reaching for `pink` on a label is the mistake
|
|
19
|
+
* this pair exists to prevent.
|
|
20
|
+
*/
|
|
21
|
+
/** The accent family. Pink leads; violet is what it resolves into. */
|
|
22
|
+
export declare const brand: {
|
|
23
|
+
/** Decorative only — gradient start, large fills. Fails AA as text. */
|
|
24
|
+
readonly pink: "#ff1fae";
|
|
25
|
+
/** Decorative — the hover end of the gradient. */
|
|
26
|
+
readonly pinkDeep: "#f01aa2";
|
|
27
|
+
/** The readable pink: text, icons, buttons, anything interactive. AA on white. */
|
|
28
|
+
readonly pinkInk: "#d61f92";
|
|
29
|
+
/** Pressed and hover states for `pinkInk`. */
|
|
30
|
+
readonly pinkInkDeep: "#b3177a";
|
|
31
|
+
readonly magenta: "#c026d3";
|
|
32
|
+
/** Decorative — gradient end. */
|
|
33
|
+
readonly violet: "#8b5cf6";
|
|
34
|
+
readonly violetDeep: "#7c4de8";
|
|
35
|
+
/** The readable violet, for the same reason as `pinkInk`. AA on white. */
|
|
36
|
+
readonly violetInk: "#7c3aed";
|
|
37
|
+
readonly purple: "#9333ea";
|
|
38
|
+
};
|
|
39
|
+
/** Text, darkest to lightest. Slate rather than grey, to sit under the pink. */
|
|
40
|
+
export declare const ink: {
|
|
41
|
+
readonly strong: "#0f172a";
|
|
42
|
+
readonly body: "#475569";
|
|
43
|
+
readonly muted: "#5f6d80";
|
|
44
|
+
/** Decorative only — 2.6:1 on white. Never put words a reader needs in this. */
|
|
45
|
+
readonly faint: "#94a3b8";
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Page and card backgrounds.
|
|
49
|
+
*
|
|
50
|
+
* `page` replaces the three different radial gradients the AppLayouts each had
|
|
51
|
+
* (two sky-blue, one indigo). It is the pink-to-violet wash, so a module opened
|
|
52
|
+
* inside the shell's iframe sits on the same backdrop as the shell around it —
|
|
53
|
+
* which is what made the seam between them visible before.
|
|
54
|
+
*/
|
|
55
|
+
export declare const surface: {
|
|
56
|
+
readonly page: "radial-gradient(circle at 50% 0%, #fdf2fa 0%, #f3edff 55%, #fbfcff 100%)";
|
|
57
|
+
readonly default: "#fbf9fd";
|
|
58
|
+
readonly paper: "#ffffff";
|
|
59
|
+
/** Table heads, inset panels, anything one step back from `paper`. */
|
|
60
|
+
readonly sunken: "#faf7fc";
|
|
61
|
+
readonly divider: "#ece7f2";
|
|
62
|
+
};
|
|
63
|
+
export declare const gradient: {
|
|
64
|
+
readonly brand: "linear-gradient(135deg, #ff1fae 0%, #8b5cf6 100%)";
|
|
65
|
+
readonly brandHover: "linear-gradient(135deg, #f01aa2 0%, #7c4de8 100%)";
|
|
66
|
+
/** For a large tint behind a section, where the full-strength gradient shouts. */
|
|
67
|
+
readonly wash: "linear-gradient(135deg, #ff1fae14 0%, #8b5cf614 100%)";
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Per-module accent, keyed by the shell's route.
|
|
71
|
+
*
|
|
72
|
+
* These were literals in shell-front's `apps.ts`, and four of the six failed
|
|
73
|
+
* AA against white — Money's `#22c55e` sat at 2.3:1 as the active tab's label,
|
|
74
|
+
* which on a phone in daylight is the one colour that has to read. Same hues,
|
|
75
|
+
* walked down until they clear 4.5:1.
|
|
76
|
+
*/
|
|
77
|
+
export declare const moduleAccent: {
|
|
78
|
+
readonly home: "#9333ea";
|
|
79
|
+
readonly journal: "#e11d48";
|
|
80
|
+
readonly album: "#2563eb";
|
|
81
|
+
readonly money: "#15803d";
|
|
82
|
+
readonly profile: "#0f172a";
|
|
83
|
+
readonly groups: "#7c3aed";
|
|
84
|
+
readonly billing: "#4f46e5";
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* The script wordmark, gradient-filled.
|
|
88
|
+
*
|
|
89
|
+
* Used by the landing hero and by the signed-in greeting. It was written out
|
|
90
|
+
* twice with the same eight properties; a single definition is the only reason
|
|
91
|
+
* the two can be trusted to match.
|
|
92
|
+
*/
|
|
93
|
+
export declare const wordmark: {
|
|
94
|
+
readonly fontFamily: "\"Pacifico\", cursive";
|
|
95
|
+
readonly lineHeight: 1.25;
|
|
96
|
+
readonly letterSpacing: 1;
|
|
97
|
+
readonly background: "linear-gradient(135deg, #ff1fae 0%, #8b5cf6 100%)";
|
|
98
|
+
readonly backgroundClip: "text";
|
|
99
|
+
readonly WebkitBackgroundClip: "text";
|
|
100
|
+
readonly WebkitTextFillColor: "transparent";
|
|
101
|
+
readonly filter: "drop-shadow(0 4px 8px rgba(0,0,0,0.08))";
|
|
102
|
+
readonly pb: 1;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* The frosted card both pages are built from.
|
|
106
|
+
*
|
|
107
|
+
* Translucent on purpose: the cloud backdrop shows through, which is what ties
|
|
108
|
+
* a card to the page rather than laying it on top.
|
|
109
|
+
*/
|
|
110
|
+
export declare const glass: {
|
|
111
|
+
readonly borderRadius: 3.5;
|
|
112
|
+
readonly background: "rgba(255,255,255,0.62)";
|
|
113
|
+
readonly backdropFilter: "blur(10px)";
|
|
114
|
+
readonly border: "1px solid rgba(255,255,255,0.7)";
|
|
115
|
+
readonly boxShadow: "0 10px 30px -22px rgba(31,38,135,0.5)";
|
|
116
|
+
};
|
|
117
|
+
/** A card that responds to being pointed at. `accent` tints the lifted shadow. */
|
|
118
|
+
export declare const glassInteractive: (accent?: string) => {
|
|
119
|
+
readonly transition: "transform .2s ease, box-shadow .2s ease";
|
|
120
|
+
readonly '&:hover': {
|
|
121
|
+
readonly transform: "translateY(-3px)";
|
|
122
|
+
readonly boxShadow: `0 20px 38px -24px ${string}cc`;
|
|
123
|
+
};
|
|
124
|
+
readonly borderRadius: 3.5;
|
|
125
|
+
readonly background: "rgba(255,255,255,0.62)";
|
|
126
|
+
readonly backdropFilter: "blur(10px)";
|
|
127
|
+
readonly border: "1px solid rgba(255,255,255,0.7)";
|
|
128
|
+
readonly boxShadow: "0 10px 30px -22px rgba(31,38,135,0.5)";
|
|
129
|
+
};
|
|
130
|
+
/** The primary call to action, wherever one appears. */
|
|
131
|
+
export declare const brandButton: {
|
|
132
|
+
readonly textTransform: "none";
|
|
133
|
+
readonly fontWeight: 600;
|
|
134
|
+
readonly borderRadius: 3;
|
|
135
|
+
readonly background: "linear-gradient(135deg, #ff1fae 0%, #8b5cf6 100%)";
|
|
136
|
+
readonly boxShadow: "0 10px 28px -10px rgba(168,45,190,0.75)";
|
|
137
|
+
readonly '&:hover': {
|
|
138
|
+
readonly background: "linear-gradient(135deg, #f01aa2 0%, #7c4de8 100%)";
|
|
139
|
+
readonly boxShadow: "0 12px 32px -10px rgba(168,45,190,0.9)";
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
/** Vertical rhythm, so sections on both pages breathe identically. */
|
|
143
|
+
export declare const section: {
|
|
144
|
+
readonly px: 2;
|
|
145
|
+
readonly py: {
|
|
146
|
+
readonly xs: 5;
|
|
147
|
+
readonly md: 7;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* The page gutter, as every layout and page should spell it.
|
|
152
|
+
*
|
|
153
|
+
* 16px on a phone is the narrowest that still keeps text off the screen's
|
|
154
|
+
* curved edge; widening it past `sm` is what stops a card looking stranded on
|
|
155
|
+
* a desktop. Pages that hardcoded `p: 3` lost 48px of a 360px screen to
|
|
156
|
+
* nothing.
|
|
157
|
+
*/
|
|
158
|
+
export declare const gutter: {
|
|
159
|
+
readonly xs: 2;
|
|
160
|
+
readonly sm: 3;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Minimum hit area for anything tapped, per the WCAG 2.2 target-size rule and
|
|
164
|
+
* both platform guidelines. MUI's medium button is 36px tall, which is a miss
|
|
165
|
+
* often enough to feel broken on a phone.
|
|
166
|
+
*/
|
|
167
|
+
export declare const TOUCH_TARGET = 44;
|
|
168
|
+
/** Below `sm` — the phone layout. Matches `theme.breakpoints.down('sm')`. */
|
|
169
|
+
export declare const PHONE = "@media (max-width:599.95px)";
|