refactor(css): scope Quill editor into styled() and delete offers.css
Phase 3 of the "fully MUI" cleanup. - New src/admin/ui/RichText.tsx exports two theme-aware styled() wrappers: RichEditorRoot (the .admin-rich-editor Quill overrides) and RichTextView (the .admin-rich-text-view read-only output). All colors resolve via theme.vars; picker/tooltip shadows use theme.applyStyles for the light/dark difference; the active-button tint uses the primary channel. - Dropped the ~170 dead font/size picker rules from the old stylesheet — the editor toolbar exposes neither picker. Kept the .ql-font-* content classes (shared by editor + view) so legacy stored HTML still renders. - RichEditor uses <RichEditorRoot> (min-height still via --re-min-height); InvoiceDetail + OrderDetail use <RichTextView> for the sanitized output. - offers.css deleted; its import removed from AdminApp. tsc -b --noEmit and npm run build clean. Verified the editor in Chrome (light + dark): toolbar, editor surface, text, icon strokes and min-height all correct and theme-reactive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
207
src/admin/ui/RichText.tsx
Normal file
207
src/admin/ui/RichText.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
/**
|
||||
* Theme-aware styling for the Quill rich-text editor and its read-only
|
||||
* rendered output. Replaces the former `offers.css` — the overrides now layer
|
||||
* on top of `react-quill-new/dist/quill.snow.css` via scoped `styled()`
|
||||
* wrappers, resolving against `theme.vars` for light/dark.
|
||||
*
|
||||
* The font/size picker rules from the old stylesheet were dropped: the editor
|
||||
* toolbar (see RichEditor.tsx) exposes neither picker. The `.ql-font-*`
|
||||
* content classes are kept so legacy stored HTML still renders with its font.
|
||||
*/
|
||||
|
||||
// Font classes applied to content spans (legacy HTML). Shared by editor + view.
|
||||
const QL_FONT_CLASSES = {
|
||||
"& .ql-font-arial": { fontFamily: "Arial, sans-serif" },
|
||||
"& .ql-font-tahoma": { fontFamily: "Tahoma, sans-serif" },
|
||||
"& .ql-font-verdana": { fontFamily: "Verdana, sans-serif" },
|
||||
"& .ql-font-georgia": { fontFamily: "Georgia, serif" },
|
||||
"& .ql-font-times-new-roman": { fontFamily: '"Times New Roman", serif' },
|
||||
"& .ql-font-courier-new": { fontFamily: '"Courier New", monospace' },
|
||||
"& .ql-font-trebuchet-ms": { fontFamily: '"Trebuchet MS", sans-serif' },
|
||||
"& .ql-font-impact": { fontFamily: "Impact, sans-serif" },
|
||||
"& .ql-font-comic-sans-ms": { fontFamily: '"Comic Sans MS", cursive' },
|
||||
"& .ql-font-lucida-console": { fontFamily: '"Lucida Console", monospace' },
|
||||
"& .ql-font-palatino-linotype": { fontFamily: '"Palatino Linotype", serif' },
|
||||
"& .ql-font-garamond": { fontFamily: "Garamond, serif" },
|
||||
} as const;
|
||||
|
||||
/** Wrapper around <ReactQuill> — was `.admin-rich-editor`. */
|
||||
export const RichEditorRoot = styled("div")(({ theme }) => ({
|
||||
border: `1px solid ${theme.vars!.palette.divider}`,
|
||||
borderRadius: "0.5rem",
|
||||
overflow: "visible",
|
||||
|
||||
"& .quill": { display: "flex", flexDirection: "column" },
|
||||
|
||||
// Toolbar
|
||||
"& .ql-toolbar.ql-snow": {
|
||||
background: theme.vars!.palette.background.paper,
|
||||
border: "none",
|
||||
borderBottom: `1px solid ${theme.vars!.palette.divider}`,
|
||||
borderRadius: "0.5rem 0.5rem 0 0",
|
||||
padding: "0.5rem",
|
||||
flexWrap: "wrap",
|
||||
gap: "2px",
|
||||
},
|
||||
"& .ql-toolbar .ql-formats": { marginRight: "8px" },
|
||||
|
||||
// Toolbar icons (idle)
|
||||
"& .ql-snow .ql-stroke": { stroke: theme.vars!.palette.text.secondary },
|
||||
"& .ql-snow .ql-fill": { fill: theme.vars!.palette.text.secondary },
|
||||
"& .ql-snow .ql-picker-label": {
|
||||
color: theme.vars!.palette.text.secondary,
|
||||
borderColor: theme.vars!.palette.divider,
|
||||
},
|
||||
|
||||
// Toolbar icons (hover)
|
||||
"& .ql-snow button:hover .ql-stroke, & .ql-snow .ql-picker-label:hover .ql-stroke":
|
||||
{ stroke: theme.vars!.palette.text.primary },
|
||||
"& .ql-snow button:hover .ql-fill, & .ql-snow .ql-picker-label:hover .ql-fill":
|
||||
{ fill: theme.vars!.palette.text.primary },
|
||||
"& .ql-snow button:hover, & .ql-snow .ql-picker-label:hover": {
|
||||
color: theme.vars!.palette.text.primary,
|
||||
},
|
||||
|
||||
// Active state
|
||||
"& .ql-snow button.ql-active": {
|
||||
color: theme.vars!.palette.primary.main,
|
||||
background: `rgba(${theme.vars!.palette.primary.mainChannel} / 0.15)`,
|
||||
borderRadius: "4px",
|
||||
},
|
||||
"& .ql-snow button.ql-active .ql-stroke": {
|
||||
stroke: theme.vars!.palette.primary.main,
|
||||
},
|
||||
"& .ql-snow button.ql-active .ql-fill, & .ql-snow button.ql-active .ql-stroke.ql-fill":
|
||||
{ fill: theme.vars!.palette.primary.main },
|
||||
"& .ql-snow .ql-picker-item.ql-selected": {
|
||||
color: theme.vars!.palette.primary.main,
|
||||
},
|
||||
"& .ql-snow .ql-picker-label.ql-active": {
|
||||
color: theme.vars!.palette.primary.main,
|
||||
},
|
||||
"& .ql-snow .ql-picker-label.ql-active .ql-stroke": {
|
||||
stroke: theme.vars!.palette.primary.main,
|
||||
},
|
||||
|
||||
// Dropdowns (color / background / align pickers)
|
||||
"& .ql-snow .ql-picker-options": {
|
||||
background: theme.vars!.palette.background.default,
|
||||
border: `1px solid ${theme.vars!.palette.divider}`,
|
||||
borderRadius: "0.375rem",
|
||||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.08)",
|
||||
zIndex: 1000,
|
||||
padding: "0.25rem",
|
||||
...theme.applyStyles("dark", {
|
||||
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.3)",
|
||||
}),
|
||||
},
|
||||
"& .ql-snow .ql-picker-item": {
|
||||
color: theme.vars!.palette.text.secondary,
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderRadius: "0.25rem",
|
||||
},
|
||||
"& .ql-snow .ql-picker-item:hover": {
|
||||
color: theme.vars!.palette.text.primary,
|
||||
background: theme.vars!.palette.background.paper,
|
||||
},
|
||||
|
||||
// Editor area
|
||||
"& .ql-container.ql-snow": {
|
||||
border: "none",
|
||||
borderRadius: "0 0 0.5rem 0.5rem",
|
||||
fontFamily: "Tahoma, sans-serif",
|
||||
fontSize: "14px",
|
||||
},
|
||||
"& .ql-editor": {
|
||||
minHeight: "var(--re-min-height, 120px)",
|
||||
padding: "0.75rem",
|
||||
color: theme.vars!.palette.text.primary,
|
||||
lineHeight: 1.6,
|
||||
fontFamily: "Tahoma, sans-serif",
|
||||
fontSize: "14px",
|
||||
background: theme.vars!.palette.background.paper,
|
||||
},
|
||||
"& .ql-editor.ql-blank::before": {
|
||||
color: theme.vars!.palette.text.disabled,
|
||||
fontStyle: "normal",
|
||||
},
|
||||
"& .ql-editor ul, & .ql-editor ol": { paddingLeft: "1.5rem" },
|
||||
|
||||
// Color picker
|
||||
'& .ql-snow .ql-color-picker .ql-picker-options[aria-hidden="false"]': {
|
||||
width: "176px",
|
||||
padding: "0.375rem",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "2px",
|
||||
},
|
||||
"& .ql-snow .ql-color-picker .ql-picker-item": {
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
borderRadius: "2px",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
flexShrink: 0,
|
||||
},
|
||||
|
||||
// Tooltip (link editor)
|
||||
"& .ql-snow .ql-tooltip": {
|
||||
background: theme.vars!.palette.background.default,
|
||||
border: `1px solid ${theme.vars!.palette.divider}`,
|
||||
borderRadius: "0.375rem",
|
||||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.08)",
|
||||
color: theme.vars!.palette.text.primary,
|
||||
...theme.applyStyles("dark", {
|
||||
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.3)",
|
||||
}),
|
||||
},
|
||||
'& .ql-snow .ql-tooltip input[type="text"]': {
|
||||
background: theme.vars!.palette.background.paper,
|
||||
border: `1px solid ${theme.vars!.palette.divider}`,
|
||||
borderRadius: "0.25rem",
|
||||
color: theme.vars!.palette.text.primary,
|
||||
padding: "0.25rem 0.5rem",
|
||||
},
|
||||
"& .ql-snow .ql-tooltip a": { color: theme.vars!.palette.primary.main },
|
||||
|
||||
...QL_FONT_CLASSES,
|
||||
|
||||
// Keep picker dropdowns on-screen on narrow phones (the color picker is a
|
||||
// fixed 176px which overflows a ~360px viewport).
|
||||
"@media (max-width:480px)": {
|
||||
"& .ql-snow .ql-picker-options": {
|
||||
minWidth: 0,
|
||||
maxWidth: "calc(100vw - 32px)",
|
||||
},
|
||||
'& .ql-snow .ql-color-picker .ql-picker-options[aria-hidden="false"]': {
|
||||
width: "auto",
|
||||
maxWidth: "calc(100vw - 32px)",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
/** Read-only rendered Quill HTML output — was `.admin-rich-text-view`. */
|
||||
export const RichTextView = styled("div")(({ theme }) => ({
|
||||
color: theme.vars!.palette.text.secondary,
|
||||
lineHeight: 1.6,
|
||||
fontSize: "0.875rem",
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "break-word",
|
||||
minWidth: 0,
|
||||
|
||||
"& ul, & ol": { paddingLeft: "1.5rem", margin: "0.25rem 0 0.75rem" },
|
||||
"& li": { marginBottom: "0.15rem" },
|
||||
"& a": { color: theme.vars!.palette.primary.main },
|
||||
"& strong, & b": {
|
||||
fontWeight: 600,
|
||||
color: theme.vars!.palette.text.primary,
|
||||
display: "inline-block",
|
||||
marginTop: "0.5rem",
|
||||
},
|
||||
"& br + b, & br + strong": { marginTop: "0.75rem" },
|
||||
"& > br:first-child, & ul + br, & ol + br": { display: "none" },
|
||||
|
||||
...QL_FONT_CLASSES,
|
||||
}));
|
||||
@@ -29,3 +29,4 @@ export { default as ProgressBar } from "./ProgressBar";
|
||||
export type { ProgressColor } from "./ProgressBar";
|
||||
export { default as ThemeToggle } from "./ThemeToggle";
|
||||
export { default as PageEnter } from "./PageEnter";
|
||||
export { RichEditorRoot, RichTextView } from "./RichText";
|
||||
|
||||
Reference in New Issue
Block a user