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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/NOTICE +32 -0
- package/README.md +282 -0
- package/dist/components/AddressFields.js +83 -0
- package/dist/components/AuthCard.js +94 -0
- package/dist/components/AuthSplash.js +88 -0
- package/dist/components/BrandHeader.js +81 -0
- package/dist/components/BrandLink.js +22 -0
- package/dist/components/BrandMark.js +69 -0
- package/dist/components/BrandProvider.js +14 -0
- package/dist/components/Breadcrumbs.js +62 -0
- package/dist/components/CardHead.js +46 -0
- package/dist/components/ConfirmProvider.js +77 -0
- package/dist/components/DashboardLayout.js +645 -0
- package/dist/components/DataTable.js +546 -0
- package/dist/components/DetailRows.js +42 -0
- package/dist/components/EmptyNote.js +28 -0
- package/dist/components/Eyebrow.js +21 -0
- package/dist/components/HeadingHelp.js +38 -0
- package/dist/components/HeadingLevel.js +18 -0
- package/dist/components/IconTile.js +42 -0
- package/dist/components/ImagePreviewDialog.js +86 -0
- package/dist/components/LabeledField.js +156 -0
- package/dist/components/LanguageSwitcher.js +43 -0
- package/dist/components/ListRow.js +59 -0
- package/dist/components/ModeToggle.js +35 -0
- package/dist/components/PageHeader.js +65 -0
- package/dist/components/PasswordField.js +43 -0
- package/dist/components/PublicLayout.js +27 -0
- package/dist/components/PublicNav.js +17 -0
- package/dist/components/RouteMessage.js +31 -0
- package/dist/components/RowList.js +10 -0
- package/dist/components/SafeHtml.js +57 -0
- package/dist/components/SaveState.js +13 -0
- package/dist/components/SectionPanel.js +48 -0
- package/dist/components/SectionTabs.js +139 -0
- package/dist/components/SiteNav.js +130 -0
- package/dist/components/Skeletons.js +63 -0
- package/dist/components/Sortable.js +81 -0
- package/dist/components/StatTile.js +66 -0
- package/dist/components/StatusChip.js +17 -0
- package/dist/components/StatusDot.js +42 -0
- package/dist/components/SubCard.js +30 -0
- package/dist/components/TabbedCard.js +20 -0
- package/dist/components/TableFilters.js +247 -0
- package/dist/components/ToastProvider.js +65 -0
- package/dist/eslint/chipRules.mjs +65 -0
- package/dist/hooks/useDocumentTitle.js +19 -0
- package/dist/hooks/useOverflowTabs.js +53 -0
- package/dist/hooks/useUnsavedChanges.js +46 -0
- package/dist/i18n/loadMessages.js +15 -0
- package/dist/i18n/mergeMessages.js +13 -0
- package/dist/i18n/messages/en.json +103 -0
- package/dist/i18n/messages/es.json +103 -0
- package/dist/i18n/messages/zh.json +103 -0
- package/dist/lib/authCard.js +4 -0
- package/dist/lib/chipTone.js +46 -0
- package/dist/lib/countryRegionData.js +29 -0
- package/dist/lib/countryRegionData.json +1 -0
- package/dist/lib/navLock.js +22 -0
- package/dist/lib/navTabSx.js +78 -0
- package/dist/lib/orgTime.js +243 -0
- package/dist/lib/pageHtml.js +139 -0
- package/dist/lib/safeNext.js +13 -0
- package/dist/lib/sanitizeHtml.js +36 -0
- package/dist/lib/slugify.js +7 -0
- package/dist/lib/tableFilter.js +147 -0
- package/dist/lib/tableStyle.js +87 -0
- package/dist/test/jsdomShims.js +53 -0
- package/dist/test/themeContract.js +178 -0
- package/dist/theme/OrgThemeRegistry.js +71 -0
- package/dist/theme/ThemeRegistry.js +17 -0
- package/dist/theme/berryNavRow.js +21 -0
- package/dist/theme/createSiteTheme.js +974 -0
- package/dist/theme/themes.js +438 -0
- 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
|
+
};
|