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:
BOHA
2026-06-07 17:37:11 +02:00
parent 2a228ea659
commit d424ef8c15
7 changed files with 215 additions and 425 deletions

207
src/admin/ui/RichText.tsx Normal file
View 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,
}));

View File

@@ -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";