This commit is contained in:
seb
2026-07-31 18:36:13 +02:00
parent ce8d7d04da
commit 083e48f621
15 changed files with 179 additions and 47 deletions

View File

@@ -12,7 +12,7 @@
body,
#root {
min-height: 100%;
background-color: #dceeea;
background-color: #c5ddd8;
}
html,

View File

@@ -1,2 +1,2 @@
// Mirrored in index.html and index.css for paint before JS loads.
export const MAIN_BACKGROUND = '#dceeea';
export const MAIN_BACKGROUND = '#c5ddd8';

View File

@@ -13,6 +13,7 @@ import {
primaryPrice,
} from '../../utils/articleDetail';
import { formatPrice } from '../../utils/price';
import { PANEL_TILE_KEYS } from '../../theme';
import { formatStock } from '../../pages/artikel/searchFormat';
function PanelValue({ label, children }) {
@@ -46,7 +47,6 @@ function ArticlePhoto() {
}
const headerTileSx = {
p: 1.25,
minHeight: 0,
};
@@ -99,6 +99,7 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
<PanelTileGrid>
<EditablePanelTile
wide={3}
tileKey={PANEL_TILE_KEYS.name}
minHeight={0}
sx={headerTileSx}
value={{ name: articleData.name ?? '' }}
@@ -145,21 +146,21 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
)}
/>
<PanelTile wide={1} minHeight={0} sx={headerTileSx}>
<PanelTile wide={1} tileKey={PANEL_TILE_KEYS.photo} minHeight={0} sx={headerTileSx}>
<ArticlePhoto />
</PanelTile>
<PanelTile wide={1} title="Bestand">
<PanelTile wide={1} tileKey={PANEL_TILE_KEYS.stock} title="Bestand">
<PanelValue>
{articleData.stock != null ? formatStock(articleData.stock) : null}
</PanelValue>
</PanelTile>
<PanelTile wide={1} title="Marke">
<PanelTile wide={1} tileKey={PANEL_TILE_KEYS.brand} title="Marke">
<PanelValue>{brandName(articleData)}</PanelValue>
</PanelTile>
<PanelTile wide={1} title="SKU / EAN">
<PanelTile wide={1} tileKey={PANEL_TILE_KEYS.identifiers} title="SKU / EAN">
<PanelValue label="SKU">{articleData.sku}</PanelValue>
{eans.map((ean) => (
<PanelValue key={ean} label="EAN">
@@ -168,7 +169,7 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
))}
</PanelTile>
<PanelTile wide={1} title="Preis">
<PanelTile wide={1} tileKey={PANEL_TILE_KEYS.price} title="Preis">
<PanelValue label="Netto">
{basePrice?.net != null ? formatPrice(basePrice.net) : null}
</PanelValue>
@@ -180,14 +181,14 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
</PanelValue>
</PanelTile>
<PanelTile wide={3} title="SEO & Beschreibung" minHeight={200}>
<PanelTile wide={3} tileKey={PANEL_TILE_KEYS.content} title="SEO & Beschreibung" minHeight={200}>
<PanelValue label="Slug">{articleData.slug}</PanelValue>
<PanelValue label="SEO-Titel">{articleData.seoTitle}</PanelValue>
<PanelValue label="SEO-Beschreibung">{articleData.seoDescription}</PanelValue>
<PanelValue label="Beschreibung">{articleData.description}</PanelValue>
</PanelTile>
<PanelTile wide={1} title="Listen" minHeight={200}>
<PanelTile wide={1} tileKey={PANEL_TILE_KEYS.lists} title="Listen" minHeight={200}>
<RelationLine
label="Vaterartikel"
active={articleData.childCount > 0}

View File

