mui-atelier 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +32 -0
  4. package/README.md +282 -0
  5. package/dist/components/AddressFields.js +83 -0
  6. package/dist/components/AuthCard.js +94 -0
  7. package/dist/components/AuthSplash.js +88 -0
  8. package/dist/components/BrandHeader.js +81 -0
  9. package/dist/components/BrandLink.js +22 -0
  10. package/dist/components/BrandMark.js +69 -0
  11. package/dist/components/BrandProvider.js +14 -0
  12. package/dist/components/Breadcrumbs.js +62 -0
  13. package/dist/components/CardHead.js +46 -0
  14. package/dist/components/ConfirmProvider.js +77 -0
  15. package/dist/components/DashboardLayout.js +645 -0
  16. package/dist/components/DataTable.js +546 -0
  17. package/dist/components/DetailRows.js +42 -0
  18. package/dist/components/EmptyNote.js +28 -0
  19. package/dist/components/Eyebrow.js +21 -0
  20. package/dist/components/HeadingHelp.js +38 -0
  21. package/dist/components/HeadingLevel.js +18 -0
  22. package/dist/components/IconTile.js +42 -0
  23. package/dist/components/ImagePreviewDialog.js +86 -0
  24. package/dist/components/LabeledField.js +156 -0
  25. package/dist/components/LanguageSwitcher.js +43 -0
  26. package/dist/components/ListRow.js +59 -0
  27. package/dist/components/ModeToggle.js +35 -0
  28. package/dist/components/PageHeader.js +65 -0
  29. package/dist/components/PasswordField.js +43 -0
  30. package/dist/components/PublicLayout.js +27 -0
  31. package/dist/components/PublicNav.js +17 -0
  32. package/dist/components/RouteMessage.js +31 -0
  33. package/dist/components/RowList.js +10 -0
  34. package/dist/components/SafeHtml.js +57 -0
  35. package/dist/components/SaveState.js +13 -0
  36. package/dist/components/SectionPanel.js +48 -0
  37. package/dist/components/SectionTabs.js +139 -0
  38. package/dist/components/SiteNav.js +130 -0
  39. package/dist/components/Skeletons.js +63 -0
  40. package/dist/components/Sortable.js +81 -0
  41. package/dist/components/StatTile.js +66 -0
  42. package/dist/components/StatusChip.js +17 -0
  43. package/dist/components/StatusDot.js +42 -0
  44. package/dist/components/SubCard.js +30 -0
  45. package/dist/components/TabbedCard.js +20 -0
  46. package/dist/components/TableFilters.js +247 -0
  47. package/dist/components/ToastProvider.js +65 -0
  48. package/dist/eslint/chipRules.mjs +65 -0
  49. package/dist/hooks/useDocumentTitle.js +19 -0
  50. package/dist/hooks/useOverflowTabs.js +53 -0
  51. package/dist/hooks/useUnsavedChanges.js +46 -0
  52. package/dist/i18n/loadMessages.js +15 -0
  53. package/dist/i18n/mergeMessages.js +13 -0
  54. package/dist/i18n/messages/en.json +103 -0
  55. package/dist/i18n/messages/es.json +103 -0
  56. package/dist/i18n/messages/zh.json +103 -0
  57. package/dist/lib/authCard.js +4 -0
  58. package/dist/lib/chipTone.js +46 -0
  59. package/dist/lib/countryRegionData.js +29 -0
  60. package/dist/lib/countryRegionData.json +1 -0
  61. package/dist/lib/navLock.js +22 -0
  62. package/dist/lib/navTabSx.js +78 -0
  63. package/dist/lib/orgTime.js +243 -0
  64. package/dist/lib/pageHtml.js +139 -0
  65. package/dist/lib/safeNext.js +13 -0
  66. package/dist/lib/sanitizeHtml.js +36 -0
  67. package/dist/lib/slugify.js +7 -0
  68. package/dist/lib/tableFilter.js +147 -0
  69. package/dist/lib/tableStyle.js +87 -0
  70. package/dist/test/jsdomShims.js +53 -0
  71. package/dist/test/themeContract.js +178 -0
  72. package/dist/theme/OrgThemeRegistry.js +71 -0
  73. package/dist/theme/ThemeRegistry.js +17 -0
  74. package/dist/theme/berryNavRow.js +21 -0
  75. package/dist/theme/createSiteTheme.js +974 -0
  76. package/dist/theme/themes.js +438 -0
  77. package/package.json +118 -0
