robobyte-front-builder 1.0.26 → 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.
Files changed (88) hide show
  1. package/INTEGRATION.md +6 -0
  2. package/LICENSE +65 -0
  3. package/README.md +166 -21
  4. package/docs/ReportViewer.md +581 -0
  5. package/docs/fetchReportData.md +379 -0
  6. package/docs/printLayout.md +405 -0
  7. package/package.json +29 -1
  8. package/src/lib/index.js +14 -0
  9. package/src/lib/muiTheme.js +655 -0
  10. package/src/lib/providers/RoboByteFrontBuilderProvider.jsx +45 -1
  11. package/src/pages/_app.js +1 -0
  12. package/src/pages/printBuilder/index.jsx +26 -19
  13. package/src/pages/viewBuilder/index.jsx +29 -19
  14. package/training/00-index.md +168 -0
  15. package/training/01-input.md +144 -0
  16. package/training/02-checkbox.md +107 -0
  17. package/training/03-dropdown.md +135 -0
  18. package/training/04-datepicker.md +139 -0
  19. package/training/05-radio.md +123 -0
  20. package/training/06-number.md +133 -0
  21. package/training/07-textarea.md +114 -0
  22. package/training/08-richtext.md +112 -0
  23. package/training/09-tag.md +110 -0
  24. package/training/10-time.md +107 -0
  25. package/training/11-toggle.md +108 -0
  26. package/training/12-signature.md +107 -0
  27. package/training/13-autocomplete.md +134 -0
  28. package/training/14-button.md +168 -0
  29. package/training/15-label.md +138 -0
  30. package/training/16-header.md +128 -0
  31. package/training/17-divider.md +96 -0
  32. package/training/18-image.md +105 -0
  33. package/training/19-link.md +108 -0
  34. package/training/20-banner.md +122 -0
  35. package/training/21-progress-circle.md +101 -0
  36. package/training/22-progress-line.md +93 -0
  37. package/training/23-menu.md +139 -0
  38. package/training/24-popover.md +114 -0
  39. package/training/25-layout.md +116 -0
  40. package/training/26-layout-cell.md +143 -0
  41. package/training/27-card.md +87 -0
  42. package/training/28-wizard.md +126 -0
  43. package/training/29-wizard-step.md +92 -0
  44. package/training/30-repeater.md +123 -0
  45. package/training/31-dialog.md +131 -0
  46. package/training/32-breadcrumb.md +121 -0
  47. package/training/33-dataGrid.md +129 -0
  48. package/training/34-dataTableViewer.md +115 -0
  49. package/training/35-reportViewer.md +673 -0
  50. package/training/36-viewRenderer.md +110 -0
  51. package/training/37-treeView.md +170 -0
  52. package/training/38-kpi-metric.md +148 -0
  53. package/training/39-kpi-trend.md +105 -0
  54. package/training/40-kpi-badge.md +112 -0
  55. package/training/41-kpi-statusDot.md +118 -0
  56. package/training/42-kpi-iconBox.md +114 -0
  57. package/training/43-kpi-gauge.md +143 -0
  58. package/training/44-kpi-bulletChart.md +126 -0
  59. package/training/45-kpi-colorScale.md +143 -0
  60. package/training/46-kpi-rating.md +125 -0
  61. package/training/47-kpi-countdown.md +151 -0
  62. package/training/48-fetchReportData.md +276 -0
  63. package/training/49-printLayout.md +215 -0
  64. package/training/examples/01-login-form.json +176 -0
  65. package/training/examples/02-contact-form.json +141 -0
  66. package/training/examples/03-kpi-cards-row.json +123 -0
  67. package/training/examples/04-settings-toggles.json +153 -0
  68. package/training/examples/05-user-profile-card.json +136 -0
  69. package/training/examples/06-date-range-filter.json +108 -0
  70. package/training/examples/07-search-bar-results.json +130 -0
  71. package/training/examples/08-notification-settings.json +131 -0
  72. package/training/examples/09-employee-profile-form.json +259 -0
  73. package/training/examples/10-invoice-form.json +241 -0
  74. package/training/examples/11-dashboard-overview.json +251 -0
  75. package/training/examples/12-registration-wizard.json +154 -0
  76. package/training/examples/13-product-catalog.json +168 -0
  77. package/training/examples/14-data-table-with-filters.json +180 -0
  78. package/training/examples/15-tabbed-profile.json +92 -0
  79. package/training/examples/16-kpi-full-row.json +203 -0
  80. package/training/examples/17-tree-detail-view.json +139 -0
  81. package/training/examples/18-employee-management.json +233 -0
  82. package/training/examples/19-sales-dashboard.json +272 -0
  83. package/training/examples/20-checkout-wizard.json +225 -0
  84. package/training/examples/21-analytics-page.json +222 -0
  85. package/training/examples/22-hr-onboarding.json +222 -0
  86. package/training/examples/23-document-browser.json +241 -0
  87. package/training/examples/24-order-management.json +290 -0
  88. package/training/examples/25-crm-contact-page.json +272 -0
@@ -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
+ }