@@ -8,6 +8,7 @@ import CircularProgress from '@mui/material/CircularProgress';
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { injectMuiTheme } from './injectMuiTheme';
import { enqueueSnackbar } from 'notistack';
import { backendErrorFrom } from '../../utils/backendError';
import { panelTilePaperSx } from './panelTileLayout';
@@ -20,7 +21,7 @@ function valuesEqual(a, b) {
* Panel tile with centralized inline-edit chrome.
* Pass `onSave` to enable edit UI; omit it for read-only tiles (no edit icon).
*/
export class EditablePanelTile extends Component {
class EditablePanelTileBase extends Component {
constructor(props) {
super(props);
this.inputRef = createRef();
@@ -186,9 +187,11 @@ export class EditablePanelTile extends Component {
render() {
const {
theme,
wide = 1,
sx,
minHeight = 120,
tileKey,
renderDisplay,
renderEdit,
} = this.props;
@@ -198,8 +201,15 @@ export class EditablePanelTile extends Component {
return (
<Paper
variant="outlined"
sx={panelTilePaperSx({ wide, sx, minHeight, pending })}
elevation={0}
sx={panelTilePaperSx({
wide,
sx,
minHeight,
pending,
tileKey,
theme,
})}
>
{this.renderActions()}
{this.renderTitle()}
@@ -220,3 +230,5 @@ export class EditablePanelTile extends Component {
);
}
}
export const EditablePanelTile = injectMuiTheme(EditablePanelTileBase);

View File

@@ -2,9 +2,10 @@ import { Component } from 'react';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { injectMuiTheme } from './injectMuiTheme';
import { GRID_COLUMNS, panelTilePaperSx } from './panelTileLayout';
export class PanelTile extends Component {
class PanelTileBase extends Component {
renderTitle() {
const { title } = this.props;
if (!title) return null;
@@ -20,12 +21,18 @@ export class PanelTile extends Component {
}
render() {
const { wide = 1, children, sx, minHeight = 120 } = this.props;
const { theme, wide = 1, children, sx, minHeight = 120, tileKey } = this.props;
return (
<Paper
variant="outlined"
sx={panelTilePaperSx({ wide, sx, minHeight })}
elevation={0}
sx={panelTilePaperSx({
wide,
sx,
minHeight,
tileKey,
theme,
})}
>
{this.renderTitle()}
<Box sx={{ flex: 1, minWidth: 0 }}>{children}</Box>
@@ -34,6 +41,8 @@ export class PanelTile extends Component {
}
}
export const PanelTile = injectMuiTheme(PanelTileBase);
export function PanelTileGrid({ children, sx }) {
return (
<Box

View File

@@ -0,0 +1,13 @@
import { useTheme } from '@mui/material/styles';
/** Injects MUI theme into class components (replaces removed withTheme HOC). */
export function injectMuiTheme(Wrapped) {
function ComponentWithTheme(props) {
const theme = useTheme();
return <Wrapped {...props} theme={theme} />;
}
const name = Wrapped.displayName || Wrapped.name || 'Component';
ComponentWithTheme.displayName = `injectMuiTheme(${name})`;
return ComponentWithTheme;
}

View File

@@ -17,7 +17,21 @@ export function responsiveColumnSpan(wide) {
export const PENDING_EDIT_RING_PX = 4;
export function panelTilePaperSx({ wide, sx, minHeight = 120, pending = false }) {
export function panelTileBackground(theme, tileKey) {
if (!tileKey || !theme?.panelTiles) return undefined;
return theme.panelTiles[tileKey];
}
export function panelTilePaperSx({
wide,
sx,
minHeight = 120,
pending = false,
tileKey,
theme,
}) {
const tileBg = panelTileBackground(theme, tileKey);
return {
gridColumn: responsiveColumnSpan(wide),
position: 'relative',
@@ -25,6 +39,9 @@ export function panelTilePaperSx({ wide, sx, minHeight = 120, pending = false })
minHeight,
display: 'flex',
flexDirection: 'column',
border: 'none',
boxShadow: 'none',
...(tileBg ? { bgcolor: tileBg } : {}),
...(pending
? {
boxShadow: `inset 0 0 0 ${PENDING_EDIT_RING_PX}px ${PENDING_EDIT_BORDER}`,

View File

@@ -2,7 +2,7 @@ html,
body,
#root {
min-height: 100%;
background-color: #dceeea;
background-color: #c5ddd8;
}
html,

View File

@@ -6,7 +6,7 @@ import { SnackbarProvider } from 'notistack';
import AuthBootstrap from './components/AuthBootstrap';
import App from './App';
import { store } from './store';
import theme from './theme';
import { getActiveTheme } from './theme';
import './index.css';
async function clearDevServiceWorker() {
@@ -25,7 +25,7 @@ if (import.meta.env.DEV) {
createRoot(document.getElementById('root')).render(
<Provider store={store}>
<ThemeProvider theme={theme}>
<ThemeProvider theme={getActiveTheme()}>
<CssBaseline />
<SnackbarProvider
maxSnack={3}

View File

@@ -1,25 +0,0 @@
import { createTheme } from '@mui/material/styles';
import { MAIN_BACKGROUND } from './colors';
const nav = {
background: '#1a3533',
mainBackground: MAIN_BACKGROUND,
item: '#9cb8b4',
itemHover: '#243f3d',
itemHoverText: '#d4ebe8',
itemActive: '#2a5a56',
itemActiveText: '#6ee7d8',
itemActiveAccent: '#2dd4bf',
};
export default createTheme({
palette: {
mode: 'light',
primary: { main: '#0d9488' },
background: {
default: nav.mainBackground,
paper: '#ffffff',
},
},
nav,
});

14
src/theme/index.js Normal file
View File

@@ -0,0 +1,14 @@
export { PANEL_TILE_KEYS } from './panelTiles';
export {
getActiveTheme,
getActiveThemeId,
getTheme,
listThemeIds,
registerTheme,
setActiveTheme,
} from './themeManager';
export { createDefaultTheme, defaultThemeId } from './themes/default';
import { getActiveTheme } from './themeManager';
export default getActiveTheme();

11
src/theme/panelTiles.js Normal file
View File

@@ -0,0 +1,11 @@
/** Semantic keys for article view panel tile backgrounds. */
export const PANEL_TILE_KEYS = {
name: 'name',
photo: 'photo',
stock: 'stock',
brand: 'brand',
identifiers: 'identifiers',
price: 'price',
content: 'content',
lists: 'lists',
};

38
src/theme/themeManager.js Normal file
View File

@@ -0,0 +1,38 @@
import { createDefaultTheme, defaultThemeId } from './themes/default.js';
const themes = {
[defaultThemeId]: createDefaultTheme(),
};
let activeThemeId = defaultThemeId;
export function getTheme(themeId = activeThemeId) {
const theme = themes[themeId];
if (!theme) {
throw new Error(`Unknown theme: ${themeId}`);
}
return theme;
}
export function getActiveTheme() {
return getTheme(activeThemeId);
}
export function getActiveThemeId() {
return activeThemeId;
}
export function setActiveTheme(themeId) {
if (!themes[themeId]) {
throw new Error(`Unknown theme: ${themeId}`);
}
activeThemeId = themeId;
}
export function listThemeIds() {
return Object.keys(themes);
}
export function registerTheme(themeId, theme) {
themes[themeId] = theme;
}

View File

@@ -0,0 +1,42 @@
import { createTheme } from '@mui/material/styles';
import { MAIN_BACKGROUND } from '../../colors';
import { PANEL_TILE_KEYS } from '../panelTiles';
const nav = {
background: '#1a3533',
mainBackground: MAIN_BACKGROUND,
item: '#9cb8b4',
itemHover: '#243f3d',
itemHoverText: '#d4ebe8',
itemActive: '#2a5a56',
itemActiveText: '#6ee7d8',
itemActiveAccent: '#2dd4bf',
};
const panelTiles = {
[PANEL_TILE_KEYS.name]: '#ffffff',
[PANEL_TILE_KEYS.photo]: '#fde68a',
[PANEL_TILE_KEYS.stock]: '#bbf7d0',
[PANEL_TILE_KEYS.brand]: '#e9d5ff',
[PANEL_TILE_KEYS.identifiers]: '#e2e8f0',
[PANEL_TILE_KEYS.price]: '#bfdbfe',
[PANEL_TILE_KEYS.content]: '#99f6e4',
[PANEL_TILE_KEYS.lists]: '#fbcfe8',
};
export const defaultThemeId = 'default';
export function createDefaultTheme() {
return createTheme({
palette: {
mode: 'light',
primary: { main: '#0d9488' },
background: {
default: nav.mainBackground,
paper: '#ffffff',
},
},
nav,
panelTiles,
});
}

View File

@@ -134,7 +134,7 @@ export default defineConfig(({ command }) => ({
start_url: '/',
scope: '/',
display: 'standalone',
background_color: '#dceeea',
background_color: '#c5ddd8',
theme_color: '#3b5bdb',
icons: [
{