@@ -0,0 +1,974 @@
1
+ import { createTheme } from "@mui/material/styles";
2
+ const BODY_FONT = 'var(--font-inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif)';
3
+ const MONO_FONT = 'var(--font-mono, "SF Mono", ui-monospace, Menlo, Consolas, "Liberation Mono", monospace)';
4
+ const AMBIENT = { warm: "87,64,30", neutral: "17,24,39", glow: "10,12,20", none: "17,24,39" };
5
+ const BERRY_CARD_SHADOW = "0 2px 14px 0 rgb(32 40 45 / 8%)";
6
+ const BERRY_CARD_SHADOW_LIFT = "0 4px 20px 0 rgb(32 40 45 / 12%)";
7
+ const OUTLINE_ALPHA = 0.8;
8
+ function createSiteTheme(def) {
9
+ const { accent, accent2, palette, radii, shadow, ui, rail, chrome, ink } = def;
10
+ const focus = def.focus ?? { light: accent.light, dark: accent.dark };
11
+ const berryChrome = ui.chrome === "berry";
12
+ const grad = "linear-gradient(135deg, var(--rt-grad-from), var(--rt-grad-to))";
13
+ const amb = AMBIENT[shadow] ?? AMBIENT.neutral;
14
+ const flat = shadow === "none";
15
+ const bodyFont = def.bodyFont ?? BODY_FONT;
16
+ const heading = {
17
+ ...def.headingFont ? { fontFamily: def.headingFont, fontWeight: 600 } : {},
18
+ // A theme may give its headings a darker ink than its body (Berry's
19
+ // grey-900 over grey-700). The var is emitted by the colour schemes below
20
+ // and re-scoped by OrgThemeRegistry like every other palette var.
21
+ ...palette.light.textHeading ? { color: "var(--mui-palette-text-heading)" } : {}
22
+ };
23
+ const dense = !!ui.dense;
24
+ const railInk = ui.nav === "rail" && rail ? {
25
+ bg: rail.bg,
26
+ fg: rail.fg,
27
+ dim: `color-mix(in srgb, ${rail.fg} 62%, transparent)`,
28
+ line: `color-mix(in srgb, ${rail.fg} 16%, transparent)`
29
+ } : {
30
+ bg: def.palette.dark.bgDefault,
31
+ fg: def.palette.dark.textPrimary,
32
+ dim: def.palette.dark.textSecondary,
33
+ line: def.palette.dark.divider
34
+ };
35
+ const lift = {
36
+ rest: `0 1px 2px rgba(${amb},0.05), 0 4px 10px -6px rgba(${amb},0.16)`,
37
+ raised: `0 2px 4px rgba(${amb},0.06), 0 10px 20px -10px rgba(${amb},0.22)`,
38
+ restDark: "0 1px 2px rgba(0,0,0,0.36), 0 4px 10px -6px rgba(0,0,0,0.46)",
39
+ raisedDark: "0 2px 4px rgba(0,0,0,0.40), 0 10px 20px -10px rgba(0,0,0,0.54)"
40
+ };
41
+ const containedPrimary = ui.button === "gradient" ? {
42
+ props: { variant: "contained", color: "primary" },
43
+ style: ({ theme }) => {
44
+ const pc = theme.vars.palette.primary.mainChannel;
45
+ return {
46
+ backgroundImage: grad,
47
+ color: theme.vars.palette.primary.contrastText,
48
+ boxShadow: ui.glow ? `0 2px 7px -2px rgba(${pc} / 0.34), 0 7px 18px -8px rgba(${pc} / 0.30)` : `0 1px 2px rgba(${amb},0.10), 0 6px 12px -8px rgba(${pc} / 0.30)`,
49
+ "&:hover": {
50
+ backgroundImage: grad,
51
+ filter: "brightness(1.06)",
52
+ transform: "translateY(-1px)",
53
+ boxShadow: ui.glow ? `0 3px 10px -2px rgba(${pc} / 0.42), 0 10px 22px -8px rgba(${pc} / 0.34)` : `0 2px 5px rgba(${amb},0.12), 0 8px 16px -8px rgba(${pc} / 0.36)`
54
+ },
55
+ "&:active": { transform: "translateY(0)", filter: "brightness(0.98)" },
56
+ "&.Mui-disabled": { backgroundImage: "none" }
57
+ };
58
+ }
59
+ } : {
60
+ props: { variant: "contained", color: "primary" },
61
+ style: ({ theme }) => {
62
+ const pc = theme.vars.palette.primary.mainChannel;
63
+ if (ui.solidShadow) {
64
+ return {
65
+ backgroundImage: "none",
66
+ backgroundColor: theme.vars.palette.primary.main,
67
+ color: theme.vars.palette.primary.contrastText,
68
+ fontWeight: 700,
69
+ boxShadow: `0 5px 10px 0 rgba(${pc} / 0.16)`,
70
+ "&:hover": { backgroundColor: theme.vars.palette.primary.dark, boxShadow: "none" },
71
+ "&:active": { backgroundColor: theme.vars.palette.primary.dark, boxShadow: "none" },
72
+ "&.Mui-disabled": { boxShadow: "none" }
73
+ };
74
+ }
75
+ if (ui.materialShadow) {
76
+ return {
77
+ backgroundImage: "none",
78
+ backgroundColor: theme.vars.palette.primary.main,
79
+ color: theme.vars.palette.primary.contrastText,
80
+ boxShadow: theme.shadows[2],
81
+ "&:hover": { backgroundColor: theme.vars.palette.primary.dark, boxShadow: theme.shadows[4] },
82
+ "&:active": { boxShadow: theme.shadows[8] },
83
+ "&.Mui-disabled": { boxShadow: "none" }
84
+ };
85
+ }
86
+ return {
87
+ backgroundImage: "none",
88
+ backgroundColor: theme.vars.palette.primary.main,
89
+ color: theme.vars.palette.primary.contrastText,
90
+ boxShadow: flat ? "none" : `0 1px 2px rgba(${amb},0.12)`,
91
+ "&:hover": {
92
+ backgroundColor: theme.vars.palette.primary.main,
93
+ filter: "brightness(0.92)",
94
+ boxShadow: flat ? "none" : `0 2px 5px rgba(${amb},0.14)`
95
+ },
96
+ "&:active": { filter: "brightness(0.86)" }
97
+ };
98
+ }
99
+ };
100
+ const inputRoot = ({ theme }) => {
101
+ const pc = theme.vars.palette.primary.mainChannel;
102
+ const base = {
103
+ borderRadius: radii.input,
104
+ transition: "background-color 140ms, box-shadow 140ms"
105
+ };
106
+ const denseInput = dense ? { "& .MuiOutlinedInput-input": { paddingBlock: 9 } } : {};
107
+ if (ui.input === "filled") {
108
+ return {
109
+ ...base,
110
+ ...denseInput,
111
+ backgroundColor: `rgba(${pc} / 0.06)`,
112
+ "& .MuiOutlinedInput-notchedOutline": { borderColor: "transparent" },
113
+ "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: `rgba(${pc} / 0.35)` },
114
+ "&.Mui-focused": { backgroundColor: theme.vars.palette.background.paper, boxShadow: `0 0 0 3px rgba(${pc} / 0.16)` },
115
+ "&.Mui-focused .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.primary.main, borderWidth: 1 }
116
+ };
117
+ }
118
+ if (ui.input === "glass") {
119
+ return {
120
+ ...base,
121
+ ...denseInput,
122
+ backgroundColor: `rgba(${pc} / 0.05)`,
123
+ "& .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.divider },
124
+ "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: `rgba(${pc} / 0.5)` },
125
+ "&.Mui-focused .MuiOutlinedInput-notchedOutline": { borderWidth: 1, borderColor: theme.vars.palette.primary.main },
126
+ "&.Mui-focused": { boxShadow: `0 0 0 3px rgba(${pc} / 0.18)` }
127
+ };
128
+ }
129
+ if (ui.input === "minimal") {
130
+ return {
131
+ ...base,
132
+ ...denseInput,
133
+ backgroundColor: "transparent",
134
+ "& .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.divider, borderWidth: 1 },
135
+ "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.text.secondary },
136
+ "&.Mui-focused .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.primary.main, borderWidth: 1 }
137
+ };
138
+ }
139
+ if (ui.input === "raised") {
140
+ return {
141
+ ...base,
142
+ ...denseInput,
143
+ backgroundColor: theme.vars.palette.background.paper,
144
+ boxShadow: lift.rest,
145
+ "& .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.divider },
146
+ "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: `rgba(${pc} / 0.44)` },
147
+ "&.Mui-focused .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.primary.main, borderWidth: 1 },
148
+ ...theme.applyStyles("dark", { boxShadow: lift.restDark })
149
+ };
150
+ }
151
+ if (ui.input === "berry") {
152
+ return {
153
+ ...base,
154
+ ...denseInput,
155
+ backgroundColor: palette.light.field,
156
+ "& .MuiOutlinedInput-input": { fontWeight: 500, paddingBlock: "15.5px" },
157
+ "& .MuiOutlinedInput-notchedOutline": { borderColor: palette.light.fieldEdge },
158
+ /*
159
+ * THE 200 STEP, not `primary.light`, once a theme names its ramp. Berry's own rule reads `primary.light` — but Berry's
160
+ * primary is the BLUE, so its light step is a near-white #E3F2FD, and
161
+ * the rule is written `'&:hover $notchedOutline'`, JSS syntax that
162
+ * emotion never matches, so in Berry itself this hover does nothing at
163
+ * all. Ours matches. Pointed at the named light step it would draw a
164
+ * hover border paler than the resting one — a control that looks
165
+ * DISABLED when the pointer reaches it. #B39DDB is the step that reads.
166
+ */
167
+ "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.primary[200] ?? theme.vars.palette.primary.light },
168
+ "&.Mui-focused .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.primary.main, borderWidth: 1 },
169
+ ...theme.applyStyles("dark", {
170
+ backgroundColor: palette.dark.field,
171
+ "& .MuiOutlinedInput-notchedOutline": { borderColor: palette.dark.fieldEdge }
172
+ })
173
+ };
174
+ }
175
+ if (ui.input === "ruled") {
176
+ return {
177
+ ...base,
178
+ ...denseInput,
179
+ borderRadius: 0,
180
+ backgroundColor: "transparent",
181
+ "& .MuiOutlinedInput-notchedOutline": { border: "none", borderBottom: `1px solid ${theme.vars.palette.text.secondary}` },
182
+ "&:hover .MuiOutlinedInput-notchedOutline": { borderBottomColor: theme.vars.palette.text.primary },
183
+ "&.Mui-focused .MuiOutlinedInput-notchedOutline": { borderBottomWidth: 2, borderBottomColor: theme.vars.palette.primary.main },
184
+ "& .MuiOutlinedInput-input": { paddingInline: 2 }
185
+ };
186
+ }
187
+ return {
188
+ ...base,
189
+ ...denseInput,
190
+ backgroundColor: theme.vars.palette.background.paper,
191
+ "& .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.divider },
192
+ "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.text.secondary },
193
+ "&.Mui-focused .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.primary.main, borderWidth: 1 },
194
+ "&.Mui-focused": { boxShadow: `0 0 0 2px rgba(${theme.vars.palette.primary.mainChannel} / 0.18)` }
195
+ };
196
+ };
197
+ const cardSurface = ({ theme }) => {
198
+ const pc = theme.vars.palette.primary.mainChannel;
199
+ const base = {
200
+ borderRadius: radii.card,
201
+ border: `1px solid ${theme.vars.palette.divider}`,
202
+ backgroundImage: "none",
203
+ transition: "box-shadow 180ms ease, transform 180ms ease, border-color 180ms ease"
204
+ };
205
+ if (ui.surface === "minimal") {
206
+ return {
207
+ borderRadius: radii.card,
208
+ border: `1px solid ${theme.vars.palette.divider}`,
209
+ backgroundImage: "none",
210
+ transition: "box-shadow 180ms ease, transform 180ms ease, border-color 180ms ease",
211
+ boxShadow: "0 0 2px 0 rgba(145,158,171,0.16), 0 8px 16px -6px rgba(145,158,171,0.08)",
212
+ ...theme.applyStyles("dark", { boxShadow: "0 0 2px 0 rgba(0,0,0,0.20), 0 8px 16px -6px rgba(0,0,0,0.26)" }),
213
+ "&:has(.MuiCardActionArea-root):hover": {
214
+ transform: "translateY(-2px)",
215
+ boxShadow: "0 0 2px 0 rgba(145,158,171,0.18), 0 12px 22px -6px rgba(145,158,171,0.16)",
216
+ ...theme.applyStyles("dark", { boxShadow: "0 0 2px 0 rgba(0,0,0,0.26), 0 12px 22px -6px rgba(0,0,0,0.34)" })
217
+ }
218
+ };
219
+ }
220
+ if (ui.surface === "berry") {
221
+ return {
222
+ borderRadius: radii.card,
223
+ border: "none",
224
+ backgroundImage: "none",
225
+ transition: "box-shadow 180ms ease",
226
+ boxShadow: BERRY_CARD_SHADOW,
227
+ "&:has(.MuiCardActionArea-root):hover": { boxShadow: BERRY_CARD_SHADOW_LIFT }
228
+ };
229
+ }
230
+ if (ui.surface === "soft") {
231
+ return {
232
+ ...base,
233
+ boxShadow: `0 1px 2px rgba(${amb},0.04), 0 6px 14px -8px rgba(${amb},0.10), 0 16px 30px -18px rgba(${amb},0.20)`,
234
+ ...theme.applyStyles("dark", { boxShadow: "0 1px 2px rgba(0,0,0,0.35), 0 10px 22px -18px rgba(0,0,0,0.42)" }),
235
+ "&:has(.MuiCardActionArea-root):hover": {
236
+ transform: "translateY(-3px)",
237
+ borderColor: `rgba(${pc} / 0.35)`,
238
+ boxShadow: `0 3px 8px rgba(${amb},0.06), 0 18px 34px -18px rgba(${pc} / 0.22)`
239
+ }
240
+ };
241
+ }
242
+ if (ui.surface === "glass") {
243
+ return {
244
+ ...base,
245
+ position: "relative",
246
+ overflow: "hidden",
247
+ backgroundImage: `linear-gradient(180deg, rgba(${pc} / 0.05), transparent)`,
248
+ boxShadow: `0 1px 2px rgba(0,0,0,0.04), 0 6px 14px -10px rgba(${pc} / 0.10), 0 18px 32px -18px rgba(${amb},0.20)`,
249
+ ...theme.applyStyles("dark", { boxShadow: `0 1px 2px rgba(0,0,0,0.4), 0 13px 28px -20px rgba(${pc} / 0.20)` }),
250
+ "&::before": { content: '""', position: "absolute", insetInline: 0, top: 0, height: "1px", background: `linear-gradient(90deg, transparent, rgba(${pc} / 0.6), transparent)` },
251
+ "&:has(.MuiCardActionArea-root):hover": { transform: "translateY(-2px)", borderColor: `rgba(${pc} / 0.5)` }
252
+ };
253
+ }
254
+ return {
255
+ ...base,
256
+ boxShadow: `0 1px 2px rgba(${amb},0.04), 0 7px 16px -12px rgba(${amb},0.13)`,
257
+ ...theme.applyStyles("dark", { boxShadow: "0 1px 2px rgba(0,0,0,0.32), 0 9px 18px -14px rgba(0,0,0,0.36)" }),
258
+ "&:has(.MuiCardActionArea-root):hover": {
259
+ borderColor: `rgba(${pc} / 0.45)`,
260
+ boxShadow: `0 2px 4px rgba(${amb},0.05), 0 11px 22px -12px rgba(${amb},0.17)`
261
+ }
262
+ };
263
+ };
264
+ const cardEdge = (theme) => {
265
+ if (ui.cardEdge === "gradient") {
266
+ return {
267
+ position: "relative",
268
+ overflow: "hidden",
269
+ "&::after": { content: '""', position: "absolute", insetInline: 0, top: 0, height: "3px", backgroundImage: "linear-gradient(90deg, var(--rt-grad-from), var(--rt-grad-to))" }
270
+ };
271
+ }
272
+ if (ui.cardEdge === "accent") {
273
+ return {
274
+ position: "relative",
275
+ overflow: "hidden",
276
+ "&::after": { content: '""', position: "absolute", insetInline: 0, top: 0, height: "3px", backgroundColor: theme.vars.palette.primary.main }
277
+ };
278
+ }
279
+ if (ui.cardEdge === "ink") {
280
+ return { borderTop: `2px solid ${theme.vars.palette.text.primary}` };
281
+ }
282
+ return {};
283
+ };
284
+ const cardRoot = (args) => ({ ...cardSurface(args), ...cardEdge(args.theme) });
285
+ const dialogPaper = ({ theme }) => {
286
+ const pc = theme.vars.palette.primary.mainChannel;
287
+ const base = {
288
+ borderRadius: radii.dialog,
289
+ border: `1px solid ${theme.vars.palette.divider}`,
290
+ backgroundImage: "none",
291
+ position: "relative",
292
+ overflow: "hidden",
293
+ // clip the top rule to the dialog's rounded corners
294
+ // On a phone use nearly the full width — MUI's default 32px margin leaves a
295
+ // form dialog cramped at 375px. A slim 12px gutter each side instead.
296
+ [theme.breakpoints.down("sm")]: {
297
+ margin: 12,
298
+ width: "calc(100% - 24px)",
299
+ maxWidth: "calc(100% - 24px)",
300
+ /*
301
+ * A TASK-SIZED DIALOG TAKES THE WHOLE PHONE.
302
+ *
303
+ * A gutter is right for a confirm — a full-screen "Delete this?" is a
304
+ * jarring answer to a small question. It is not right for a wizard, a
305
+ * template editor, an import preview or a picker, each of which is a
306
+ * whole task rendered at `maxWidth="md"`: at 375px those become a form
307
+ * inside a box inside a box, with the 24px of gutter taken off the only dimension that
308
+ * mattered.
309
+ *
310
+ * Keyed on the SIZE the dialog already declares rather than on a new
311
+ * prop, so all twenty-seven get the right treatment without a single
312
+ * call site changing — and a dialog added later inherits it by saying
313
+ * how big it is, which it has to do anyway.
314
+ */
315
+ "&.MuiDialog-paperWidthMd, &.MuiDialog-paperWidthLg, &.MuiDialog-paperWidthXl": {
316
+ margin: 0,
317
+ width: "100%",
318
+ maxWidth: "100%",
319
+ height: "100%",
320
+ maxHeight: "100%",
321
+ borderRadius: 0,
322
+ border: 0
323
+ }
324
+ },
325
+ "&::before": {
326
+ content: '""',
327
+ position: "absolute",
328
+ insetInline: 0,
329
+ top: 0,
330
+ height: "3px",
331
+ backgroundImage: grad
332
+ }
333
+ };
334
+ if (ui.surface === "berry") {
335
+ return {
336
+ ...base,
337
+ border: "none",
338
+ padding: "12px 0",
339
+ boxShadow: theme.shadows[24],
340
+ "&::before": { display: "none" }
341
+ };
342
+ }
343
+ if (ui.surface === "minimal") {
344
+ return {
345
+ ...base,
346
+ border: "none",
347
+ boxShadow: "0 0 3px 0 rgba(145,158,171,0.18), 0 20px 40px -14px rgba(145,158,171,0.22)",
348
+ ...theme.applyStyles("dark", { boxShadow: "0 0 3px 0 rgba(0,0,0,0.34), 0 20px 40px -14px rgba(0,0,0,0.46)" })
349
+ };
350
+ }
351
+ if (ui.surface === "glass") {
352
+ return {
353
+ ...base,
354
+ backgroundImage: `linear-gradient(180deg, rgba(${pc} / 0.06), transparent 160px)`,
355
+ boxShadow: ui.glow ? `0 0 0 1px rgba(${pc} / 0.12), 0 22px 46px -18px rgba(${pc} / 0.28)` : `0 19px 40px -18px rgba(${amb},0.34)`,
356
+ ...theme.applyStyles("dark", { boxShadow: `0 0 0 1px rgba(${pc} / 0.16), 0 22px 46px -18px rgba(0,0,0,0.55)` })
357
+ };
358
+ }
359
+ if (flat) {
360
+ return {
361
+ ...base,
362
+ boxShadow: `0 10px 30px -16px rgba(${amb},0.24)`,
363
+ ...theme.applyStyles("dark", { boxShadow: "0 12px 34px -16px rgba(0,0,0,0.52)" })
364
+ };
365
+ }
366
+ return {
367
+ ...base,
368
+ boxShadow: `0 2px 5px rgba(${amb},0.05), 0 20px 40px -18px rgba(${amb},0.30)`,
369
+ ...theme.applyStyles("dark", { boxShadow: "0 2px 5px rgba(0,0,0,0.4), 0 20px 40px -16px rgba(0,0,0,0.5)" })
370
+ };
371
+ };
372
+ const softLabel = (color) => ({
373
+ props: { variant: "filled", color },
374
+ style: ({ theme }) => ({
375
+ backgroundColor: `rgba(${theme.vars.palette[color].mainChannel} / 0.16)`,
376
+ color: theme.vars.palette[color].dark,
377
+ ...theme.applyStyles("dark", { color: theme.vars.palette[color].light })
378
+ })
379
+ });
380
+ const defaultChip = {
381
+ props: { variant: "filled", color: "default" },
382
+ style: ({ theme }) => ({ backgroundColor: theme.vars.palette.action.hover, color: theme.vars.palette.text.secondary })
383
+ };
384
+ const solidPrimaryChip = {
385
+ props: { variant: "filled", color: "primary" },
386
+ style: ({ theme }) => ({
387
+ backgroundColor: theme.vars.palette.primary.main,
388
+ color: theme.vars.palette.primary.contrastText,
389
+ /*
390
+ * THE DARK OVERRIDE HAS TO BE RESTATED, and it is not belt-and-braces.
391
+ *
392
+ * `softFill` above sets its dark colour through `applyStyles('dark', …)`,
393
+ * which emits a NESTED SELECTOR. A flat `color` here loses to it on
394
+ * SPECIFICITY no matter that this variant comes later in the array —
395
+ * order only decides ties. So the chip took the solid background from
396
+ * this variant and the pale label from that one: `primary.light` text on
397
+ * `primary.main` fill, which on damson measured 1.2:1 and was effectively
398
+ * invisible. Matching selector shape puts the two back on equal footing,
399
+ * where being last wins.
400
+ *
401
+ * Light mode was never as bad (softFill uses `.dark` there, on a solid
402
+ * mid-tone) which is exactly why this survived review.
403
+ */
404
+ ...theme.applyStyles("dark", { color: theme.vars.palette.primary.contrastText }),
405
+ fontWeight: 700
406
+ })
407
+ };
408
+ const accent2Chip = accent2 ? [{
409
+ props: { variant: "outlined", color: "default" },
410
+ style: ({ theme }) => ({
411
+ borderColor: `color-mix(in srgb, ${accent2.light} 55%, transparent)`,
412
+ color: accent2.light,
413
+ ...theme.applyStyles("dark", {
414
+ borderColor: `color-mix(in srgb, ${accent2.dark} 55%, transparent)`,
415
+ color: accent2.dark
416
+ })
417
+ })
418
+ }] : [];
419
+ const softFill = (color, alpha) => ({
420
+ props: { variant: "filled", color },
421
+ style: ({ theme }) => ({
422
+ backgroundColor: `rgba(${theme.vars.palette[color].mainChannel} / ${alpha})`,
423
+ color: theme.vars.palette[color].dark,
424
+ ...theme.applyStyles("dark", { color: theme.vars.palette[color].light })
425
+ })
426
+ });
427
+ const berryChip = (color) => ({
428
+ props: { variant: "filled", color },
429
+ style: ({ theme }) => ({
430
+ backgroundColor: `rgba(${theme.vars.palette[color].mainChannel} / 0.12)`,
431
+ color: theme.vars.palette[color].main,
432
+ ...theme.applyStyles("dark", {
433
+ backgroundColor: `rgba(${theme.vars.palette[color].mainChannel} / 0.2)`,
434
+ color: theme.vars.palette[color].light
435
+ })
436
+ })
437
+ });
438
+ const chipVariants = [
439
+ ...ui.chips === "berry" ? [berryChip("primary"), berryChip("secondary"), berryChip("info"), berryChip("success"), berryChip("warning"), berryChip("error"), defaultChip] : ui.labelChips ? [softLabel("primary"), softLabel("info"), softLabel("success"), softLabel("warning"), softLabel("error"), defaultChip] : [
440
+ softFill("primary", 0.14),
441
+ // The data hue is a chip colour like the other four — without this it
442
+ // fell through to MUI's own filled chip, a SOLID block with white
443
+ // text, and every "Published" on nineteen themes was the one heavy
444
+ // thing in a row of tints.
445
+ softFill("info", 0.14),
446
+ softFill("success", 0.16),
447
+ softFill("warning", 0.2),
448
+ softFill("error", 0.2),
449
+ defaultChip
450
+ ],
451
+ // After the base variants, so these win on order.
452
+ ...ui.solidChips ? [solidPrimaryChip] : [],
453
+ ...accent2Chip
454
+ ];
455
+ const ramp = (scheme, key) => def.ramps?.[scheme]?.[key] ?? {};
456
+ const grey = (scheme) => def.ramps?.[scheme]?.grey ? { grey: def.ramps[scheme].grey } : {};
457
+ return createTheme({
458
+ cssVariables: { colorSchemeSelector: "class" },
459
+ colorSchemes: {
460
+ light: {
461
+ palette: {
462
+ primary: { ...ramp("light", "primary"), main: accent.light, contrastText: accent.contrastLight },
463
+ secondary: { ...ramp("light", "secondary"), main: palette.light.secondary, contrastText: "#FFFFFF" },
464
+ success: { ...ramp("light", "success"), main: palette.light.success },
465
+ warning: { ...ramp("light", "warning"), main: palette.light.warning },
466
+ error: { ...ramp("light", "error"), main: palette.light.error },
467
+ /*
468
+ * `info` IS THE DATA HUE — the colour a status wears when it is in
469
+ * flight (Submitted, Scheduled, Mitigated). Every theme names its own
470
+ * (palette.*.data): far enough from its accent to read as a different
471
+ * thing, out of the green and amber bands so it cannot be mistaken
472
+ * for success or caution, and AA on that theme's paper. It used to be
473
+ * a copy of the accent, which is why nothing could use it.
474
+ */
475
+ info: { ...ramp("light", "info"), main: palette.light.data, contrastText: "#FFFFFF" },
476
+ text: { primary: palette.light.textPrimary, secondary: palette.light.textSecondary, ...palette.light.textHeading ? { heading: palette.light.textHeading } : {} },
477
+ ...grey("light"),
478
+ divider: palette.light.divider,
479
+ background: { default: palette.light.bgDefault, paper: palette.light.bgPaper }
480
+ }
481
+ },
482
+ dark: {
483
+ palette: {
484
+ primary: { ...ramp("dark", "primary"), main: accent.dark, contrastText: accent.contrastDark },
485
+ secondary: { ...ramp("dark", "secondary"), main: palette.dark.secondary, contrastText: "#0A0B0F" },
486
+ success: { ...ramp("dark", "success"), main: palette.dark.success },
487
+ warning: { ...ramp("dark", "warning"), main: palette.dark.warning },
488
+ error: { ...ramp("dark", "error"), main: palette.dark.error },
489
+ info: { ...ramp("dark", "info"), main: palette.dark.data, contrastText: "#0A0B0F" },
490
+ text: { primary: palette.dark.textPrimary, secondary: palette.dark.textSecondary, ...palette.dark.textHeading ? { heading: palette.dark.textHeading } : {} },
491
+ ...grey("dark"),
492
+ divider: palette.dark.divider,
493
+ background: { default: palette.dark.bgDefault, paper: palette.dark.bgPaper }
494
+ }
495
+ }
496
+ },
497
+ typography: def.typeRamp === "berry" ? {
498
+ /*
499
+ * berry: Berry's ramp — Roboto at 14px, 700/600/500 weights and NO
500
+ * negative tracking — keyed to the roles the variants play here (a page
501
+ * title is h4 or h3 depending on the app; a panel title is h6), so
502
+ * Berry's h6 at 0.75rem is not what a SectionPanel title becomes.
503
+ */
504
+ fontFamily: bodyFont,
505
+ h1: { fontSize: "2.125rem", fontWeight: 700, lineHeight: 1.2, ...heading },
506
+ h2: { fontSize: "1.75rem", fontWeight: 700, lineHeight: 1.25, ...heading },
507
+ h3: { fontSize: "1.5rem", fontWeight: 600, lineHeight: 1.3, ...heading },
508
+ h4: { fontSize: "1.25rem", fontWeight: 600, ...heading },
509
+ h5: { fontSize: "1.125rem", fontWeight: 500, ...heading },
510
+ h6: { fontSize: "1rem", fontWeight: 500, ...heading },
511
+ subtitle1: { fontSize: "0.875rem", fontWeight: 500 },
512
+ subtitle2: { fontSize: "0.75rem", fontWeight: 400 },
513
+ body1: { fontSize: "0.875rem", lineHeight: 1.5 },
514
+ body2: { fontSize: "0.875rem", lineHeight: 1.5 },
515
+ caption: { fontSize: "0.75rem" },
516
+ button: { textTransform: "capitalize", fontWeight: 500, letterSpacing: "0.02857em" }
517
+ } : {
518
+ fontFamily: bodyFont,
519
+ h1: { fontSize: "2.75rem", fontWeight: 800, letterSpacing: "-0.03em", lineHeight: 1.1, ...heading },
520
+ h2: { fontSize: "2.125rem", fontWeight: 800, letterSpacing: "-0.025em", lineHeight: 1.15, ...heading },
521
+ h3: { fontSize: "1.5rem", fontWeight: 700, letterSpacing: "-0.02em", lineHeight: 1.2, ...heading },
522
+ h4: { fontSize: "1.25rem", fontWeight: 700, letterSpacing: "-0.015em", ...heading },
523
+ h5: { fontSize: "1.075rem", fontWeight: 700, letterSpacing: "-0.01em", ...heading },
524
+ h6: { fontSize: "0.95rem", fontWeight: 700, letterSpacing: "-0.005em", ...heading },
525
+ subtitle1: { letterSpacing: "-0.01em" },
526
+ body1: { lineHeight: 1.6 },
527
+ body2: { lineHeight: 1.55 },
528
+ button: { textTransform: "none", fontWeight: 600, letterSpacing: ui.button === "solid" ? "0.01em" : "0" }
529
+ },
530
+ shape: { borderRadius: radii.base },
531
+ // Custom extension: the theme's PERSONALITY, so the kit's components
532
+ // (ListRow, RowList, IconTile, EmptyNote, …) and any future component can
533
+ // render per-theme via useTheme().rt — not just inherit palette/shape.
534
+ // `rail` is read by DashboardLayout's own nav branch; `accent2` is here so a
535
+ // future component can reach the second hue without importing the theme defs.
536
+ rt: {
537
+ ui,
538
+ mono: MONO_FONT,
539
+ radii,
540
+ headingFont: def.headingFont ?? null,
541
+ outlineAlpha: OUTLINE_ALPHA,
542
+ accent2: accent2 ?? null,
543
+ rail: rail ?? null,
544
+ lift,
545
+ // berry: the chrome surface and the bar height the shells read. 64 is the
546
+ // MUI Toolbar height at md+, what every non-Berry shell already assumes.
547
+ // 84 for the ink bar: the marketing site's header height, so the app's
548
+ // front door and the page that links to it are the same object.
549
+ chrome: chrome ?? null,
550
+ barHeight: berryChrome ? 88 : ui.header === "ink" ? 84 : 64,
551
+ // berry: the resting card shadow, for the one non-Card surface that is
552
+ // drawn as a card (PageHeader's title strip). null elsewhere.
553
+ cardShadow: ui.surface === "berry" ? BERRY_CARD_SHADOW : null,
554
+ // A signed-in portal's section bar, which pins itself under the brand bar
555
+ // and which a sticky rail below (a form stepper, say) clears. One number,
556
+ // here, so the bar, the drawer and the rail cannot drift apart.
557
+ navHeight: 48
558
+ },
559
+ components: {
560
+ MuiCssBaseline: {
561
+ styleOverrides: (theme) => ({
562
+ body: {
563
+ "--rt-grad-from": accent.gradient.light[0],
564
+ "--rt-grad-to": accent.gradient.light[1],
565
+ ...theme.applyStyles("dark", { "--rt-grad-from": accent.gradient.dark[0], "--rt-grad-to": accent.gradient.dark[1] }),
566
+ /*
567
+ * THE FOCUS RING, as a variable, so a surface that the accent
568
+ * cannot be seen on can override it for its own subtree rather than
569
+ * re-declaring the whole rule. The dark rails do exactly that (see
570
+ * DashboardLayout): their ground IS the brand colour, and an accent
571
+ * ring on it is a colour on a colour.
572
+ */
573
+ "--rt-focus": focus.light,
574
+ ...theme.applyStyles("dark", { "--rt-focus": focus.dark }),
575
+ /*
576
+ * THE BRAND MARK'S SHAPE, for a theme that states one. Only the
577
+ * brand chrome does: the marketing site draws the glyph as a hard
578
+ * 2px square with no glow, and BrandMark's own default — a corner
579
+ * at 30% of the size, under a coloured glow — is a different mark
580
+ * standing next to the same wordmark. Absent, BrandMark keeps that
581
+ * default, so no org theme's mark moves.
582
+ */
583
+ ...ink?.markRadius ? { "--rt-mark-radius": ink.markRadius } : {},
584
+ ...ink?.markShadow ? { "--rt-mark-shadow": ink.markShadow } : {}
585
+ },
586
+ /*
587
+ * WHERE THE KEYBOARD IS. One `:focus-visible` rule for every theme.
588
+ *
589
+ * Inputs have a good focus treatment of their own (the tinted 3px ring
590
+ * further down this file); without this rule everything else falls
591
+ * back to MUI's
592
+ * `.Mui-focusVisible`, which is a faint `action.hover`-weight tint. On
593
+ * a nav row that already carries a tint, a gradient or a solid accent
594
+ * fill — which is most of them, across twelve nav voices — that is no
595
+ * indicator at all. A keyboard user tabbing through an admin could
596
+ * not tell where they were — WCAG 2.4.7, failing on every theme at once.
597
+ *
598
+ * `outline-offset` is what makes ONE rule work everywhere: the ring is
599
+ * drawn OUTSIDE the element, on whatever the element sits on, rather
600
+ * than on the element's own fill. A focused contained button therefore
601
+ * gets an accent ring on the page ground (where the accent is gated to
602
+ * 4.5:1) instead of an accent ring on an accent fill.
603
+ *
604
+ * `outline` rather than `box-shadow` deliberately: it survives Windows
605
+ * High Contrast Mode, where box-shadows are dropped entirely.
606
+ */
607
+ /*
608
+ * `html :focus-visible`, NOT a bare `:focus-visible`, and the extra
609
+ * type selector is the entire reason this works.
610
+ *
611
+ * MUI's ButtonBase emits `outline: 0px` in its own emotion class. A
612
+ * bare `:focus-visible` is specificity (0,1,0) — identical to a single
613
+ * class — and emotion's component styles are injected AFTER
614
+ * CssBaseline, so source order handed the tie to MUI and the ring was
615
+ * silently removed from every Button, IconButton, ListItemButton, Tab,
616
+ * Chip and CardActionArea in the product. It survived only on plain
617
+ * anchors, which is what made it look like it was working.
618
+ *
619
+ * `html` lifts this to (0,1,1), which beats a single class regardless
620
+ * of order. The input exclusions below are (0,2,0) and still win over
621
+ * this, which is what they are for.
622
+ *
623
+ * This was caught by tabbing through a real page. Both unit tests
624
+ * passed the whole time — one asserts the rule is emitted, the other
625
+ * that its colour contrasts, and neither can see a cascade.
626
+ */
627
+ "html :focus-visible": {
628
+ outline: "2px solid var(--rt-focus)",
629
+ outlineOffset: "2px"
630
+ },
631
+ /*
632
+ * The exceptions, and both are "this already has a focus treatment".
633
+ *
634
+ * A text field's ring is drawn on its notched outline by the input
635
+ * overrides in this file; the <input> inside would otherwise take a
636
+ * SECOND ring, nested a couple of pixels in from the first.
637
+ */
638
+ ".MuiInputBase-input:focus-visible, .MuiSelect-select:focus-visible": {
639
+ outline: "none"
640
+ }
641
+ })
642
+ },
643
+ MuiAppBar: {
644
+ defaultProps: { elevation: 0, color: "inherit" },
645
+ styleOverrides: {
646
+ root: ({ theme }) => {
647
+ if (ui.header === "ink" && ink) {
648
+ return {
649
+ backgroundColor: ink.bg,
650
+ backgroundImage: "none",
651
+ color: ink.text,
652
+ borderBottom: `1px solid ${ink.divider}`,
653
+ boxShadow: "none",
654
+ /*
655
+ * A BARE `position: relative` HERE OVERRODE `<AppBar position="fixed">`.
656
+ * DashboardLayout renders a fixed bar and puts a spacer under it; with
657
+ * the bar dropped back into flow the spacer's height was added a second
658
+ * time and every dashboard's content was pushed off the fold. Kept
659
+ * scoped to a STATIC bar, which is the only one that needs it.
660
+ */
661
+ "&.MuiAppBar-positionStatic": { position: "relative" },
662
+ /*
663
+ * An IconButton takes `action.active` — an INK colour, chosen for a
664
+ * light surface — rather than inheriting. On this bar that is the mode
665
+ * toggle and the language picker rendered in near-black on near-black.
666
+ */
667
+ "& .MuiIconButton-root": { color: "inherit" },
668
+ /*
669
+ * The wordmark as the site sets it: 24px, 650, tracked in, and
670
+ * lowercase with a full stop. `rt-wordmark`, NOT `.MuiTypography-h6`
671
+ * — the dashboard bar uses that variant for the ORG'S OWN NAME, and
672
+ * this would have lowercased it and punctuated it.
673
+ *
674
+ * `textTransform` is presentation only: the DOM still holds the name
675
+ * as written, so what is read aloud and what is copied are unchanged.
676
+ * The stop is generated content for the same reason — it belongs to
677
+ * the drawing of the logo, not to the name of the company.
678
+ */
679
+ "& .rt-wordmark": {
680
+ fontSize: "1.5rem",
681
+ fontWeight: 650,
682
+ letterSpacing: "-1.2px",
683
+ textTransform: "lowercase",
684
+ "&::after": { content: '"."', color: ink.period }
685
+ },
686
+ /*
687
+ * DARK SCHEME. The site has no dark mode to copy, and copying the bar
688
+ * anyway would put #0B172D against a #0B1632 page — a bar with no
689
+ * edge. The dark paper is what separates it, and the hairline stays.
690
+ */
691
+ ...theme.applyStyles("dark", { backgroundColor: theme.vars.palette.background.paper })
692
+ };
693
+ }
694
+ if (ui.nav === "inverse" || ui.nav === "rail") {
695
+ return {
696
+ backgroundColor: theme.vars.palette.background.default,
697
+ borderBottom: "none"
698
+ };
699
+ }
700
+ if (berryChrome) {
701
+ return {
702
+ backgroundColor: chrome.light,
703
+ borderBottom: "none",
704
+ boxShadow: "none",
705
+ ...theme.applyStyles("dark", { backgroundColor: chrome.dark })
706
+ };
707
+ }
708
+ return {
709
+ backgroundColor: `rgba(${theme.vars.palette.background.paperChannel} / ${ui.surface === "glass" ? 0.6 : 0.72})`,
710
+ backdropFilter: "saturate(180%) blur(12px)",
711
+ WebkitBackdropFilter: "saturate(180%) blur(12px)",
712
+ borderBottom: `1px solid ${theme.vars.palette.divider}`
713
+ };
714
+ }
715
+ }
716
+ },
717
+ /*
718
+ * nav:'inverse' — the dashboard sidebar keeps the theme's DARK ground in
719
+ * both colour schemes (the dark-chrome SaaS chrome cobalt copies). Colours
720
+ * are literals from palette.dark, NOT vars: the drawer must not flip when
721
+ * the scheme does. Only dashboard drawers exist in these apps, so keying
722
+ * MuiDrawer is safe; row selection colours live in DashboardLayout, which
723
+ * reads rt.ui.nav for the same branch.
724
+ */
725
+ /*
726
+ * nav:'rail' owns its column too, but paints it in a BRAND surface of its
727
+ * own (def.rail) rather than in the theme's dark page ground. That is the
728
+ * whole difference between the two: verdigris's #0B5E58 is not its
729
+ * dark-mode ground, and a rail that borrowed the ground would lose the
730
+ * colour the theme is named for. Literals either way, for the same reason
731
+ * — the drawer must not flip when the scheme does.
732
+ */
733
+ ...berryChrome ? {
734
+ MuiDrawer: {
735
+ styleOverrides: {
736
+ paper: ({ theme }) => ({
737
+ backgroundColor: chrome.light,
738
+ borderRight: "none",
739
+ ...theme.applyStyles("dark", { backgroundColor: chrome.dark })
740
+ })
741
+ }
742
+ }
743
+ } : {},
744
+ ...ui.nav === "inverse" || ui.nav === "rail" ? {
745
+ MuiDrawer: {
746
+ styleOverrides: {
747
+ paper: {
748
+ backgroundColor: railInk.bg,
749
+ color: railInk.fg,
750
+ borderRight: "none",
751
+ "& .MuiListSubheader-root": { color: railInk.dim },
752
+ "& .MuiDivider-root": { borderColor: railInk.line },
753
+ "& .MuiChip-outlined": { color: railInk.dim, borderColor: railInk.line },
754
+ /*
755
+ * THE FOCUS RING, RE-POINTED FOR THIS COLUMN. Everywhere else the
756
+ * ring is the accent, which is gated to 4.5:1 on paper and on the
757
+ * page ground. Neither of those is what this drawer is made of:
758
+ * `inverse` paints it the theme's dark ground and `rail` paints it
759
+ * the BRAND colour itself, so an accent ring here is either lost
760
+ * in a dark surface or a colour drawn on the same colour.
761
+ *
762
+ * The rail already names the ink that reads on it — the same value
763
+ * its labels use — so the ring borrows that and inherits the
764
+ * contrast the surface was designed with.
765
+ */
766
+ "--rt-focus": railInk.fg
767
+ }
768
+ }
769
+ }
770
+ } : {},
771
+ MuiCard: { defaultProps: { elevation: 0 }, styleOverrides: { root: cardRoot } },
772
+ MuiButton: {
773
+ styleOverrides: {
774
+ root: {
775
+ borderRadius: radii.button,
776
+ paddingInline: ui.buttonCase ? 16 : dense ? 14 : 18,
777
+ paddingBlock: dense ? 5 : void 0,
778
+ transition: "transform 140ms ease, box-shadow 140ms ease, background-color 140ms ease, filter 140ms ease"
779
+ },
780
+ sizeLarge: { paddingBlock: dense ? 8 : 11, paddingInline: dense ? 18 : 22, fontSize: "0.95rem", borderRadius: radii.buttonLarge },
781
+ sizeSmall: { paddingInline: dense ? 10 : 14 },
782
+ /*
783
+ * A BUTTON'S EDGE IS WHAT SAYS IT IS A BUTTON, so it is held to
784
+ * WCAG 1.4.11's 3:1 for non-text UI boundaries — not to the 4.5 the
785
+ * label answers to, and not to nothing.
786
+ *
787
+ * It used to be `divider`, a hairline meant for separating rows of
788
+ * text. Against each theme's own page ground that measured 1.10–1.59
789
+ * — verdigris worst — and the control read as a floating label with a
790
+ * suggestion of a box round it. The hover state was no better: accent
791
+ * at 0.5 alpha lands at 1.99–2.46, so pointing at it barely helped.
792
+ *
793
+ * OUTLINE_ALPHA is the accent's weight at rest, resolved once in `rt`
794
+ * so the test that pins this reads the same number rather than a
795
+ * second copy of it. Hover goes to the accent at FULL strength, which
796
+ * is both a real affordance and strictly an increase — the previous
797
+ * pair had hover fainter than this rest.
798
+ */
799
+ outlined: ({ theme }) => ({
800
+ borderColor: `rgba(${theme.vars.palette.primary.mainChannel} / ${OUTLINE_ALPHA})`,
801
+ "&:hover": {
802
+ borderColor: theme.vars.palette.primary.main,
803
+ backgroundColor: theme.vars.palette.action.hover
804
+ }
805
+ }),
806
+ text: ui.underlineLinks ? { "&:hover": { backgroundColor: "transparent", textDecoration: "underline", textUnderlineOffset: "3px" } } : void 0
807
+ },
808
+ variants: [containedPrimary]
809
+ },
810
+ MuiChip: {
811
+ styleOverrides: {
812
+ // minimals.cc <Label>: compact 24px pill, 6px radius, bold 12px text,
813
+ // tight side padding. Other themes keep their softer 600-weight chip.
814
+ root: ui.labelChips ? { borderRadius: 6, height: 24, fontWeight: 700, fontSize: "0.75rem", "& .MuiChip-label": { paddingInline: 6 } } : { borderRadius: ui.pillChips ? 999 : radii.chip, fontWeight: 600, paddingInline: 2 }
815
+ },
816
+ variants: chipVariants
817
+ },
818
+ /*
819
+ * microLabels — the label moves ABOVE the field as an uppercase
820
+ * micro-label, and that is a theme decision no page should have to know
821
+ * about. Forcing `shrink` lifts every label out of every field at once,
822
+ * dialogs and settings panes included, and collapsing the notch legend
823
+ * closes the outline back up behind it.
824
+ *
825
+ * These two entries are ONE MuiOutlinedInput key either way — a second
826
+ * one later in the object would silently win.
827
+ */
828
+ ...ui.microLabels ? {
829
+ MuiInputLabel: {
830
+ defaultProps: { shrink: true },
831
+ styleOverrides: {
832
+ root: ({ theme }) => ({
833
+ position: "static",
834
+ transform: "none",
835
+ maxWidth: "100%",
836
+ marginBottom: 5,
837
+ fontSize: "0.625rem",
838
+ fontWeight: 700,
839
+ textTransform: "uppercase",
840
+ letterSpacing: "0.09em",
841
+ color: theme.vars.palette.text.secondary,
842
+ // Not the accent on focus: at this size a colour change reads as
843
+ // an error rather than as focus, and the field's own rule already
844
+ // says where the caret is.
845
+ "&.Mui-focused": { color: theme.vars.palette.text.secondary }
846
+ })
847
+ }
848
+ },
849
+ MuiOutlinedInput: {
850
+ styleOverrides: { root: inputRoot, notchedOutline: { "& legend": { display: "none" } } }
851
+ }
852
+ } : {
853
+ MuiOutlinedInput: { styleOverrides: { root: inputRoot } }
854
+ },
855
+ /*
856
+ * The header micro-label is the THEME's, not the page's: a page writes
857
+ * <TableCell>Last name</TableCell> and nothing else, so if the register
858
+ * look lives anywhere it has to live here.
859
+ */
860
+ MuiTableCell: {
861
+ styleOverrides: {
862
+ root: ({ theme }) => ({
863
+ ...dense ? { padding: "7px 12px", fontSize: "0.75rem" } : {},
864
+ ...ui.tableRules === "columns" ? { borderRight: `1px solid ${theme.vars.palette.divider}`, "&:last-of-type": { borderRight: "none" } } : {},
865
+ /*
866
+ * A ROW RULE DRAWN FROM THE INK, not from `divider`.
867
+ *
868
+ * MUI rules table rows with `palette.divider`, and the panelled
869
+ * themes set that very light on purpose — it is tuned for a hairline
870
+ * BETWEEN PANELS on a tinted ground (#E1E7E5 on verdigris). At that
871
+ * weight it vanishes against white paper, so the table read as one
872
+ * undivided block. 12% of the text colour is a line you can follow
873
+ * across a wide row, and it still tracks the palette in both schemes.
874
+ *
875
+ * Bundled with `tableHead: 'band'` rather than given a flag of its
876
+ * own: a banded header, visible row rules and no column rules are
877
+ * one look — the panelled table — and splitting them would be two
878
+ * switches nobody would set differently.
879
+ */
880
+ ...ui.tableHead === "band" ? { borderBottom: `1px solid color-mix(in srgb, ${theme.vars.palette.text.primary} 12%, transparent)` } : {}
881
+ }),
882
+ /*
883
+ * TWO WEIGHTS OF THE SAME IDEA, and the difference is what carries
884
+ * the separation. 'rule' puts the caps over a 2px INK rule — the
885
+ * printed-register move, which needs the weight because its rows are
886
+ * ruled too. 'band' lets a tinted strip do the work instead, so the
887
+ * header reads as a header without a black line across a soft,
888
+ * rounded card.
889
+ *
890
+ * The caps themselves are the theme's, not the page's: a page writes
891
+ * <TableCell>Last name</TableCell> and nothing else, so this is the
892
+ * only place the treatment can live.
893
+ */
894
+ head: ({ theme }) => {
895
+ const tint = `color-mix(in srgb, ${theme.vars.palette.text.primary} 4%, transparent)`;
896
+ const band = {
897
+ backgroundColor: theme.vars.palette.background.paper,
898
+ backgroundImage: `linear-gradient(${tint}, ${tint})`
899
+ };
900
+ return {
901
+ // A dense theme keeps its tighter rows; it is a size step, not a
902
+ // different treatment.
903
+ fontSize: dense ? "0.8125rem" : "0.875rem",
904
+ fontWeight: 500,
905
+ color: palette.light.textHeading ? "var(--mui-palette-text-heading)" : theme.vars.palette.text.primary,
906
+ ...band,
907
+ /*
908
+ * The rule is the one thing the variants still disagree about,
909
+ * because it is drawn rather than set: `rule` is the printed
910
+ * register's heavy ink line, and it belongs to the two themes
911
+ * that rule their columns too.
912
+ */
913
+ borderBottom: ui.tableHead === "rule" ? `2px solid ${theme.vars.palette.text.primary}` : `1px solid ${theme.vars.palette.divider}`
914
+ };
915
+ }
916
+ }
917
+ },
918
+ /*
919
+ * Zebra INSTEAD of row rules, not on top of them — a banded table that
920
+ * also rules every row reads as a grid of boxes, so the band comes with
921
+ * the row border removed and the column rules carry the vertical
922
+ * structure.
923
+ *
924
+ * nth-of-type rather than nth-child: a caption or colgroup dropped in
925
+ * later would shift nth-child by one.
926
+ */
927
+ MuiTableRow: {
928
+ styleOverrides: {
929
+ root: ({ theme }) => ui.tableZebra ? {
930
+ "&:nth-of-type(odd) > .MuiTableCell-body": {
931
+ backgroundColor: `color-mix(in srgb, ${theme.vars.palette.text.primary} 4%, transparent)`
932
+ },
933
+ // The band separates; ruling every row as well would make a grid
934
+ // of boxes. Only `ledger` bands, and its columns are ruled.
935
+ "& > .MuiTableCell-body": { borderBottom: "none" },
936
+ /*
937
+ * HOVER HAS TO WIN, and it does not for free: the band is
938
+ * painted on the CELLS while MUI's `hover` prop colours the
939
+ * ROW, so an opaque cell sits on top of it and every other row
940
+ * silently stops responding to the pointer. Clearing the cells
941
+ * on hover lets the row's own colour through.
942
+ */
943
+ "&:hover > .MuiTableCell-body": { backgroundColor: "transparent" }
944
+ } : {}
945
+ }
946
+ },
947
+ MuiDialog: { defaultProps: { slotProps: { paper: { elevation: 0 } } }, styleOverrides: { paper: dialogPaper } },
948
+ /*
949
+ * berry: Berry's tabs are MUI's — an underline in the data hue (the
950
+ * secondary blue), a divider under the whole strip. The theme sets the
951
+ * colour so SectionTabs and any bare <Tabs> agree.
952
+ */
953
+ ...ui.tabs === "underline" ? {
954
+ MuiTabs: {
955
+ defaultProps: { indicatorColor: "secondary", textColor: "secondary" },
956
+ styleOverrides: { root: ({ theme }) => ({ "& .MuiTabs-list": { borderBottom: `1px solid ${theme.vars.palette.divider}` } }) }
957
+ },
958
+ MuiTab: { styleOverrides: { root: { textTransform: "capitalize", fontWeight: 500, minWidth: 90 } } }
959
+ } : {},
960
+ MuiMenu: {
961
+ styleOverrides: {
962
+ paper: ({ theme }) => ({
963
+ borderRadius: radii.menu,
964
+ border: `1px solid ${theme.vars.palette.divider}`,
965
+ boxShadow: flat ? "0 5px 16px -12px rgba(0,0,0,0.16)" : `0 7px 22px -10px rgba(${amb},0.19)`
966
+ })
967
+ }
968
+ }
969
+ }
970
+ });
971
+ }
972
+ export {
973
+ createSiteTheme as default
974
+ };