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 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. [fetchReportDataByPageId](#fetchreportdatabypageid)
43
- 12. [ReportViewer as a component](#reportviewer-as-a-component)
44
- 13. [Data Grid component](#data-grid-component)
45
- 14. [Dialog component](#dialog-component)
46
- 15. [Popover component](#popover-component)
47
- 16. [Excel Upload component](#excel-upload-component)
48
- 17. [Wizard component](#wizard-component)
49
- 18. [Repeater component](#repeater-component)
50
- 19. [Menu component](#menu-component)
51
- 20. [View Renderer component](#view-renderer-component)
52
- 21. [Layout Grid component](#layout-grid-component)
53
- 22. [Breadcrumb component](#breadcrumb-component)
54
- 23. [Print Layout Builder](#print-layout-builder)
55
- 24. [Calculation Scope Reference](#calculation-scope-reference)
56
- 25. [KPI Component Actions](#kpi-component-actions)
57
- 26. [Global Data Store](#global-data-store)
58
- 27. [Dark / Light theme](#dark--light-theme)
59
- 28. [Syncing local changes](#syncing-local-changes)
60
- 29. [Troubleshooting](#troubleshooting)
61
- 30. [Publishing](#publishing)
62
- 31. [Changelog](#changelog)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robobyte-front-builder",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "description": "RoboByte low-code UI builder, Report builder, and navigation extension system",
5
5
  "keywords": [
6
6
  "low-code",
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
- return (
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 { createTheme } from '@mui/material/styles'
14
+ import { buildMuiTheme } from 'src/lib/muiTheme'
15
15
 
16
- // ── Builder themes (inlined to avoid cross-package path resolution issues) ────
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: createTheme({
20
- palette: {
21
- mode: 'dark',
22
- primary: { main: '#9c27b0' },
23
- secondary: { main: '#f5a623' },
24
- background: { default: '#141618', paper: '#1e2227' },
25
- divider: '#3a3f45',
26
- text: { primary: '#e8eaed', secondary: '#9aa0aa' },
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
- light: createTheme({
30
- palette: {
31
- mode: 'light',
32
- primary: { main: '#9c27b0' },
33
- secondary: { main: '#f5a623' },
34
- background: { default: '#f0f2f5', paper: '#ffffff' },
35
- divider: '#dde1e7',
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 { createTheme } from '@mui/material/styles'
12
+ import { buildMuiTheme } from 'src/lib/muiTheme'
13
13
 
14
- // ── Builder themes (inlined to avoid cross-package path resolution issues) ────
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: createTheme({
18
- palette: {
19
- mode: 'dark',
20
- primary: { main: '#9c27b0' },
21
- secondary: { main: '#f5a623' },
22
- background: { default: '#141618', paper: '#1e2227' },
23
- divider: '#3a3f45',
24
- text: { primary: '#e8eaed', secondary: '#9aa0aa' },
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
- light: createTheme({
28
- palette: {
29
- mode: 'light',
30
- primary: { main: '#9c27b0' },
31
- secondary: { main: '#f5a623' },
32
- background: { default: '#f0f2f5', paper: '#ffffff' },
33
- divider: '#dde1e7',
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' }