robobyte-front-builder 1.0.27 → 1.0.28
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/INTEGRATION.md +6 -0
- package/README.md +109 -21
- package/package.json +1 -1
- package/src/lib/index.js +14 -0
- package/src/lib/muiTheme.js +655 -0
- package/src/lib/providers/RoboByteFrontBuilderProvider.jsx +45 -1
- package/src/pages/_app.js +1 -0
- package/src/pages/printBuilder/index.jsx +26 -19
- package/src/pages/viewBuilder/index.jsx +29 -19
package/INTEGRATION.md
CHANGED
|
@@ -176,6 +176,10 @@ function RoboByteBridge({ children }) {
|
|
|
176
176
|
// Optional — Quartz theme overrides for every <AgGridReact> in the
|
|
177
177
|
// package. Merged on top of the package defaults.
|
|
178
178
|
// agGridTheme={{ accentColor: '#3b82f6', headerHeight: 32 }}
|
|
179
|
+
// Optional — MUI theme overrides for every MUI component the package
|
|
180
|
+
// renders. Deep-merged on top of DEFAULT_MUI_THEME_OPTIONS in
|
|
181
|
+
// lib/muiTheme.js (Inter font, 8px radius, soft shadows, flat buttons).
|
|
182
|
+
// muiTheme={{ palette: { primary: { main: '#6366f1' } }, shape: { borderRadius: 12 } }}
|
|
179
183
|
>
|
|
180
184
|
{children}
|
|
181
185
|
</RoboByteFrontBuilderProvider>
|
|
@@ -204,6 +208,8 @@ export default function App({ Component, pageProps }) {
|
|
|
204
208
|
| `accessToken` | string | Bearer token — attached to every robobyte service call |
|
|
205
209
|
| `agGridLicenseKey` | string | AG Grid Enterprise license key. The provider calls `LicenseManager.setLicenseKey()` and registers `AllEnterpriseModule` + `IntegratedChartsModule.with(AgChartsEnterpriseModule)` internally. |
|
|
206
210
|
| `agGridTheme` | object | Optional. Quartz theme overrides applied to every `<AgGridReact>` in the package. Merged on top of `DEFAULT_AG_THEME_PARAMS` (see [`src/lib/agGridTheme.js`](src/lib/agGridTheme.js)). Example: `{ accentColor: '#3b82f6', headerHeight: 32 }`. |
|
|
211
|
+
| `muiTheme` | object \| Theme | Optional. MUI theme overrides (options object) deep-merged on top of `DEFAULT_MUI_THEME_OPTIONS` (see [`src/lib/muiTheme.js`](src/lib/muiTheme.js)), OR a pre-built MUI `Theme` used as-is. Defaults give Inter font, 8px radius, soft shadows, flat buttons. Example: `{ palette: { primary: { main: '#6366f1' } }, shape: { borderRadius: 12 } }`. |
|
|
212
|
+
| `disableMuiTheme` | boolean | Optional. When `true`, the provider skips its inner `<ThemeProvider>` — use this when the host has its own MUI ThemeProvider wrapping the app and wants it to apply inside the builder pages without merging. Default `false`. |
|
|
207
213
|
| `navExtensions` | array | Static nav items to inject into the sidebar (optional) |
|
|
208
214
|
| `endpoints` | object | Full-URL overrides per endpoint group + name (optional) |
|
|
209
215
|
|
package/README.md
CHANGED
|
@@ -39,27 +39,28 @@ A low-code **UI Builder**, **Report Builder**, **Print Layout Designer**, and **
|
|
|
39
39
|
8. [Navigation Extension API](#navigation-extension-api)
|
|
40
40
|
9. [Provider props reference](#provider-props-reference)
|
|
41
41
|
10. [AG Grid theme](#ag-grid-theme)
|
|
42
|
-
11. [
|
|
43
|
-
12. [
|
|
44
|
-
13. [
|
|
45
|
-
14. [
|
|
46
|
-
15. [
|
|
47
|
-
16. [
|
|
48
|
-
17. [
|
|
49
|
-
18. [
|
|
50
|
-
19. [
|
|
51
|
-
20. [
|
|
52
|
-
21. [
|
|
53
|
-
22. [
|
|
54
|
-
23. [
|
|
55
|
-
24. [
|
|
56
|
-
25. [
|
|
57
|
-
26. [
|
|
58
|
-
27. [
|
|
59
|
-
28. [
|
|
60
|
-
29. [
|
|
61
|
-
30. [
|
|
62
|
-
31. [
|
|
42
|
+
11. [MUI theme](#mui-theme)
|
|
43
|
+
12. [fetchReportDataByPageId](#fetchreportdatabypageid)
|
|
44
|
+
13. [ReportViewer as a component](#reportviewer-as-a-component)
|
|
45
|
+
14. [Data Grid component](#data-grid-component)
|
|
46
|
+
15. [Dialog component](#dialog-component)
|
|
47
|
+
16. [Popover component](#popover-component)
|
|
48
|
+
17. [Excel Upload component](#excel-upload-component)
|
|
49
|
+
18. [Wizard component](#wizard-component)
|
|
50
|
+
19. [Repeater component](#repeater-component)
|
|
51
|
+
20. [Menu component](#menu-component)
|
|
52
|
+
21. [View Renderer component](#view-renderer-component)
|
|
53
|
+
22. [Layout Grid component](#layout-grid-component)
|
|
54
|
+
23. [Breadcrumb component](#breadcrumb-component)
|
|
55
|
+
24. [Print Layout Builder](#print-layout-builder)
|
|
56
|
+
25. [Calculation Scope Reference](#calculation-scope-reference)
|
|
57
|
+
26. [KPI Component Actions](#kpi-component-actions)
|
|
58
|
+
27. [Global Data Store](#global-data-store)
|
|
59
|
+
28. [Dark / Light theme](#dark--light-theme)
|
|
60
|
+
29. [Syncing local changes](#syncing-local-changes)
|
|
61
|
+
30. [Troubleshooting](#troubleshooting)
|
|
62
|
+
31. [Publishing](#publishing)
|
|
63
|
+
32. [Changelog](#changelog)
|
|
63
64
|
|
|
64
65
|
---
|
|
65
66
|
|
|
@@ -198,6 +199,14 @@ function RoboByteBridge({ children }) {
|
|
|
198
199
|
// "AG Grid theme" section for the full list of params and the
|
|
199
200
|
// programmatic API. Omit to use the package defaults.
|
|
200
201
|
agGridTheme={{ accentColor: '#3b82f6' }}
|
|
202
|
+
// Optional: MUI theme overrides applied to every MUI component the
|
|
203
|
+
// package renders. Deep-merged on top of DEFAULT_MUI_THEME_OPTIONS.
|
|
204
|
+
// See the "MUI theme" section for the full token list. Omit to use
|
|
205
|
+
// the package defaults (Inter font, 8px radius, soft shadows, etc.).
|
|
206
|
+
muiTheme={{
|
|
207
|
+
palette: { primary: { main: '#3b82f6' } },
|
|
208
|
+
shape: { borderRadius: 10 },
|
|
209
|
+
}}
|
|
201
210
|
>
|
|
202
211
|
{children}
|
|
203
212
|
</RoboByteFrontBuilderProvider>
|
|
@@ -344,6 +353,8 @@ function MyFeaturePlugin() {
|
|
|
344
353
|
| `accessToken` | string | Bearer token attached to every service call |
|
|
345
354
|
| `agGridLicenseKey` | string | AG Grid Enterprise license key — the provider calls `LicenseManager.setLicenseKey()` and registers all enterprise modules internally |
|
|
346
355
|
| `agGridTheme` | object | Quartz theme overrides applied to every `<AgGridReact>` in the package. Merged on top of `DEFAULT_AG_THEME_PARAMS`. Example: `{ accentColor: '#3b82f6', headerHeight: 32 }`. See [AG Grid theme](#ag-grid-theme). |
|
|
356
|
+
| `muiTheme` | object \| Theme | MUI theme overrides (options object) deep-merged on top of `DEFAULT_MUI_THEME_OPTIONS`, OR a pre-built MUI `Theme` used as-is. Example: `{ palette: { primary: { main: '#6366f1' } }, shape: { borderRadius: 12 } }`. See [MUI theme](#mui-theme). |
|
|
357
|
+
| `disableMuiTheme` | boolean | When `true`, the provider skips its inner `<ThemeProvider>` entirely — use this if the host app already wraps everything in its own MUI ThemeProvider and wants it to apply inside the builder pages without merging. Default `false`. |
|
|
347
358
|
| `navExtensions` | array | Static nav items to inject into the sidebar |
|
|
348
359
|
| `endpoints` | object | Full-URL overrides per endpoint group + name |
|
|
349
360
|
|
|
@@ -403,6 +414,83 @@ The hook falls back to `DEFAULT_AG_THEME` when called outside `RoboByteFrontBuil
|
|
|
403
414
|
|
|
404
415
|
---
|
|
405
416
|
|
|
417
|
+
## MUI theme
|
|
418
|
+
|
|
419
|
+
Every Material-UI component the package renders (`Button`, `TextField`, `Dialog`, `Tooltip`, `Chip`, `Card`, `Tabs`, …) reads its theme from a single MUI `<ThemeProvider>` mounted by `RoboByteFrontBuilderProvider`. The default theme lives at [`src/lib/muiTheme.js`](src/lib/muiTheme.js) and is designed to look distinctly *not* like stock MUI — Inter typography, 8px radius, soft shadows, flat buttons, no ripple, faster tooltips, dark mode pair.
|
|
420
|
+
|
|
421
|
+
### Key default tokens
|
|
422
|
+
|
|
423
|
+
| Token | Value | Rationale |
|
|
424
|
+
|---|---|---|
|
|
425
|
+
| `typography.fontFamily` | `Inter, SF Pro Display, system-ui, …` | Modern UI font with graceful system fallbacks. |
|
|
426
|
+
| `shape.borderRadius` | `8` | Soft corners across the surface (vs MUI's default 4). |
|
|
427
|
+
| `palette.primary.main` | `#3b82f6` | Tailwind blue-500, professional neutral primary. |
|
|
428
|
+
| `palette.secondary.main` | `#FF1185` | Matches `DEFAULT_AG_THEME_PARAMS.accentColor` — brand color shared with AG Grid surfaces. |
|
|
429
|
+
| `shadows[1..8]` | soft, large-radius | Tailwind-like elevation instead of Material's hard shadows. |
|
|
430
|
+
| `MuiButton.disableElevation` | `true` | Flat buttons by default. |
|
|
431
|
+
| `MuiButton.disableRipple` | `true` | Desktop-first. |
|
|
432
|
+
| `MuiTextField.size` | `'small'` | Builder UI is data-dense. |
|
|
433
|
+
| `MuiTooltip.arrow` / `enterDelay` | `true` / `200ms` | Less hover-spam, more obvious anchoring. |
|
|
434
|
+
|
|
435
|
+
### Host-app override via the provider
|
|
436
|
+
|
|
437
|
+
```jsx
|
|
438
|
+
<RoboByteFrontBuilderProvider
|
|
439
|
+
muiTheme={{
|
|
440
|
+
palette: {
|
|
441
|
+
primary: { main: '#6366f1' }, // indigo-500
|
|
442
|
+
},
|
|
443
|
+
shape: { borderRadius: 12 },
|
|
444
|
+
typography: { fontFamily: '"Plus Jakarta Sans", sans-serif' },
|
|
445
|
+
}}
|
|
446
|
+
>
|
|
447
|
+
<App />
|
|
448
|
+
</RoboByteFrontBuilderProvider>
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
The override is deep-merged on top of the package defaults — keys you set win, everything else falls back.
|
|
452
|
+
|
|
453
|
+
### Three ways to pass `muiTheme`
|
|
454
|
+
|
|
455
|
+
| Pass | What happens |
|
|
456
|
+
|---|---|
|
|
457
|
+
| Nothing | Uses `DEFAULT_MUI_THEME`. |
|
|
458
|
+
| Theme **options** object (`{ palette, shape, typography, components, … }`) | Deep-merged with defaults; package builds the final `Theme`. |
|
|
459
|
+
| A pre-built MUI `Theme` (from your own `createTheme`) | Used as-is. Escape hatch for full control. |
|
|
460
|
+
|
|
461
|
+
### Disable the inner ThemeProvider
|
|
462
|
+
|
|
463
|
+
If the host has its own `<ThemeProvider>` wrapping the entire app and wants it to apply inside the builder pages without merging, pass `disableMuiTheme={true}`:
|
|
464
|
+
|
|
465
|
+
```jsx
|
|
466
|
+
<ThemeProvider theme={yourGlobalTheme}>
|
|
467
|
+
<RoboByteFrontBuilderProvider disableMuiTheme>
|
|
468
|
+
<App />
|
|
469
|
+
</RoboByteFrontBuilderProvider>
|
|
470
|
+
</ThemeProvider>
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Programmatic use
|
|
474
|
+
|
|
475
|
+
```js
|
|
476
|
+
import {
|
|
477
|
+
DEFAULT_MUI_THEME_OPTIONS, // raw default options (frozen)
|
|
478
|
+
DARK_MUI_THEME_OPTIONS, // dark counterpart
|
|
479
|
+
DEFAULT_MUI_THEME, // pre-built light Theme
|
|
480
|
+
DEFAULT_MUI_THEME_DARK, // pre-built dark Theme
|
|
481
|
+
buildMuiTheme, // (overrides, { mode? }) => Theme
|
|
482
|
+
isResolvedMuiTheme, // (value) => boolean — detect options vs Theme
|
|
483
|
+
} from 'robobyte-front-builder'
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Coordinating with AG Grid + brand identity
|
|
487
|
+
|
|
488
|
+
The MUI `palette.secondary.main` (`#FF1185`) and the AG Grid `accentColor` are intentionally the same string. Change one, change the other, or wait for the future unified `theme` prop that will derive both from a shared token bag.
|
|
489
|
+
|
|
490
|
+
> **Future plan.** A single unified `theme` prop will derive MUI, AG Grid, and chart themes from one set of design tokens. Until then, `muiTheme` and `agGridTheme` are the two sources to coordinate.
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
406
494
|
## `fetchReportDataByPageId`
|
|
407
495
|
|
|
408
496
|
Fetches report data by `pageId` without any AG Grid dependency. Always imported from the package path — the `NormalModuleReplacementPlugin` in `next.config.js` routes it to the package's canonical implementation.
|
package/package.json
CHANGED
package/src/lib/index.js
CHANGED
|
@@ -113,3 +113,17 @@ export { default as fetchReportDataByPageId } from '../services/reportData/fetch
|
|
|
113
113
|
// every internal grid uses to read the live theme.
|
|
114
114
|
export { DEFAULT_AG_THEME_PARAMS, buildAgGridTheme, DEFAULT_AG_THEME } from './agGridTheme'
|
|
115
115
|
export { AgGridThemeProvider, useAgGridTheme } from './agGridThemeContext'
|
|
116
|
+
|
|
117
|
+
// ── MUI theme ────────────────────────────────────────────────────────────────
|
|
118
|
+
// Default MUI theme options + a builder. Host apps override via the
|
|
119
|
+
// `muiTheme` prop on RoboByteFrontBuilderProvider; deep-merged with defaults.
|
|
120
|
+
// Tokens are coordinated with DEFAULT_AG_THEME_PARAMS so brand colors stay
|
|
121
|
+
// in sync between MUI surfaces and AG Grid surfaces — see lib/muiTheme.js.
|
|
122
|
+
export {
|
|
123
|
+
DEFAULT_MUI_THEME_OPTIONS,
|
|
124
|
+
DARK_MUI_THEME_OPTIONS,
|
|
125
|
+
DEFAULT_MUI_THEME,
|
|
126
|
+
DEFAULT_MUI_THEME_DARK,
|
|
127
|
+
buildMuiTheme,
|
|
128
|
+
isResolvedMuiTheme,
|
|
129
|
+
} from './muiTheme'
|
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized MUI theme configuration.
|
|
3
|
+
*
|
|
4
|
+
* Every `<RoboByteFrontBuilderProvider>` wraps its children in a MUI
|
|
5
|
+
* ThemeProvider built from these defaults. Host apps can override any token
|
|
6
|
+
* by passing the `muiTheme` prop:
|
|
7
|
+
*
|
|
8
|
+
* <RoboByteFrontBuilderProvider
|
|
9
|
+
* muiTheme={{
|
|
10
|
+
* palette: { primary: { main: '#6366f1' } },
|
|
11
|
+
* shape: { borderRadius: 12 },
|
|
12
|
+
* }}
|
|
13
|
+
* />
|
|
14
|
+
*
|
|
15
|
+
* The override object is deep-merged on top of DEFAULT_MUI_THEME_OPTIONS —
|
|
16
|
+
* keys you set win, everything else falls back to the package defaults.
|
|
17
|
+
*
|
|
18
|
+
* Default identity (in plain English):
|
|
19
|
+
* - Inter font family (with sensible system fallbacks).
|
|
20
|
+
* - 8px border-radius across the surface.
|
|
21
|
+
* - Soft layered shadows (Tailwind-style, not Material elevation).
|
|
22
|
+
* - Flat buttons (no ripple, no elevation) with cleaner hover states.
|
|
23
|
+
* - Outlined text fields with rounded corners, default size 'small'.
|
|
24
|
+
* - Tooltips: dark, arrowed, faster open delay.
|
|
25
|
+
* - Compact tabs / chips / icon buttons matching the buttons' radius.
|
|
26
|
+
* - Light & dark palette pair, both designed in true greys (not blue-tinted).
|
|
27
|
+
*
|
|
28
|
+
* Future: a single unified `theme` prop will derive MUI, AG Grid, and chart
|
|
29
|
+
* themes from a shared token bag. Until then, this file plus `agGridTheme.js`
|
|
30
|
+
* are the two sources to coordinate when changing brand colors.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { createTheme, alpha } from '@mui/material/styles'
|
|
34
|
+
import merge from 'lodash/merge'
|
|
35
|
+
|
|
36
|
+
// ── Brand tokens ─────────────────────────────────────────────────────────────
|
|
37
|
+
// Keep COLORS coordinated with DEFAULT_AG_THEME_PARAMS in agGridTheme.js so a
|
|
38
|
+
// brand-color change in one place doesn't desync the report grids.
|
|
39
|
+
|
|
40
|
+
const COLORS = {
|
|
41
|
+
primary: {
|
|
42
|
+
main: '#3b82f6', // blue-500
|
|
43
|
+
light: '#60a5fa',
|
|
44
|
+
dark: '#2563eb',
|
|
45
|
+
contrastText: '#ffffff',
|
|
46
|
+
},
|
|
47
|
+
secondary: {
|
|
48
|
+
main: '#FF1185', // matches DEFAULT_AG_THEME_PARAMS.accentColor
|
|
49
|
+
light: '#ff4ea0',
|
|
50
|
+
dark: '#d10b6e',
|
|
51
|
+
contrastText: '#ffffff',
|
|
52
|
+
},
|
|
53
|
+
success: {
|
|
54
|
+
main: '#10b981',
|
|
55
|
+
light: '#34d399',
|
|
56
|
+
dark: '#059669',
|
|
57
|
+
contrastText: '#ffffff',
|
|
58
|
+
},
|
|
59
|
+
warning: {
|
|
60
|
+
main: '#f59e0b',
|
|
61
|
+
light: '#fbbf24',
|
|
62
|
+
dark: '#d97706',
|
|
63
|
+
contrastText: '#ffffff',
|
|
64
|
+
},
|
|
65
|
+
error: {
|
|
66
|
+
main: '#ef4444',
|
|
67
|
+
light: '#f87171',
|
|
68
|
+
dark: '#dc2626',
|
|
69
|
+
contrastText: '#ffffff',
|
|
70
|
+
},
|
|
71
|
+
info: {
|
|
72
|
+
main: '#0ea5e9',
|
|
73
|
+
light: '#38bdf8',
|
|
74
|
+
dark: '#0284c7',
|
|
75
|
+
contrastText: '#ffffff',
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const LIGHT_PALETTE = {
|
|
80
|
+
mode: 'light',
|
|
81
|
+
...COLORS,
|
|
82
|
+
background: { default: '#fafafa', paper: '#ffffff' },
|
|
83
|
+
text: {
|
|
84
|
+
primary: '#0f172a',
|
|
85
|
+
secondary: '#475569',
|
|
86
|
+
disabled: '#94a3b8',
|
|
87
|
+
},
|
|
88
|
+
divider: 'rgba(15, 23, 42, 0.08)',
|
|
89
|
+
action: {
|
|
90
|
+
hover: 'rgba(15, 23, 42, 0.04)',
|
|
91
|
+
selected: 'rgba(15, 23, 42, 0.08)',
|
|
92
|
+
selectedOpacity: 0.08,
|
|
93
|
+
disabled: 'rgba(15, 23, 42, 0.30)',
|
|
94
|
+
disabledBackground: 'rgba(15, 23, 42, 0.06)',
|
|
95
|
+
focus: 'rgba(15, 23, 42, 0.10)',
|
|
96
|
+
},
|
|
97
|
+
// Internal helpers used by component overrides — kept off-spec on `palette`
|
|
98
|
+
// so they don't collide with MUI's color augmentation. Read them via
|
|
99
|
+
// theme.palette.rbb.X inside styleOverride callbacks.
|
|
100
|
+
rbb: {
|
|
101
|
+
inputBackground: '#ffffff',
|
|
102
|
+
inputBackgroundHover:'#fafafa',
|
|
103
|
+
focusRingColor: 'rgba(59, 130, 246, 0.20)', // primary @ 20% — soft halo
|
|
104
|
+
focusRingError: 'rgba(239, 68, 68, 0.20)',
|
|
105
|
+
surface1: '#f8fafc', // subtle surface tint
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const DARK_PALETTE = {
|
|
110
|
+
mode: 'dark',
|
|
111
|
+
...COLORS,
|
|
112
|
+
background: { default: '#0a0a0a', paper: '#171717' },
|
|
113
|
+
text: {
|
|
114
|
+
primary: '#fafafa',
|
|
115
|
+
secondary: '#a1a1aa',
|
|
116
|
+
disabled: '#52525b',
|
|
117
|
+
},
|
|
118
|
+
divider: 'rgba(250, 250, 250, 0.08)',
|
|
119
|
+
action: {
|
|
120
|
+
hover: 'rgba(250, 250, 250, 0.06)',
|
|
121
|
+
selected: 'rgba(250, 250, 250, 0.10)',
|
|
122
|
+
selectedOpacity: 0.10,
|
|
123
|
+
disabled: 'rgba(250, 250, 250, 0.30)',
|
|
124
|
+
disabledBackground: 'rgba(250, 250, 250, 0.06)',
|
|
125
|
+
focus: 'rgba(250, 250, 250, 0.12)',
|
|
126
|
+
},
|
|
127
|
+
rbb: {
|
|
128
|
+
inputBackground: '#0f0f0f',
|
|
129
|
+
inputBackgroundHover:'#171717',
|
|
130
|
+
focusRingColor: 'rgba(96, 165, 250, 0.25)', // primary-light @ 25%
|
|
131
|
+
focusRingError: 'rgba(248, 113, 113, 0.25)',
|
|
132
|
+
surface1: '#0f0f0f',
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Typography ───────────────────────────────────────────────────────────────
|
|
137
|
+
// The font stack falls back gracefully if the host hasn't loaded Inter.
|
|
138
|
+
// Hosts can add `import { Inter } from 'next/font/google'` for the real thing.
|
|
139
|
+
|
|
140
|
+
const FONT_FAMILY = [
|
|
141
|
+
'"Inter"',
|
|
142
|
+
'"SF Pro Display"',
|
|
143
|
+
'-apple-system',
|
|
144
|
+
'BlinkMacSystemFont',
|
|
145
|
+
'"Segoe UI"',
|
|
146
|
+
'Roboto',
|
|
147
|
+
'"Helvetica Neue"',
|
|
148
|
+
'Arial',
|
|
149
|
+
'sans-serif',
|
|
150
|
+
].join(',')
|
|
151
|
+
|
|
152
|
+
const TYPOGRAPHY = {
|
|
153
|
+
fontFamily: FONT_FAMILY,
|
|
154
|
+
fontSize: 14,
|
|
155
|
+
// Headings use slightly tighter tracking and bolder weight than MUI default.
|
|
156
|
+
h1: { fontWeight: 700, letterSpacing: '-0.02em', lineHeight: 1.2 },
|
|
157
|
+
h2: { fontWeight: 700, letterSpacing: '-0.02em', lineHeight: 1.25 },
|
|
158
|
+
h3: { fontWeight: 600, letterSpacing: '-0.015em', lineHeight: 1.3 },
|
|
159
|
+
h4: { fontWeight: 600, letterSpacing: '-0.01em', lineHeight: 1.35 },
|
|
160
|
+
h5: { fontWeight: 600, lineHeight: 1.4 },
|
|
161
|
+
h6: { fontWeight: 600, lineHeight: 1.4 },
|
|
162
|
+
subtitle1: { fontWeight: 500 },
|
|
163
|
+
subtitle2: { fontWeight: 500 },
|
|
164
|
+
body1: { lineHeight: 1.5 },
|
|
165
|
+
body2: { lineHeight: 1.5 },
|
|
166
|
+
button: { fontWeight: 500, textTransform: 'none', letterSpacing: 0 },
|
|
167
|
+
caption: { letterSpacing: '0.01em' },
|
|
168
|
+
overline: { fontWeight: 600, letterSpacing: '0.08em' },
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Shadows ──────────────────────────────────────────────────────────────────
|
|
172
|
+
// 25-step elevation ladder. Softer and more spread than MUI defaults so
|
|
173
|
+
// surfaces float subtly without that "drawn shadow" look. Steps 0-6 are the
|
|
174
|
+
// commonly-used ones; 7-24 are reserved for very-elevated surfaces (drag
|
|
175
|
+
// previews, drawers, etc.) — kept distinct but capped to avoid heaviness.
|
|
176
|
+
|
|
177
|
+
const SOFT_SHADOWS = [
|
|
178
|
+
'none',
|
|
179
|
+
'0 1px 2px 0 rgba(15, 23, 42, 0.04)',
|
|
180
|
+
'0 1px 3px 0 rgba(15, 23, 42, 0.06), 0 1px 2px -1px rgba(15, 23, 42, 0.06)',
|
|
181
|
+
'0 4px 6px -1px rgba(15, 23, 42, 0.08), 0 2px 4px -2px rgba(15, 23, 42, 0.06)',
|
|
182
|
+
'0 10px 15px -3px rgba(15, 23, 42, 0.08), 0 4px 6px -4px rgba(15, 23, 42, 0.06)',
|
|
183
|
+
'0 12px 20px -4px rgba(15, 23, 42, 0.10), 0 6px 8px -4px rgba(15, 23, 42, 0.06)',
|
|
184
|
+
'0 20px 25px -5px rgba(15, 23, 42, 0.10), 0 8px 10px -6px rgba(15, 23, 42, 0.08)',
|
|
185
|
+
'0 24px 40px -8px rgba(15, 23, 42, 0.12)',
|
|
186
|
+
'0 25px 50px -12px rgba(15, 23, 42, 0.15)',
|
|
187
|
+
]
|
|
188
|
+
const HIGH_SHADOW = '0 25px 50px -12px rgba(15, 23, 42, 0.18)'
|
|
189
|
+
const SHADOWS = [
|
|
190
|
+
...SOFT_SHADOWS,
|
|
191
|
+
...Array(25 - SOFT_SHADOWS.length).fill(HIGH_SHADOW),
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
// ── Component overrides ──────────────────────────────────────────────────────
|
|
195
|
+
// Each entry either changes defaultProps (e.g. disable ripple) or layers
|
|
196
|
+
// styleOverrides on top of MUI defaults. Keep these minimal and surgical —
|
|
197
|
+
// the goal is "make MUI not look like stock MUI", not "rewrite MUI."
|
|
198
|
+
|
|
199
|
+
const TRANSITION = 'all 150ms cubic-bezier(0.4, 0, 0.2, 1)'
|
|
200
|
+
|
|
201
|
+
const COMPONENT_OVERRIDES = {
|
|
202
|
+
// ── Buttons ──────────────────────────────────────────────────────────────
|
|
203
|
+
// Flat by default, no ripple. Modern hover + active + focus states. Focus
|
|
204
|
+
// uses :focus-visible so mouse clicks don't show the keyboard outline.
|
|
205
|
+
MuiButton: {
|
|
206
|
+
defaultProps: {
|
|
207
|
+
disableElevation: true,
|
|
208
|
+
disableRipple: true,
|
|
209
|
+
},
|
|
210
|
+
styleOverrides: {
|
|
211
|
+
root: ({ theme }) => ({
|
|
212
|
+
borderRadius: theme.shape.borderRadius,
|
|
213
|
+
fontWeight: 500,
|
|
214
|
+
textTransform: 'none',
|
|
215
|
+
letterSpacing: 0,
|
|
216
|
+
boxShadow: 'none',
|
|
217
|
+
transition: TRANSITION,
|
|
218
|
+
// Reset MUI's default ripple-based active visual; we use a subtle
|
|
219
|
+
// scale-down instead — the same micro-interaction Linear/Vercel use.
|
|
220
|
+
'&:active': { transform: 'scale(0.98)', boxShadow: 'none' },
|
|
221
|
+
'&:hover': { boxShadow: 'none' },
|
|
222
|
+
// Keyboard-only focus ring: 2px offset, theme-aware color.
|
|
223
|
+
'&:focus-visible': {
|
|
224
|
+
outline: `2px solid ${theme.palette.primary.main}`,
|
|
225
|
+
outlineOffset: 2,
|
|
226
|
+
},
|
|
227
|
+
// Cleaner disabled — 50% opacity, not greyed-out.
|
|
228
|
+
'&.Mui-disabled': {
|
|
229
|
+
opacity: 0.5,
|
|
230
|
+
transform: 'none',
|
|
231
|
+
boxShadow: 'none',
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
sizeSmall: { padding: '4px 12px', minHeight: 32, fontSize: 13 },
|
|
235
|
+
sizeMedium: { padding: '6px 16px', minHeight: 36 },
|
|
236
|
+
sizeLarge: { padding: '8px 22px', minHeight: 42 },
|
|
237
|
+
// Contained: solid background, slight darken on hover (no shadow noise).
|
|
238
|
+
containedPrimary: ({ theme }) => ({
|
|
239
|
+
'&:hover': { backgroundColor: theme.palette.primary.dark },
|
|
240
|
+
}),
|
|
241
|
+
containedSecondary: ({ theme }) => ({
|
|
242
|
+
'&:hover': { backgroundColor: theme.palette.secondary.dark },
|
|
243
|
+
}),
|
|
244
|
+
containedSuccess: ({ theme }) => ({
|
|
245
|
+
'&:hover': { backgroundColor: theme.palette.success.dark },
|
|
246
|
+
}),
|
|
247
|
+
containedError: ({ theme }) => ({
|
|
248
|
+
'&:hover': { backgroundColor: theme.palette.error.dark },
|
|
249
|
+
}),
|
|
250
|
+
containedWarning: ({ theme }) => ({
|
|
251
|
+
'&:hover': { backgroundColor: theme.palette.warning.dark },
|
|
252
|
+
}),
|
|
253
|
+
containedInfo: ({ theme }) => ({
|
|
254
|
+
'&:hover': { backgroundColor: theme.palette.info.dark },
|
|
255
|
+
}),
|
|
256
|
+
// Outlined: hover deepens the border + adds a soft bg tint.
|
|
257
|
+
outlined: ({ theme }) => ({
|
|
258
|
+
borderColor: theme.palette.divider,
|
|
259
|
+
'&:hover': {
|
|
260
|
+
backgroundColor: theme.palette.action.hover,
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
outlinedPrimary: ({ theme }) => ({
|
|
264
|
+
'&:hover': {
|
|
265
|
+
borderColor: theme.palette.primary.main,
|
|
266
|
+
backgroundColor: alpha(theme.palette.primary.main, 0.05),
|
|
267
|
+
},
|
|
268
|
+
}),
|
|
269
|
+
outlinedSecondary: ({ theme }) => ({
|
|
270
|
+
'&:hover': {
|
|
271
|
+
borderColor: theme.palette.secondary.main,
|
|
272
|
+
backgroundColor: alpha(theme.palette.secondary.main, 0.05),
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
outlinedError: ({ theme }) => ({
|
|
276
|
+
'&:hover': {
|
|
277
|
+
borderColor: theme.palette.error.main,
|
|
278
|
+
backgroundColor: alpha(theme.palette.error.main, 0.05),
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
// Text: subtle bg tint on hover, no underline.
|
|
282
|
+
text: ({ theme }) => ({
|
|
283
|
+
'&:hover': { backgroundColor: theme.palette.action.hover },
|
|
284
|
+
}),
|
|
285
|
+
textPrimary: ({ theme }) => ({
|
|
286
|
+
'&:hover': { backgroundColor: alpha(theme.palette.primary.main, 0.05) },
|
|
287
|
+
}),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
// ── Button group ─────────────────────────────────────────────────────────
|
|
292
|
+
MuiButtonGroup: {
|
|
293
|
+
defaultProps: { disableElevation: true, disableRipple: true },
|
|
294
|
+
styleOverrides: {
|
|
295
|
+
root: ({ theme }) => ({ boxShadow: 'none', borderRadius: theme.shape.borderRadius }),
|
|
296
|
+
grouped: { '&:not(:last-of-type)': { borderRightColor: 'inherit' } },
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
// ── Toggle buttons ───────────────────────────────────────────────────────
|
|
301
|
+
MuiToggleButton: {
|
|
302
|
+
defaultProps: { disableRipple: true },
|
|
303
|
+
styleOverrides: {
|
|
304
|
+
root: ({ theme }) => ({
|
|
305
|
+
borderRadius: theme.shape.borderRadius,
|
|
306
|
+
fontWeight: 500,
|
|
307
|
+
textTransform: 'none',
|
|
308
|
+
transition: TRANSITION,
|
|
309
|
+
'&.Mui-selected': {
|
|
310
|
+
backgroundColor: alpha(theme.palette.primary.main, 0.08),
|
|
311
|
+
color: theme.palette.primary.main,
|
|
312
|
+
'&:hover': { backgroundColor: alpha(theme.palette.primary.main, 0.12) },
|
|
313
|
+
},
|
|
314
|
+
'&:focus-visible': {
|
|
315
|
+
outline: `2px solid ${theme.palette.primary.main}`,
|
|
316
|
+
outlineOffset: 2,
|
|
317
|
+
},
|
|
318
|
+
}),
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
// ── Icon buttons ─────────────────────────────────────────────────────────
|
|
323
|
+
// Matches Button's interaction language: same transition, focus ring,
|
|
324
|
+
// hover bg, scale-on-active.
|
|
325
|
+
MuiIconButton: {
|
|
326
|
+
defaultProps: { disableRipple: true },
|
|
327
|
+
styleOverrides: {
|
|
328
|
+
root: ({ theme }) => ({
|
|
329
|
+
borderRadius: Math.max(theme.shape.borderRadius - 2, 6),
|
|
330
|
+
transition: TRANSITION,
|
|
331
|
+
'&:hover': { backgroundColor: theme.palette.action.hover },
|
|
332
|
+
'&:active': { transform: 'scale(0.94)' },
|
|
333
|
+
'&:focus-visible': {
|
|
334
|
+
outline: `2px solid ${theme.palette.primary.main}`,
|
|
335
|
+
outlineOffset: 2,
|
|
336
|
+
},
|
|
337
|
+
'&.Mui-disabled': { opacity: 0.5 },
|
|
338
|
+
}),
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
// ── Floating action button ───────────────────────────────────────────────
|
|
343
|
+
MuiFab: {
|
|
344
|
+
defaultProps: { disableRipple: true },
|
|
345
|
+
styleOverrides: {
|
|
346
|
+
root: ({ theme }) => ({
|
|
347
|
+
boxShadow: theme.shadows[4],
|
|
348
|
+
transition: TRANSITION,
|
|
349
|
+
'&:hover': { boxShadow: theme.shadows[6] },
|
|
350
|
+
'&:active': { transform: 'scale(0.95)' },
|
|
351
|
+
}),
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
// ── Text fields & inputs ─────────────────────────────────────────────────
|
|
356
|
+
// Small + outlined by default. Distinctive but subtle modern focus:
|
|
357
|
+
// - resting: 1px divider border, subtle surface tint
|
|
358
|
+
// - hover: border darkens to text.disabled
|
|
359
|
+
// - focus: 1px primary border + soft halo shadow (not double-thick border)
|
|
360
|
+
// - error: 1px error border + red halo on focus
|
|
361
|
+
MuiTextField: {
|
|
362
|
+
defaultProps: { size: 'small', variant: 'outlined' },
|
|
363
|
+
},
|
|
364
|
+
MuiOutlinedInput: {
|
|
365
|
+
styleOverrides: {
|
|
366
|
+
root: ({ theme }) => ({
|
|
367
|
+
borderRadius: theme.shape.borderRadius,
|
|
368
|
+
backgroundColor: theme.palette.rbb?.inputBackground ?? theme.palette.background.paper,
|
|
369
|
+
transition: TRANSITION,
|
|
370
|
+
|
|
371
|
+
// Resting border
|
|
372
|
+
'& .MuiOutlinedInput-notchedOutline': {
|
|
373
|
+
borderColor: theme.palette.divider,
|
|
374
|
+
transition: TRANSITION,
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
// Hover: stronger border, subtle bg shift
|
|
378
|
+
'&:hover': {
|
|
379
|
+
backgroundColor: theme.palette.rbb?.inputBackgroundHover ?? theme.palette.background.paper,
|
|
380
|
+
},
|
|
381
|
+
'&:hover .MuiOutlinedInput-notchedOutline': {
|
|
382
|
+
borderColor: theme.palette.text.disabled,
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
// Focus: thin primary border + soft halo shadow (Vercel/Linear style)
|
|
386
|
+
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
387
|
+
borderWidth: 1,
|
|
388
|
+
borderColor: theme.palette.primary.main,
|
|
389
|
+
},
|
|
390
|
+
'&.Mui-focused': {
|
|
391
|
+
boxShadow: `0 0 0 3px ${theme.palette.rbb?.focusRingColor ?? 'rgba(59, 130, 246, 0.20)'}`,
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
// Error state — red border, red halo on focus
|
|
395
|
+
'&.Mui-error .MuiOutlinedInput-notchedOutline': {
|
|
396
|
+
borderColor: theme.palette.error.main,
|
|
397
|
+
},
|
|
398
|
+
'&.Mui-error.Mui-focused': {
|
|
399
|
+
boxShadow: `0 0 0 3px ${theme.palette.rbb?.focusRingError ?? 'rgba(239, 68, 68, 0.20)'}`,
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
// Disabled — soften, don't grey out
|
|
403
|
+
'&.Mui-disabled': {
|
|
404
|
+
backgroundColor: theme.palette.action.disabledBackground,
|
|
405
|
+
opacity: 0.7,
|
|
406
|
+
},
|
|
407
|
+
}),
|
|
408
|
+
input: ({ theme }) => ({
|
|
409
|
+
'&::placeholder': {
|
|
410
|
+
color: theme.palette.text.disabled,
|
|
411
|
+
opacity: 1,
|
|
412
|
+
},
|
|
413
|
+
}),
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
// ── Filled / standard input variants ─────────────────────────────────────
|
|
418
|
+
// Same focus halo treatment for consistency, in case consumers pick these.
|
|
419
|
+
MuiFilledInput: {
|
|
420
|
+
styleOverrides: {
|
|
421
|
+
root: ({ theme }) => ({
|
|
422
|
+
borderRadius: theme.shape.borderRadius,
|
|
423
|
+
backgroundColor: theme.palette.rbb?.surface1 ?? theme.palette.action.hover,
|
|
424
|
+
transition: TRANSITION,
|
|
425
|
+
'&:before, &:after': { display: 'none' },
|
|
426
|
+
'&.Mui-focused': {
|
|
427
|
+
boxShadow: `0 0 0 3px ${theme.palette.rbb?.focusRingColor ?? 'rgba(59, 130, 246, 0.20)'}`,
|
|
428
|
+
backgroundColor: theme.palette.background.paper,
|
|
429
|
+
},
|
|
430
|
+
}),
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
// ── Input labels: cleaner sizing ─────────────────────────────────────────
|
|
435
|
+
MuiInputLabel: {
|
|
436
|
+
styleOverrides: {
|
|
437
|
+
root: { fontSize: 14 },
|
|
438
|
+
shrink: { fontWeight: 500 },
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
// ── Form helper text ─────────────────────────────────────────────────────
|
|
443
|
+
// No layout shift between empty and filled — reserve the line.
|
|
444
|
+
MuiFormHelperText: {
|
|
445
|
+
styleOverrides: {
|
|
446
|
+
root: { marginLeft: 2, marginTop: 4, fontSize: 12, lineHeight: 1.4 },
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
// ── Autocomplete: cleaner option list ────────────────────────────────────
|
|
451
|
+
MuiAutocomplete: {
|
|
452
|
+
styleOverrides: {
|
|
453
|
+
paper: ({ theme }) => ({
|
|
454
|
+
borderRadius: theme.shape.borderRadius,
|
|
455
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
456
|
+
boxShadow: theme.shadows[4],
|
|
457
|
+
marginTop: 4,
|
|
458
|
+
}),
|
|
459
|
+
option: ({ theme }) => ({
|
|
460
|
+
borderRadius: Math.max(theme.shape.borderRadius - 4, 4),
|
|
461
|
+
margin: '1px 4px',
|
|
462
|
+
'&[aria-selected="true"]': {
|
|
463
|
+
backgroundColor: alpha(theme.palette.primary.main, 0.08),
|
|
464
|
+
color: theme.palette.primary.main,
|
|
465
|
+
},
|
|
466
|
+
}),
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
// ── Select: align dropdown with outlined input ───────────────────────────
|
|
471
|
+
MuiSelect: {
|
|
472
|
+
styleOverrides: {
|
|
473
|
+
icon: ({ theme }) => ({ color: theme.palette.text.secondary }),
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
// Chips: smaller radius than the global shape — chips read better as pills.
|
|
478
|
+
MuiChip: {
|
|
479
|
+
styleOverrides: {
|
|
480
|
+
root: { borderRadius: 6, fontWeight: 500 },
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
// Tooltips: dark, smaller font, faster open. Arrowed by default.
|
|
485
|
+
MuiTooltip: {
|
|
486
|
+
defaultProps: {
|
|
487
|
+
arrow: true,
|
|
488
|
+
enterDelay: 200,
|
|
489
|
+
enterNextDelay: 150,
|
|
490
|
+
placement: 'top',
|
|
491
|
+
},
|
|
492
|
+
styleOverrides: {
|
|
493
|
+
tooltip: { fontSize: 12, padding: '4px 8px', fontWeight: 500 },
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
// Cards: flat by default with a hairline border instead of an elevation.
|
|
498
|
+
MuiCard: {
|
|
499
|
+
defaultProps: { elevation: 0 },
|
|
500
|
+
styleOverrides: {
|
|
501
|
+
root: ({ theme }) => ({
|
|
502
|
+
border: '1px solid',
|
|
503
|
+
borderColor: theme.palette.divider,
|
|
504
|
+
borderRadius: theme.shape.borderRadius * 1.25,
|
|
505
|
+
}),
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
// Paper: don't auto-apply elevation — components that want it set it explicitly.
|
|
510
|
+
MuiPaper: {
|
|
511
|
+
defaultProps: { elevation: 0 },
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
// Dialogs: slightly larger radius (more "modal-y"), softer shadow.
|
|
515
|
+
MuiDialog: {
|
|
516
|
+
styleOverrides: {
|
|
517
|
+
paper: ({ theme }) => ({
|
|
518
|
+
borderRadius: theme.shape.borderRadius * 1.5,
|
|
519
|
+
boxShadow: theme.shadows[8],
|
|
520
|
+
}),
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
// AppBar / Toolbar: flat with hairline divider, theme-paper background.
|
|
525
|
+
MuiAppBar: {
|
|
526
|
+
defaultProps: { elevation: 0, color: 'inherit' },
|
|
527
|
+
styleOverrides: {
|
|
528
|
+
root: ({ theme }) => ({
|
|
529
|
+
backgroundColor: theme.palette.background.paper,
|
|
530
|
+
color: theme.palette.text.primary,
|
|
531
|
+
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
532
|
+
}),
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
// Inputs: no ripple on selection controls (desktop-first).
|
|
537
|
+
MuiCheckbox: { defaultProps: { disableRipple: true } },
|
|
538
|
+
MuiRadio: { defaultProps: { disableRipple: true } },
|
|
539
|
+
MuiSwitch: { defaultProps: { disableRipple: true } },
|
|
540
|
+
|
|
541
|
+
// Tabs: cleaner typography, no uppercase.
|
|
542
|
+
MuiTab: {
|
|
543
|
+
styleOverrides: {
|
|
544
|
+
root: { textTransform: 'none', fontWeight: 500, minHeight: 40 },
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
MuiTabs: {
|
|
548
|
+
styleOverrides: {
|
|
549
|
+
root: { minHeight: 40 },
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
// Menus / popovers: subtle border, soft shadow.
|
|
554
|
+
MuiMenu: {
|
|
555
|
+
styleOverrides: {
|
|
556
|
+
paper: ({ theme }) => ({
|
|
557
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
558
|
+
borderRadius: theme.shape.borderRadius,
|
|
559
|
+
boxShadow: theme.shadows[4],
|
|
560
|
+
}),
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
MuiPopover: {
|
|
564
|
+
styleOverrides: {
|
|
565
|
+
paper: ({ theme }) => ({
|
|
566
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
567
|
+
borderRadius: theme.shape.borderRadius,
|
|
568
|
+
}),
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
// Lists & list items: a touch tighter than MUI default.
|
|
573
|
+
MuiListItemButton: {
|
|
574
|
+
styleOverrides: {
|
|
575
|
+
root: ({ theme }) => ({
|
|
576
|
+
borderRadius: Math.max(theme.shape.borderRadius - 4, 4),
|
|
577
|
+
}),
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
// Accordions: flat, hairline separators.
|
|
582
|
+
MuiAccordion: {
|
|
583
|
+
defaultProps: { elevation: 0, disableGutters: true },
|
|
584
|
+
styleOverrides: {
|
|
585
|
+
root: ({ theme }) => ({
|
|
586
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
587
|
+
borderRadius: theme.shape.borderRadius,
|
|
588
|
+
'&:before': { display: 'none' },
|
|
589
|
+
'&.Mui-expanded': { margin: 0 },
|
|
590
|
+
}),
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ── Shape ────────────────────────────────────────────────────────────────────
|
|
596
|
+
const SHAPE = { borderRadius: 8 }
|
|
597
|
+
|
|
598
|
+
// ── Public exports ───────────────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* MUI theme options object — light mode, package defaults.
|
|
602
|
+
* Pass these (or a deep-merged override) to `createTheme`.
|
|
603
|
+
*/
|
|
604
|
+
export const DEFAULT_MUI_THEME_OPTIONS = Object.freeze({
|
|
605
|
+
palette: LIGHT_PALETTE,
|
|
606
|
+
typography: TYPOGRAPHY,
|
|
607
|
+
shape: SHAPE,
|
|
608
|
+
shadows: SHADOWS,
|
|
609
|
+
components: COMPONENT_OVERRIDES,
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* MUI theme options object — dark mode counterpart.
|
|
614
|
+
* Use by passing `{ palette: { mode: 'dark' } }` as the `muiTheme` override
|
|
615
|
+
* on the provider, or by calling `buildMuiTheme(null, { mode: 'dark' })`.
|
|
616
|
+
*/
|
|
617
|
+
export const DARK_MUI_THEME_OPTIONS = Object.freeze({
|
|
618
|
+
palette: DARK_PALETTE,
|
|
619
|
+
typography: TYPOGRAPHY,
|
|
620
|
+
shape: SHAPE,
|
|
621
|
+
shadows: SHADOWS,
|
|
622
|
+
components: COMPONENT_OVERRIDES,
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Build a fully-resolved MUI Theme from optional overrides.
|
|
627
|
+
*
|
|
628
|
+
* @param {Object} [overrides] - theme options merged on top of defaults.
|
|
629
|
+
* @param {Object} [opts]
|
|
630
|
+
* @param {'light'|'dark'} [opts.mode='light']
|
|
631
|
+
* @returns {import('@mui/material/styles').Theme}
|
|
632
|
+
*/
|
|
633
|
+
export function buildMuiTheme(overrides, { mode = 'light' } = {}) {
|
|
634
|
+
const base = mode === 'dark' ? DARK_MUI_THEME_OPTIONS : DEFAULT_MUI_THEME_OPTIONS
|
|
635
|
+
// lodash.merge does deep merge — safer than {...base, ...overrides} which
|
|
636
|
+
// would clobber nested keys like palette.primary.
|
|
637
|
+
const merged = merge({}, base, overrides ?? {})
|
|
638
|
+
return createTheme(merged)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Pre-built light theme — convenient when no override is in play. */
|
|
642
|
+
export const DEFAULT_MUI_THEME = buildMuiTheme()
|
|
643
|
+
|
|
644
|
+
/** Pre-built dark theme — same overrides, dark palette. */
|
|
645
|
+
export const DEFAULT_MUI_THEME_DARK = buildMuiTheme(null, { mode: 'dark' })
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Convenience: detect whether the value passed to a provider's `muiTheme`
|
|
649
|
+
* prop is an already-built Theme (vs. a raw options object). A Theme has
|
|
650
|
+
* `spacing` as a function and `palette.augmentColor` etc.; an options
|
|
651
|
+
* object does not.
|
|
652
|
+
*/
|
|
653
|
+
export function isResolvedMuiTheme(value) {
|
|
654
|
+
return Boolean(value && typeof value.spacing === 'function' && value.palette?.augmentColor)
|
|
655
|
+
}
|
|
@@ -46,10 +46,12 @@
|
|
|
46
46
|
import { useEffect, useMemo } from 'react'
|
|
47
47
|
import { useRouter } from 'next/router'
|
|
48
48
|
import { Toaster } from 'react-hot-toast'
|
|
49
|
+
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles'
|
|
49
50
|
import { ModuleRegistry } from 'ag-grid-community'
|
|
50
51
|
import { AllEnterpriseModule, LicenseManager } from 'ag-grid-enterprise'
|
|
51
52
|
import { NavigationExtensionProvider } from '../navigation/NavigationExtensionContext'
|
|
52
53
|
import { AgGridThemeProvider } from '../agGridThemeContext'
|
|
54
|
+
import { buildMuiTheme, isResolvedMuiTheme, DEFAULT_MUI_THEME } from '../muiTheme'
|
|
53
55
|
import { configureRoboByte } from '../../services/config'
|
|
54
56
|
import { AuthContext } from '../../context/AuthContext'
|
|
55
57
|
import { setRouter } from '../../services/routerRef'
|
|
@@ -107,6 +109,26 @@ const RoboByteFrontBuilderProvider = ({
|
|
|
107
109
|
* pass a plain params object.
|
|
108
110
|
*/
|
|
109
111
|
agGridTheme = null,
|
|
112
|
+
/**
|
|
113
|
+
* Optional MUI theme overrides applied to every component the package
|
|
114
|
+
* renders. Three accepted shapes:
|
|
115
|
+
*
|
|
116
|
+
* 1. nothing (default) — uses DEFAULT_MUI_THEME from lib/muiTheme.js
|
|
117
|
+
* 2. theme options object — deep-merged on top of the defaults, e.g.
|
|
118
|
+
* muiTheme={{ palette: { primary: { main: '#6366f1' } }, shape: { borderRadius: 12 } }}
|
|
119
|
+
* 3. a pre-built MUI Theme — used as-is (escape hatch for full control)
|
|
120
|
+
*
|
|
121
|
+
* The package mounts a `<MuiThemeProvider>` wrapping children with the
|
|
122
|
+
* resolved theme. Host apps with their own outer `<ThemeProvider>` should
|
|
123
|
+
* pass their theme options here so the package's internal components share
|
|
124
|
+
* the host's visual identity.
|
|
125
|
+
*
|
|
126
|
+
* Pass `disableMuiTheme={true}` to skip the inner ThemeProvider entirely —
|
|
127
|
+
* use this when the host has already wrapped the app in its own
|
|
128
|
+
* ThemeProvider and wants it to win without merging.
|
|
129
|
+
*/
|
|
130
|
+
muiTheme = null,
|
|
131
|
+
disableMuiTheme = false,
|
|
110
132
|
toasterProps = {},
|
|
111
133
|
}) => {
|
|
112
134
|
// Apply URL + endpoint config synchronously before any child renders.
|
|
@@ -130,7 +152,29 @@ const RoboByteFrontBuilderProvider = ({
|
|
|
130
152
|
[user, accessToken]
|
|
131
153
|
)
|
|
132
154
|
|
|
133
|
-
|
|
155
|
+
// ── Resolve the MUI theme ────────────────────────────────────────────────
|
|
156
|
+
// Three cases:
|
|
157
|
+
// - disableMuiTheme: no wrapping ThemeProvider; host provides its own
|
|
158
|
+
// - muiTheme is a pre-built Theme: use as-is
|
|
159
|
+
// - muiTheme is null OR an options object: deep-merge with defaults
|
|
160
|
+
// JSON.stringify keys the memo so a fresh `{ palette: { ... } }` literal
|
|
161
|
+
// each render doesn't rebuild the theme. Resolved Themes (case 2) are
|
|
162
|
+
// expected to be stable from the caller's side.
|
|
163
|
+
const resolvedMuiTheme = useMemo(() => {
|
|
164
|
+
if (disableMuiTheme) return null
|
|
165
|
+
if (isResolvedMuiTheme(muiTheme)) return muiTheme
|
|
166
|
+
if (!muiTheme) return DEFAULT_MUI_THEME
|
|
167
|
+
return buildMuiTheme(muiTheme)
|
|
168
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
169
|
+
}, [disableMuiTheme, isResolvedMuiTheme(muiTheme) ? muiTheme : JSON.stringify(muiTheme ?? null)])
|
|
170
|
+
|
|
171
|
+
// Wrapper helper — applies MuiThemeProvider conditionally based on flag.
|
|
172
|
+
const withMuiTheme = (node) =>
|
|
173
|
+
disableMuiTheme
|
|
174
|
+
? node
|
|
175
|
+
: <MuiThemeProvider theme={resolvedMuiTheme}>{node}</MuiThemeProvider>
|
|
176
|
+
|
|
177
|
+
return withMuiTheme(
|
|
134
178
|
<AuthContext.Provider value={authValue}>
|
|
135
179
|
<NavigationExtensionProvider items={navExtensions}>
|
|
136
180
|
<AgGridThemeProvider params={agGridTheme}>
|
package/src/pages/_app.js
CHANGED
|
@@ -31,6 +31,7 @@ export default function App({ Component, pageProps }) {
|
|
|
31
31
|
user={DEV_USER}
|
|
32
32
|
accessToken={DEV_ACCESS_TOKEN}
|
|
33
33
|
agGridLicenseKey={process.env.NEXT_PUBLIC_AG_GRID_LICENSE_KEY}
|
|
34
|
+
agGridTheme={{ accentColor: '#3b82f6', headerHeight: 32 }}
|
|
34
35
|
>
|
|
35
36
|
{getLayout(<Component {...pageProps} />)}
|
|
36
37
|
</RoboByteFrontBuilderProvider>
|
|
@@ -11,30 +11,37 @@ import { Endpoints, Services } from 'services/Endpoints'
|
|
|
11
11
|
import { PRINT_COMPONENT_SECTIONS } from 'views/builder/sidebar/tabs/Components/printComponentCatalog'
|
|
12
12
|
import ComponentsTab from 'views/builder/sidebar/tabs/Components/ComponentsTab'
|
|
13
13
|
import TreeTab from 'views/builder/sidebar/tabs/TreeTab'
|
|
14
|
-
import {
|
|
14
|
+
import { buildMuiTheme } from 'src/lib/muiTheme'
|
|
15
15
|
|
|
16
|
-
// ── Builder themes
|
|
16
|
+
// ── Builder themes ────────────────────────────────────────────────────────────
|
|
17
|
+
// Deep-merge with the package's DEFAULT_MUI_THEME_OPTIONS so every component
|
|
18
|
+
// override (button polish, input focus halo, transitions, Inter typography,
|
|
19
|
+
// soft shadows) is inherited. The builder UI only overrides palette.
|
|
17
20
|
const _STORAGE_KEY = 'rbb:builderThemeMode'
|
|
18
21
|
const BUILDER_THEMES = {
|
|
19
|
-
dark:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
dark: buildMuiTheme(
|
|
23
|
+
{
|
|
24
|
+
palette: {
|
|
25
|
+
primary: { main: '#9c27b0' },
|
|
26
|
+
secondary: { main: '#f5a623' },
|
|
27
|
+
background: { default: '#141618', paper: '#1e2227' },
|
|
28
|
+
divider: '#3a3f45',
|
|
29
|
+
text: { primary: '#e8eaed', secondary: '#9aa0aa' },
|
|
30
|
+
},
|
|
27
31
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
{ mode: 'dark' }
|
|
33
|
+
),
|
|
34
|
+
light: buildMuiTheme(
|
|
35
|
+
{
|
|
36
|
+
palette: {
|
|
37
|
+
primary: { main: '#9c27b0' },
|
|
38
|
+
secondary: { main: '#f5a623' },
|
|
39
|
+
background: { default: '#f0f2f5', paper: '#ffffff' },
|
|
40
|
+
divider: '#dde1e7',
|
|
41
|
+
},
|
|
36
42
|
},
|
|
37
|
-
|
|
43
|
+
{ mode: 'light' }
|
|
44
|
+
),
|
|
38
45
|
}
|
|
39
46
|
function getBuilderThemeMode() {
|
|
40
47
|
try { return localStorage.getItem(_STORAGE_KEY) === 'light' ? 'light' : 'dark' } catch { return 'dark' }
|
|
@@ -9,30 +9,40 @@ import { useRouter } from 'next/router'
|
|
|
9
9
|
import { useEffect, useState } from 'react'
|
|
10
10
|
import { Endpoints, Services } from 'services/Endpoints'
|
|
11
11
|
import { ChevronLeft, ChevronRight } from '@mui/icons-material'
|
|
12
|
-
import {
|
|
12
|
+
import { buildMuiTheme } from 'src/lib/muiTheme'
|
|
13
13
|
|
|
14
|
-
// ── Builder themes
|
|
14
|
+
// ── Builder themes ────────────────────────────────────────────────────────────
|
|
15
|
+
// These deep-merge ON TOP of the package's DEFAULT_MUI_THEME_OPTIONS so every
|
|
16
|
+
// component override (button polish, input focus halo, transitions, soft
|
|
17
|
+
// shadows, Inter typography, …) is inherited. The builder UI just contributes
|
|
18
|
+
// its own palette (purple primary, gold secondary, builder-specific surface
|
|
19
|
+
// colors). Without this merge the builder pages used a bare-palette theme
|
|
20
|
+
// and looked completely different from the rest of the package.
|
|
15
21
|
const _STORAGE_KEY = 'rbb:builderThemeMode'
|
|
16
22
|
const BUILDER_THEMES = {
|
|
17
|
-
dark:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
dark: buildMuiTheme(
|
|
24
|
+
{
|
|
25
|
+
palette: {
|
|
26
|
+
primary: { main: '#9c27b0' },
|
|
27
|
+
secondary: { main: '#f5a623' },
|
|
28
|
+
background: { default: '#141618', paper: '#1e2227' },
|
|
29
|
+
divider: '#3a3f45',
|
|
30
|
+
text: { primary: '#e8eaed', secondary: '#9aa0aa' },
|
|
31
|
+
},
|
|
25
32
|
},
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
{ mode: 'dark' }
|
|
34
|
+
),
|
|
35
|
+
light: buildMuiTheme(
|
|
36
|
+
{
|
|
37
|
+
palette: {
|
|
38
|
+
primary: { main: '#9c27b0' },
|
|
39
|
+
secondary: { main: '#f5a623' },
|
|
40
|
+
background: { default: '#f0f2f5', paper: '#ffffff' },
|
|
41
|
+
divider: '#dde1e7',
|
|
42
|
+
},
|
|
34
43
|
},
|
|
35
|
-
|
|
44
|
+
{ mode: 'light' }
|
|
45
|
+
),
|
|
36
46
|
}
|
|
37
47
|
function getBuilderThemeMode() {
|
|
38
48
|
try { return localStorage.getItem(_STORAGE_KEY) === 'light' ? 'light' : 'dark' } catch { return 'dark' }
|